Skip to main content

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