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