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