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