use std::{collections::HashMap, io, sync::Arc};
use crate::{
build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
config::Config,
lockfile::{
FlushLockfileError, LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState,
PinnedState, ReadWrite,
},
lua_installation::{LuaInstallation, LuaInstallationError},
lua_rockspec::BuildBackendSpec,
lua_version::LuaVersionUnset,
luarocks::{
install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
},
operations::resolve::{Resolve, ResolveDependenciesError},
package::{PackageName, PackageNameList},
progress::{MultiProgress, Progress, ProgressBar},
project::{Project, ProjectTreeError},
remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
rockspec::Rockspec,
tree::{self, Tree, TreeError},
};
pub use crate::operations::install::spec::PackageInstallSpec;
use bon::Builder;
use bytes::Bytes;
use futures::StreamExt;
use itertools::Itertools;
use thiserror::Error;
use super::{DownloadedRockspec, RemoteRockDownload};
pub mod spec;
#[derive(Builder)]
#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
pub struct Install<'a> {
#[builder(start_fn)]
config: &'a Config,
#[builder(field)]
packages: Vec<PackageInstallSpec>,
#[builder(setters(name = "_tree", vis = ""))]
tree: Tree,
package_db: Option<RemotePackageDB>,
progress: Option<Arc<Progress<MultiProgress>>>,
}
impl<'a, State> InstallBuilder<'a, State>
where
State: install_builder::State,
{
pub fn tree(self, tree: Tree) -> InstallBuilder<'a, install_builder::SetTree<State>>
where
State::Tree: install_builder::IsUnset,
{
self._tree(tree)
}
pub fn project(
self,
project: &'a Project,
) -> Result<InstallBuilder<'a, install_builder::SetTree<State>>, ProjectTreeError>
where
State::Tree: install_builder::IsUnset,
{
let config = self.config;
Ok(self._tree(project.tree(config)?))
}
pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
Self { packages, ..self }
}
pub fn package(self, package: PackageInstallSpec) -> Self {
Self {
packages: self
.packages
.into_iter()
.chain(std::iter::once(package))
.collect(),
..self
}
}
}
impl<State> InstallBuilder<'_, State>
where
State: install_builder::State + install_builder::IsComplete,
{
pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
let install_built = self._build();
if install_built.packages.is_empty() {
return Ok(Vec::default());
}
let progress = match install_built.progress {
Some(p) => p,
None => MultiProgress::new_arc(install_built.config),
};
let package_db = match install_built.package_db {
Some(db) => db,
None => {
let bar = progress.map(|p| p.new_bar());
RemotePackageDB::from_config(install_built.config, &bar).await?
}
};
let duplicate_entrypoints = install_built
.packages
.iter()
.filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
.map(|pkg| pkg.package.name())
.duplicates()
.cloned()
.collect_vec();
if !duplicate_entrypoints.is_empty() {
return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
duplicate_entrypoints,
)));
}
install_impl(
install_built.packages,
Arc::new(package_db),
install_built.config,
&install_built.tree,
progress,
)
.await
}
}
#[derive(Error, Debug)]
pub enum InstallError {
#[error("unable to resolve dependencies:\n{0}")]
ResolveDependencies(#[from] ResolveDependenciesError),
#[error(transparent)]
LuaVersionUnset(#[from] LuaVersionUnset),
#[error(transparent)]
LuaInstallation(#[from] LuaInstallationError),
#[error(transparent)]
FlushLockfile(#[from] FlushLockfileError),
#[error(transparent)]
Tree(#[from] TreeError),
#[error("error instantiating LuaRocks compatibility layer:\n{0}")]
LuaRocksError(#[from] LuaRocksError),
#[error("error installing LuaRocks compatibility layer:\n{0}")]
LuaRocksInstallError(#[from] LuaRocksInstallError),
#[error("failed to build {0}: {1}")]
BuildError(PackageName, BuildError),
#[error("failed to install build depencency {0}:\n{1}")]
BuildDependencyError(PackageName, BuildError),
#[error("error initialising remote package DB:\n{0}")]
RemotePackageDB(#[from] RemotePackageDBError),
#[error("failed to install pre-built rock {0}:\n{1}")]
InstallBinaryRockError(PackageName, InstallBinaryRockError),
#[error("integrity error for package {0}:\n{1}")]
Integrity(PackageName, RemotePackageDbIntegrityError),
#[error(transparent)]
ProjectTreeError(#[from] ProjectTreeError),
#[error("cannot install duplicate entrypoints:\n{0}")]
DuplicateEntrypoints(PackageNameList),
}
#[allow(clippy::too_many_arguments)]
async fn install_impl(
packages: Vec<PackageInstallSpec>,
package_db: Arc<RemotePackageDB>,
config: &Config,
tree: &Tree,
progress_arc: Arc<Progress<MultiProgress>>,
) -> Result<Vec<LocalPackage>, InstallError> {
let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
let (build_dep_tx, mut build_dep_rx) = tokio::sync::mpsc::unbounded_channel();
let lockfile = tree.lockfile()?;
let build_lockfile = tree.build_tree(config)?.lockfile()?;
Resolve::new()
.dependencies_tx(dep_tx)
.build_dependencies_tx(build_dep_tx)
.packages(packages)
.package_db(package_db.clone())
.lockfile(Arc::new(lockfile.clone()))
.build_lockfile(Arc::new(build_lockfile.clone()))
.config(config)
.progress(progress_arc.clone())
.get_all_dependencies()
.await?;
let lua = Arc::new(
LuaInstallation::new_from_config(config, &progress_arc.map(|progress| progress.new_bar()))
.await?,
);
while let Some(build_dep_spec) = build_dep_rx.recv().await {
let rockspec = build_dep_spec.downloaded_rock.rockspec();
let bar = progress_arc.map(|p| {
p.add(ProgressBar::from(format!(
"💻 Installing build dependency: {}",
build_dep_spec.downloaded_rock.rockspec().package(),
)))
});
let package = rockspec.package().clone();
let build_tree = tree.build_tree(config)?;
let mut build_lockfile = build_tree.lockfile()?.write_guard();
let pkg = Build::new()
.rockspec(rockspec)
.lua(&lua)
.tree(&build_tree)
.entry_type(tree::EntryType::Entrypoint)
.config(config)
.progress(&bar)
.constraint(build_dep_spec.spec.constraint())
.behaviour(build_dep_spec.build_behaviour)
.build()
.await
.map_err(|err| InstallError::BuildDependencyError(package, err))?;
build_lockfile.add_entrypoint(&pkg);
}
let mut all_packages = HashMap::with_capacity(dep_rx.len());
while let Some(dep) = dep_rx.recv().await {
all_packages.insert(dep.spec.id(), dep);
}
let installed_packages =
futures::stream::iter(all_packages.clone().into_values().map(|install_spec| {
let progress_arc = progress_arc.clone();
let downloaded_rock = install_spec.downloaded_rock;
let config = config.clone();
let tree = tree.clone();
let lua = lua.clone();
tokio::spawn({
async move {
let pkg = match downloaded_rock {
RemoteRockDownload::RockspecOnly { rockspec_download } => {
install_rockspec(
rockspec_download,
None,
install_spec.spec.constraint(),
install_spec.build_behaviour,
install_spec.pin,
install_spec.opt,
install_spec.entry_type,
&lua,
&tree,
&config,
progress_arc,
)
.await?
}
RemoteRockDownload::BinaryRock {
rockspec_download,
packed_rock,
} => {
install_binary_rock(
rockspec_download,
packed_rock,
install_spec.spec.constraint(),
install_spec.build_behaviour,
install_spec.pin,
install_spec.opt,
install_spec.entry_type,
&config,
&tree,
progress_arc,
)
.await?
}
RemoteRockDownload::SrcRock {
rockspec_download,
src_rock,
source_url,
} => {
let src_rock_source = SrcRockSource {
bytes: src_rock,
source_url,
};
install_rockspec(
rockspec_download,
Some(src_rock_source),
install_spec.spec.constraint(),
install_spec.build_behaviour,
install_spec.pin,
install_spec.opt,
install_spec.entry_type,
&lua,
&tree,
&config,
progress_arc,
)
.await?
}
};
Ok::<_, InstallError>((pkg.id(), (pkg, install_spec.entry_type)))
}
})
}))
.buffered(config.max_jobs())
.collect::<Vec<_>>()
.await
.into_iter()
.flatten()
.try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
id: &LocalPackageId,
pkg: &LocalPackage,
entry_type: tree::EntryType|
-> io::Result<()> {
if entry_type == tree::EntryType::Entrypoint {
lockfile.add_entrypoint(pkg);
}
for dependency_id in all_packages
.get(id)
.map(|pkg| pkg.spec.dependencies())
.unwrap_or_default()
.into_iter()
{
lockfile.add_dependency(
pkg,
installed_packages
.get(dependency_id)
.map(|(pkg, _)| pkg)
.ok_or(io::Error::other(
r#"
error writing dependencies to the lockfile.
A required dependency was not installed correctly.
This is likely because an install thread panicked and was interrupted unexpectedly.
[THIS IS A BUG!]
"#,
))?,
);
}
Ok(())
};
lockfile.map_then_flush(|lockfile| {
for (id, (pkg, is_entrypoint)) in installed_packages.iter() {
write_dependency(lockfile, id, pkg, *is_entrypoint)?;
}
Ok::<_, io::Error>(())
})?;
Ok(installed_packages
.into_values()
.map(|(pkg, _)| pkg)
.collect_vec())
}
#[allow(clippy::too_many_arguments)]
async fn install_rockspec(
rockspec_download: DownloadedRockspec,
src_rock_source: Option<SrcRockSource>,
constraint: LockConstraint,
behaviour: BuildBehaviour,
pin: PinnedState,
opt: OptState,
entry_type: tree::EntryType,
lua: &LuaInstallation,
tree: &Tree,
config: &Config,
progress_arc: Arc<Progress<MultiProgress>>,
) -> Result<LocalPackage, InstallError> {
let progress = Arc::clone(&progress_arc);
let rockspec = rockspec_download.rockspec;
let source = rockspec_download.source;
let package = rockspec.package().clone();
let bar = progress.map(|p| p.add(ProgressBar::from(format!("💻 Installing {}", &package,))));
if let Some(BuildBackendSpec::LuaRock(_)) = &rockspec.build().current_platform().build_backend {
let luarocks_tree = tree.build_tree(config)?;
let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
luarocks.ensure_installed(lua, &bar).await?;
}
let source_spec = match src_rock_source {
Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
};
let pkg = Build::new()
.rockspec(&rockspec)
.lua(lua)
.tree(tree)
.entry_type(entry_type)
.config(config)
.progress(&bar)
.pin(pin)
.opt(opt)
.constraint(constraint)
.behaviour(behaviour)
.source(source)
.source_spec(source_spec)
.build()
.await
.map_err(|err| InstallError::BuildError(package, err))?;
bar.map(|b| b.finish_and_clear());
Ok(pkg)
}
#[allow(clippy::too_many_arguments)]
async fn install_binary_rock(
rockspec_download: DownloadedRockspec,
packed_rock: Bytes,
constraint: LockConstraint,
behaviour: BuildBehaviour,
pin: PinnedState,
opt: OptState,
entry_type: tree::EntryType,
config: &Config,
tree: &Tree,
progress_arc: Arc<Progress<MultiProgress>>,
) -> Result<LocalPackage, InstallError> {
let progress = Arc::clone(&progress_arc);
let rockspec = rockspec_download.rockspec;
let package = rockspec.package().clone();
let bar = progress.map(|p| {
p.add(ProgressBar::from(format!(
"💻 Installing {} (pre-built)",
&package,
)))
});
let pkg = BinaryRockInstall::new(
&rockspec,
rockspec_download.source,
packed_rock,
entry_type,
config,
tree,
&bar,
)
.pin(pin)
.opt(opt)
.constraint(constraint)
.behaviour(behaviour)
.install()
.await
.map_err(|err| InstallError::InstallBinaryRockError(package, err))?;
bar.map(|b| b.finish_and_clear());
Ok(pkg)
}