lux_lib/operations/install/
mod.rs

1use std::{collections::HashMap, io, sync::Arc};
2
3use crate::{
4    build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
5    config::{Config, LuaVersionUnset},
6    lockfile::{
7        LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState, PinnedState, ReadOnly,
8        ReadWrite,
9    },
10    lua_rockspec::BuildBackendSpec,
11    luarocks::{
12        install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
13        luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
14    },
15    operations::build_dependencies::InstallBuildDependencies,
16    package::{PackageName, PackageNameList},
17    progress::{MultiProgress, Progress, ProgressBar},
18    project::{Project, ProjectTreeError},
19    remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
20    rockspec::Rockspec,
21    tree::{self, Tree, TreeError},
22};
23
24pub use crate::operations::install::build_dependencies::InstallBuildDependenciesError;
25pub use crate::operations::install::spec::PackageInstallSpec;
26
27use bon::Builder;
28use bytes::Bytes;
29use futures::future::join_all;
30use itertools::Itertools;
31use thiserror::Error;
32
33use super::{
34    resolve::get_all_dependencies, DownloadedRockspec, RemoteRockDownload, SearchAndDownloadError,
35};
36
37pub mod spec;
38
39pub(crate) mod build_dependencies;
40
41/// A rocks package installer, providing fine-grained control
42/// over how packages should be installed.
43/// Can install multiple packages in parallel.
44#[derive(Builder)]
45#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
46pub struct Install<'a> {
47    #[builder(start_fn)]
48    config: &'a Config,
49    #[builder(field)]
50    packages: Vec<PackageInstallSpec>,
51    #[builder(setters(name = "_tree", vis = ""))]
52    tree: Tree,
53    package_db: Option<RemotePackageDB>,
54    progress: Option<Arc<Progress<MultiProgress>>>,
55}
56
57impl<'a, State> InstallBuilder<'a, State>
58where
59    State: install_builder::State,
60{
61    pub fn tree(self, tree: Tree) -> InstallBuilder<'a, install_builder::SetTree<State>>
62    where
63        State::Tree: install_builder::IsUnset,
64    {
65        self._tree(tree)
66    }
67
68    pub fn project(
69        self,
70        project: &'a Project,
71    ) -> Result<InstallBuilder<'a, install_builder::SetTree<State>>, ProjectTreeError>
72    where
73        State::Tree: install_builder::IsUnset,
74    {
75        let config = self.config;
76        Ok(self._tree(project.tree(config)?))
77    }
78
79    pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
80        Self { packages, ..self }
81    }
82
83    pub fn package(self, package: PackageInstallSpec) -> Self {
84        Self {
85            packages: self
86                .packages
87                .into_iter()
88                .chain(std::iter::once(package))
89                .collect(),
90            ..self
91        }
92    }
93}
94
95impl<State> InstallBuilder<'_, State>
96where
97    State: install_builder::State + install_builder::IsComplete,
98{
99    /// Install the packages.
100    pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
101        let install_built = self._build();
102        let progress = match install_built.progress {
103            Some(p) => p,
104            None => MultiProgress::new_arc(),
105        };
106        let package_db = match install_built.package_db {
107            Some(db) => db,
108            None => {
109                let bar = progress.map(|p| p.new_bar());
110                RemotePackageDB::from_config(install_built.config, &bar).await?
111            }
112        };
113
114        let duplicate_entrypoints = install_built
115            .packages
116            .iter()
117            .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
118            .map(|pkg| pkg.package.name())
119            .duplicates()
120            .cloned()
121            .collect_vec();
122
123        if !duplicate_entrypoints.is_empty() {
124            return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
125                duplicate_entrypoints,
126            )));
127        }
128
129        install_impl(
130            install_built.packages,
131            Arc::new(package_db),
132            install_built.config,
133            &install_built.tree,
134            install_built.tree.lockfile()?,
135            progress,
136        )
137        .await
138    }
139}
140
141#[derive(Error, Debug)]
142pub enum InstallError {
143    #[error(transparent)]
144    SearchAndDownloadError(#[from] SearchAndDownloadError),
145    #[error(transparent)]
146    LuaVersionUnset(#[from] LuaVersionUnset),
147    #[error(transparent)]
148    Io(#[from] io::Error),
149    #[error(transparent)]
150    Tree(#[from] TreeError),
151    #[error("error instantiating LuaRocks compatibility layer: {0}")]
152    LuaRocksError(#[from] LuaRocksError),
153    #[error("error installing LuaRocks compatibility layer: {0}")]
154    LuaRocksInstallError(#[from] LuaRocksInstallError),
155    #[error("error installing LuaRocks build dependencies: {0}")]
156    InstallBuildDependenciesError(#[from] InstallBuildDependenciesError),
157    #[error("failed to build {0}: {1}")]
158    BuildError(PackageName, BuildError),
159    #[error("error initialising remote package DB: {0}")]
160    RemotePackageDB(#[from] RemotePackageDBError),
161    #[error("failed to install pre-built rock {0}: {1}")]
162    InstallBinaryRockError(PackageName, InstallBinaryRockError),
163    #[error("integrity error for package {0}: {1}\n")]
164    Integrity(PackageName, RemotePackageDbIntegrityError),
165    #[error(transparent)]
166    ProjectTreeError(#[from] ProjectTreeError),
167    #[error("cannot install duplicate entrypoints: {0}")]
168    DuplicateEntrypoints(PackageNameList),
169}
170
171// TODO(vhyrro): This function has too many arguments. Refactor it.
172#[allow(clippy::too_many_arguments)]
173async fn install_impl(
174    packages: Vec<PackageInstallSpec>,
175    package_db: Arc<RemotePackageDB>,
176    config: &Config,
177    tree: &Tree,
178    lockfile: Lockfile<ReadOnly>,
179    progress_arc: Arc<Progress<MultiProgress>>,
180) -> Result<Vec<LocalPackage>, InstallError> {
181    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
182
183    get_all_dependencies(
184        tx,
185        packages,
186        package_db.clone(),
187        Arc::new(lockfile.clone()),
188        config,
189        progress_arc.clone(),
190    )
191    .await?;
192
193    let mut all_packages = HashMap::with_capacity(rx.len());
194
195    while let Some(dep) = rx.recv().await {
196        all_packages.insert(dep.spec.id(), dep);
197    }
198
199    let installed_packages = join_all(all_packages.clone().into_values().map(|install_spec| {
200        let progress_arc = progress_arc.clone();
201        let downloaded_rock = install_spec.downloaded_rock;
202        let config = config.clone();
203        let tree = tree.clone();
204
205        tokio::spawn({
206            let package_db = package_db.clone();
207            async move {
208                let rockspec = downloaded_rock.rockspec();
209                if let Some(BuildBackendSpec::LuaRock(build_backend)) =
210                    &rockspec.build().current_platform().build_backend
211                {
212                    let luarocks_tree = tree.build_tree(&config)?;
213                    let luarocks = LuaRocksInstallation::new(&config, luarocks_tree)?;
214                    luarocks
215                        .install_build_dependencies(build_backend, rockspec, progress_arc.clone())
216                        .await?;
217                } else {
218                    let build_dependencies = rockspec
219                        .build_dependencies()
220                        .current_platform()
221                        .to_vec()
222                        .into_iter()
223                        .map(|dep| {
224                            PackageInstallSpec::new(dep.package_req, tree::EntryType::Entrypoint)
225                                .build()
226                        })
227                        .collect_vec();
228                    if !build_dependencies.is_empty() {
229                        InstallBuildDependencies::new()
230                            .config(&config)
231                            .tree(&tree.build_tree(&config)?)
232                            .package_db(&package_db)
233                            .progress(progress_arc.clone())
234                            .packages(build_dependencies)
235                            .install()
236                            .await?;
237                    }
238                }
239
240                let pkg = match downloaded_rock {
241                    RemoteRockDownload::RockspecOnly { rockspec_download } => {
242                        install_rockspec(
243                            rockspec_download,
244                            None,
245                            install_spec.spec.constraint(),
246                            install_spec.build_behaviour,
247                            install_spec.pin,
248                            install_spec.opt,
249                            install_spec.entry_type,
250                            &tree,
251                            &config,
252                            progress_arc,
253                        )
254                        .await?
255                    }
256                    RemoteRockDownload::BinaryRock {
257                        rockspec_download,
258                        packed_rock,
259                    } => {
260                        install_binary_rock(
261                            rockspec_download,
262                            packed_rock,
263                            install_spec.spec.constraint(),
264                            install_spec.build_behaviour,
265                            install_spec.pin,
266                            install_spec.opt,
267                            install_spec.entry_type,
268                            &config,
269                            &tree,
270                            progress_arc,
271                        )
272                        .await?
273                    }
274                    RemoteRockDownload::SrcRock {
275                        rockspec_download,
276                        src_rock,
277                        source_url,
278                    } => {
279                        let src_rock_source = SrcRockSource {
280                            bytes: src_rock,
281                            source_url,
282                        };
283                        install_rockspec(
284                            rockspec_download,
285                            Some(src_rock_source),
286                            install_spec.spec.constraint(),
287                            install_spec.build_behaviour,
288                            install_spec.pin,
289                            install_spec.opt,
290                            install_spec.entry_type,
291                            &tree,
292                            &config,
293                            progress_arc,
294                        )
295                        .await?
296                    }
297                };
298
299                Ok::<_, InstallError>((pkg.id(), (pkg, install_spec.entry_type)))
300            }
301        })
302    }))
303    .await
304    .into_iter()
305    .flatten()
306    .try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
307
308    let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
309                            id: &LocalPackageId,
310                            pkg: &LocalPackage,
311                            entry_type: tree::EntryType| {
312        if entry_type == tree::EntryType::Entrypoint {
313            lockfile.add_entrypoint(pkg);
314        }
315
316        all_packages
317            .get(id)
318            .map(|pkg| pkg.spec.dependencies())
319            .unwrap_or_default()
320            .into_iter()
321            .for_each(|dependency_id| {
322                lockfile.add_dependency(
323                    pkg,
324                    installed_packages
325                        .get(dependency_id)
326                        .map(|(pkg, _)| pkg)
327                        // NOTE: This can happen if an install thread panics
328                        .expect("required dependency not found [This is a bug!]"),
329                );
330            });
331    };
332
333    lockfile.map_then_flush(|lockfile| {
334        installed_packages
335            .iter()
336            .for_each(|(id, (pkg, is_entrypoint))| {
337                write_dependency(lockfile, id, pkg, *is_entrypoint)
338            });
339
340        Ok::<_, io::Error>(())
341    })?;
342
343    Ok(installed_packages
344        .into_values()
345        .map(|(pkg, _)| pkg)
346        .collect_vec())
347}
348
349#[allow(clippy::too_many_arguments)]
350async fn install_rockspec(
351    rockspec_download: DownloadedRockspec,
352    src_rock_source: Option<SrcRockSource>,
353    constraint: LockConstraint,
354    behaviour: BuildBehaviour,
355    pin: PinnedState,
356    opt: OptState,
357    entry_type: tree::EntryType,
358    tree: &Tree,
359    config: &Config,
360    progress_arc: Arc<Progress<MultiProgress>>,
361) -> Result<LocalPackage, InstallError> {
362    let progress = Arc::clone(&progress_arc);
363    let rockspec = rockspec_download.rockspec;
364    let source = rockspec_download.source;
365    let package = rockspec.package().clone();
366    let bar = progress.map(|p| p.add(ProgressBar::from(format!("💻 Installing {}", &package,))));
367
368    if let Some(BuildBackendSpec::LuaRock(build_backend)) =
369        &rockspec.build().current_platform().build_backend
370    {
371        let luarocks_tree = tree.build_tree(config)?;
372        let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
373        luarocks.ensure_installed(&bar).await?;
374        luarocks
375            .install_build_dependencies(build_backend, &rockspec, progress_arc)
376            .await?;
377    }
378
379    let source_spec = match src_rock_source {
380        Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
381        None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
382    };
383
384    let pkg = Build::new(&rockspec, tree, entry_type, config, &bar)
385        .pin(pin)
386        .opt(opt)
387        .constraint(constraint)
388        .behaviour(behaviour)
389        .source(source)
390        .source_spec(source_spec)
391        .build()
392        .await
393        .map_err(|err| InstallError::BuildError(package, err))?;
394
395    bar.map(|b| b.finish_and_clear());
396
397    Ok(pkg)
398}
399
400#[allow(clippy::too_many_arguments)]
401async fn install_binary_rock(
402    rockspec_download: DownloadedRockspec,
403    packed_rock: Bytes,
404    constraint: LockConstraint,
405    behaviour: BuildBehaviour,
406    pin: PinnedState,
407    opt: OptState,
408    entry_type: tree::EntryType,
409    config: &Config,
410    tree: &Tree,
411    progress_arc: Arc<Progress<MultiProgress>>,
412) -> Result<LocalPackage, InstallError> {
413    let progress = Arc::clone(&progress_arc);
414    let rockspec = rockspec_download.rockspec;
415    let package = rockspec.package().clone();
416    let bar = progress.map(|p| {
417        p.add(ProgressBar::from(format!(
418            "💻 Installing {} (pre-built)",
419            &package,
420        )))
421    });
422    let pkg = BinaryRockInstall::new(
423        &rockspec,
424        rockspec_download.source,
425        packed_rock,
426        entry_type,
427        config,
428        tree,
429        &bar,
430    )
431    .pin(pin)
432    .opt(opt)
433    .constraint(constraint)
434    .behaviour(behaviour)
435    .install()
436    .await
437    .map_err(|err| InstallError::InstallBinaryRockError(package, err))?;
438
439    bar.map(|b| b.finish_and_clear());
440
441    Ok(pkg)
442}