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