Skip to main content

lux_lib/operations/install/
mod.rs

1use std::{
2    collections::{HashMap, HashSet},
3    io,
4    sync::Arc,
5};
6
7use crate::{
8    build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
9    config::Config,
10    lockfile::{
11        FlushLockfileError, LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState,
12        PinnedState, ReadOnly, ReadWrite,
13    },
14    lua_installation::{LuaInstallation, LuaInstallationError},
15    lua_rockspec::BuildBackendSpec,
16    lua_version::LuaVersionUnset,
17    luarocks::{
18        install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
19        luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
20    },
21    operations::resolve::{
22        build_dependency_names, PackageInstallData, Resolve, ResolveDependenciesError,
23    },
24    package::{PackageName, PackageNameList, PackageReq},
25    remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
26    rockspec::Rockspec,
27    tree::{self, InstallTree, Tree, TreeError},
28    workspace::{Workspace, WorkspaceTreeError},
29};
30
31pub use crate::operations::install::spec::PackageInstallSpec;
32
33use super::{DownloadedRockspec, RemoteRockDownload};
34use bon::Builder;
35use bytes::Bytes;
36use futures::stream::FuturesUnordered;
37use futures::StreamExt;
38use itertools::Itertools;
39use miette::Diagnostic;
40use thiserror::Error;
41
42use tracing::{info_span, span, Instrument};
43pub mod spec;
44
45/// A rocks package installer, providing fine-grained control
46/// over how packages should be installed.
47/// Can install multiple packages in parallel.
48#[derive(Builder)]
49#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
50pub struct Install<'a, T>
51where
52    T: InstallTree + Clone + Send + Sync,
53{
54    #[builder(start_fn)]
55    config: &'a Config,
56    #[builder(field)]
57    packages: Vec<PackageInstallSpec>,
58    #[builder(setters(name = "_tree", vis = ""))]
59    tree: T,
60    package_db: Option<RemotePackageDB>,
61}
62
63impl<'a, State> InstallBuilder<'a, Tree, State>
64where
65    State: install_builder::State,
66{
67    pub fn workspace(
68        self,
69        workspace: &'a Workspace,
70    ) -> Result<InstallBuilder<'a, Tree, install_builder::SetTree<State>>, WorkspaceTreeError>
71    where
72        State::Tree: install_builder::IsUnset,
73    {
74        let config = self.config;
75        Ok(self._tree(workspace.tree(config)?))
76    }
77}
78
79impl<'a, T, State> InstallBuilder<'a, T, State>
80where
81    State: install_builder::State,
82    T: InstallTree + Clone + Send + Sync,
83{
84    pub fn tree(self, tree: T) -> InstallBuilder<'a, T, install_builder::SetTree<State>>
85    where
86        State::Tree: install_builder::IsUnset,
87    {
88        self._tree(tree)
89    }
90
91    pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
92        Self { packages, ..self }
93    }
94
95    pub fn package(self, package: PackageInstallSpec) -> Self {
96        Self {
97            packages: self
98                .packages
99                .into_iter()
100                .chain(std::iter::once(package))
101                .collect(),
102            ..self
103        }
104    }
105}
106
107impl<State, T> InstallBuilder<'_, T, State>
108where
109    State: install_builder::State + install_builder::IsComplete,
110    T: InstallTree + Clone + Send + Sync + 'static,
111{
112    /// Install the packages.
113    pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
114        let install_built = self._build();
115        if install_built.packages.is_empty() {
116            return Ok(Vec::default());
117        }
118        let count = install_built.packages.len();
119        let span = if count > 1 {
120            info_span!("Installing", count,)
121        } else {
122            let install_spec = &install_built.packages[0];
123            info_span!("Installing", package = install_spec.package.to_string(),)
124        };
125        let _enter = span.enter();
126        let package_db = match install_built.package_db {
127            Some(db) => db,
128            None => RemotePackageDB::from_config(install_built.config).await?,
129        };
130
131        let duplicate_entrypoints = install_built
132            .packages
133            .iter()
134            .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
135            .map(|pkg| pkg.package.name())
136            .duplicates()
137            .cloned()
138            .collect_vec();
139
140        if !duplicate_entrypoints.is_empty() {
141            return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
142                duplicate_entrypoints,
143            )));
144        }
145
146        install_impl(
147            install_built.packages,
148            Arc::new(package_db),
149            install_built.config,
150            &install_built.tree,
151        )
152        .await
153    }
154}
155
156type InstallWorkerOutput = Result<(LocalPackageId, (LocalPackage, tree::EntryType)), InstallError>;
157
158#[derive(Error, Debug, Diagnostic)]
159pub enum InstallError {
160    #[error("unable to resolve dependencies:\n{0}")]
161    #[diagnostic(forward(0))]
162    ResolveDependencies(#[from] ResolveDependenciesError),
163    #[error(transparent)]
164    #[diagnostic(transparent)]
165    LuaVersionUnset(#[from] LuaVersionUnset),
166    #[error(transparent)]
167    #[diagnostic(transparent)]
168    LuaInstallation(#[from] LuaInstallationError),
169    #[error(transparent)]
170    #[diagnostic(transparent)]
171    FlushLockfile(#[from] FlushLockfileError),
172    #[error(transparent)]
173    #[diagnostic(transparent)]
174    Tree(#[from] TreeError),
175    #[error(transparent)]
176    #[diagnostic(transparent)]
177    WorkspaceTree(#[from] WorkspaceTreeError),
178    #[error("error instantiating LuaRocks compatibility layer:\n{0}")]
179    #[diagnostic(forward(0))]
180    LuaRocks(#[from] LuaRocksError),
181    #[error("error installing LuaRocks compatibility layer:\n{0}")]
182    #[diagnostic(forward(0))]
183    LuaRocksInstall(#[from] LuaRocksInstallError),
184    #[error("failed to build {0}: {1}")]
185    Build(PackageName, BuildError),
186    #[error("failed to install build depencency {0}:\n{1}")]
187    BuildDependency(PackageName, BuildError),
188    #[error("error initialising remote package DB:\n{0}")]
189    #[diagnostic(forward(0))]
190    RemotePackageDB(#[from] RemotePackageDBError),
191    #[error("failed to install pre-built rock {0}:\n{1}")]
192    InstallBinaryRock(PackageName, InstallBinaryRockError),
193    #[error("integrity error for package '{package}'")]
194    Integrity {
195        package: PackageName,
196        #[diagnostic_source]
197        err: RemotePackageDbIntegrityError,
198    },
199    #[error("cannot install duplicate entrypoints:\n{0}")]
200    DuplicateEntrypoints(PackageNameList),
201    #[error("install worker panicked")]
202    #[diagnostic(help(
203        r#"this is a bug in Lux, please report it, ideally with `RUST_BACKTRACE=1`.
204retrying with fewer parallel jobs (`--max-jobs`) may avoid the panic in the meantime"#
205    ))]
206    Join(#[from] tokio::task::JoinError),
207}
208
209// TODO(vhyrro): This function has too many arguments. Refactor it.
210#[allow(clippy::too_many_arguments)]
211async fn install_impl<T>(
212    packages: Vec<PackageInstallSpec>,
213    package_db: Arc<RemotePackageDB>,
214    config: &Config,
215    tree: &T,
216) -> Result<Vec<LocalPackage>, InstallError>
217where
218    T: InstallTree + Clone + Send + Sync + 'static,
219{
220    let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
221    let (build_dep_tx, mut build_dep_rx) = tokio::sync::mpsc::unbounded_channel();
222    let (build_dep_install_done_tx, mut build_dep_install_done_rx) =
223        tokio::sync::mpsc::unbounded_channel::<PackageName>();
224
225    let lockfile = tree.lockfile()?;
226    let build_lockfile = tree.build_tree(config)?.lockfile()?;
227
228    let lua = Arc::new(LuaInstallation::new_from_config(config).await?);
229
230    let mut resolve = tokio::spawn({
231        let config = config.clone();
232        let lockfile = Arc::new(lockfile.clone());
233        let build_lockfile = Arc::new(build_lockfile.clone());
234        async move {
235            Resolve::new()
236                .dependencies_tx(dep_tx)
237                .build_dependencies_tx(build_dep_tx)
238                .packages(packages)
239                .package_db(package_db)
240                .lockfile(lockfile)
241                .build_lockfile(build_lockfile)
242                .config(&config)
243                .get_all_dependencies()
244                .await?;
245            Ok::<(), InstallError>(())
246        }
247    })
248    .instrument(tracing::trace_span!("resolve_worker"));
249
250    // We have to install transitive build dependencies sequentially,
251    // because a build dependency can itself depend on other build dependencies.
252    let mut build_deps = tokio::spawn({
253        let config = config.clone();
254        let tree = tree.clone();
255        let lua = lua.clone();
256        async move {
257            while let Some(build_dep_spec) = build_dep_rx.recv().await {
258                let rockspec = build_dep_spec.downloaded_rock.rockspec();
259                let package = rockspec.package().clone();
260                let span = info_span!(
261                    "Installing build dependency",
262                    package = package.to_string(),
263                    version = rockspec.version().to_string()
264                );
265                async {
266                    let build_tree = tree.build_tree(&config)?;
267                    // We have to write to the build tree's lockfile after each build,
268                    // so that each transitive build dependency is available for the
269                    // next build dependencies that may depend on it.
270                    let mut build_lockfile = build_tree.lockfile()?.write_guard();
271                    let pkg = Build::new()
272                        .rockspec(rockspec)
273                        .lua(&lua)
274                        .tree(&build_tree)
275                        .entry_type(tree::EntryType::Entrypoint)
276                        .config(&config)
277                        .constraint(build_dep_spec.spec.constraint())
278                        .behaviour(build_dep_spec.build_behaviour)
279                        .build()
280                        .await
281                        .map_err(|err| InstallError::BuildDependency(package.clone(), err))?;
282                    build_lockfile.add_entrypoint(&pkg);
283                    Ok::<_, InstallError>(())
284                }
285                .instrument(span)
286                .await?;
287                let _ = build_dep_install_done_tx.send(package);
288            }
289            Ok::<(), InstallError>(())
290        }
291    })
292    .instrument(tracing::trace_span!("build_deps_worker"));
293
294    let mut all_packages: HashMap<LocalPackageId, PackageInstallData> = HashMap::new();
295    let mut scheduled: HashSet<LocalPackageId> = HashSet::new();
296    let mut installed_packages: HashMap<LocalPackageId, (LocalPackage, tree::EntryType)> =
297        HashMap::new();
298    let mut installed_build_deps: HashSet<PackageName> = HashSet::new();
299    let mut installs: FuturesUnordered<
300        tracing::instrument::Instrumented<tokio::task::JoinHandle<InstallWorkerOutput>>,
301    > = FuturesUnordered::new();
302    let mut resolve_done = false;
303    let mut build_deps_done = false;
304    let mut dep_rx_drained = false;
305    let mut build_dep_rx_drained = false;
306    let mut error: Option<InstallError> = None;
307    let max_jobs = config.max_jobs();
308
309    'install: loop {
310        if resolve_done && build_deps_done && dep_rx_drained && build_dep_rx_drained {
311            break;
312        }
313        tokio::select! {
314            resolve_result = &mut resolve, if !resolve_done => {
315                match resolve_result {
316                    Ok(Ok(_)) => resolve_done = true,
317                    Ok(Err(err)) => {
318                        error = Some(err);
319                        break 'install;
320                    }
321                    Err(join) => {
322                        error = Some(join.into());
323                        break 'install;
324                    }
325                }
326            }
327            build_deps_result = &mut build_deps, if !build_deps_done => {
328                match build_deps_result {
329                    Ok(Ok(_)) => build_deps_done = true,
330                    Ok(Err(err)) => {
331                        error = Some(err);
332                        break 'install;
333                    }
334                    Err(join) => {
335                        error = Some(join.into());
336                        break 'install;
337                    }
338                }
339            }
340            name = build_dep_install_done_rx.recv(), if !build_dep_rx_drained => {
341                if let Some(name) = name {
342                    installed_build_deps.insert(name);
343                } else {
344                    build_dep_rx_drained = true;
345                }
346            }
347            dep = dep_rx.recv(), if !dep_rx_drained => {
348                if let Some(dep) = dep {
349                    all_packages.insert(dep.spec.id(), dep);
350                } else {
351                    dep_rx_drained = true;
352                }
353            }
354        }
355
356        // Schedule installs for packages whose build dependencies have all
357        // been installed into the build tree.
358        // NOTE: Binary rocks don't need their build dependencies installed.
359        let ready: Vec<(LocalPackageId, PackageInstallData)> = all_packages
360            .iter()
361            .filter(|(id, data)| {
362                !scheduled.contains(*id)
363                    && match &data.downloaded_rock {
364                        RemoteRockDownload::BinaryRock { .. } => true,
365                        _ => build_dependencies_ready(
366                            data.downloaded_rock.rockspec(),
367                            data.build_behaviour,
368                            &build_lockfile,
369                            &installed_build_deps,
370                        ),
371                    }
372            })
373            .map(|(id, data)| (id.clone(), data.clone()))
374            .collect();
375
376        for (package_id, data) in ready {
377            if max_jobs > 0 && installs.len() >= max_jobs {
378                if let Some(result) = installs.next().await {
379                    match result {
380                        Ok(Ok(installed)) => {
381                            installed_packages.insert(installed.0, installed.1);
382                        }
383                        Ok(Err(err)) => {
384                            error = Some(err);
385                            break 'install;
386                        }
387                        Err(join) => {
388                            error = Some(join.into());
389                            break 'install;
390                        }
391                    }
392                }
393            }
394            scheduled.insert(package_id);
395            let config = config.clone();
396            let tree = tree.clone();
397            let lua = lua.clone();
398            installs.push(
399                tokio::spawn({
400                    async move {
401                        let pkg = match data.downloaded_rock {
402                            RemoteRockDownload::RockspecOnly { rockspec_download } => {
403                                install_rockspec(
404                                    rockspec_download,
405                                    None,
406                                    data.spec.constraint(),
407                                    data.build_behaviour,
408                                    data.pin,
409                                    data.opt,
410                                    data.entry_type,
411                                    &lua,
412                                    &tree,
413                                    &config,
414                                )
415                                .await?
416                            }
417                            RemoteRockDownload::BinaryRock {
418                                rockspec_download,
419                                packed_rock,
420                            } => {
421                                install_binary_rock(
422                                    rockspec_download,
423                                    packed_rock,
424                                    data.spec.constraint(),
425                                    data.build_behaviour,
426                                    data.pin,
427                                    data.opt,
428                                    data.entry_type,
429                                    &config,
430                                    &tree,
431                                )
432                                .await?
433                            }
434                            RemoteRockDownload::SrcRock {
435                                rockspec_download,
436                                src_rock,
437                                source_url,
438                            } => {
439                                let src_rock_source = SrcRockSource {
440                                    bytes: src_rock,
441                                    source_url,
442                                };
443                                install_rockspec(
444                                    rockspec_download,
445                                    Some(src_rock_source),
446                                    data.spec.constraint(),
447                                    data.build_behaviour,
448                                    data.pin,
449                                    data.opt,
450                                    data.entry_type,
451                                    &lua,
452                                    &tree,
453                                    &config,
454                                )
455                                .await?
456                            }
457                        };
458
459                        Ok::<_, InstallError>((pkg.id(), (pkg, data.entry_type)))
460                    }
461                })
462                .instrument(tracing::trace_span!("install_worker")),
463            );
464        }
465    }
466
467    if let Some(err) = error {
468        resolve.into_inner().abort();
469        build_deps.into_inner().abort();
470        for install in installs {
471            install.into_inner().abort();
472        }
473        return Err(err);
474    }
475
476    while let Some(result) = installs.next().await {
477        match result {
478            Ok(Ok(installed)) => {
479                installed_packages.insert(installed.0, installed.1);
480            }
481            Ok(Err(err)) => return Err(err),
482            Err(join) => return Err(join.into()),
483        }
484    }
485
486    let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
487                            id: &LocalPackageId,
488                            pkg: &LocalPackage,
489                            entry_type: tree::EntryType|
490     -> io::Result<()> {
491        if entry_type == tree::EntryType::Entrypoint {
492            lockfile.add_entrypoint(pkg);
493        }
494
495        for dependency_id in all_packages
496            .get(id)
497            .map(|pkg| pkg.spec.dependencies())
498            .unwrap_or_default()
499            .into_iter()
500        {
501            lockfile.add_dependency(
502                pkg,
503                installed_packages
504                    .get(dependency_id)
505                    .map(|(pkg, _)| pkg)
506                    .ok_or(io::Error::other(
507                        r#"
508error writing dependencies to the lockfile.
509A required dependency was not installed correctly.
510This is likely because an install thread panicked and was interrupted unexpectedly.
511
512[THIS IS A BUG!]
513"#,
514                    ))?,
515            );
516        }
517        Ok(())
518    };
519
520    lockfile.map_then_flush(|lockfile| {
521        for (id, (pkg, is_entrypoint)) in installed_packages.iter() {
522            write_dependency(lockfile, id, pkg, *is_entrypoint)?;
523        }
524        Ok::<_, io::Error>(())
525    })?;
526
527    Ok(installed_packages
528        .into_values()
529        .map(|(pkg, _)| pkg)
530        .collect_vec())
531}
532
533/// Whether all build dependencies of the given rockspec have been installed
534/// into the build tree, so that the package can start building.
535///
536/// A build dependency is considered ready when it has been freshly installed
537/// into the build tree, or when it was already present in the build lockfile
538/// and satisfies the dependency constraint.
539fn build_dependencies_ready(
540    rockspec: &impl Rockspec,
541    behaviour: BuildBehaviour,
542    build_lockfile: &Lockfile<ReadOnly>,
543    installed: &HashSet<PackageName>,
544) -> bool {
545    let build_deps = rockspec.build_dependencies().current_platform();
546    build_dependency_names(rockspec).iter().all(|name| {
547        installed.contains(name)
548            || (behaviour != BuildBehaviour::Force
549                && build_lockfile
550                    .has_rock(
551                        &build_deps
552                            .iter()
553                            .find(|dep| dep.name() == name)
554                            .map(|dep| dep.package_req().clone())
555                            .unwrap_or_else(|| PackageReq::from(name.clone())),
556                        None,
557                    )
558                    .is_some())
559    })
560}
561
562#[allow(clippy::too_many_arguments)]
563async fn install_rockspec<T>(
564    rockspec_download: DownloadedRockspec,
565    src_rock_source: Option<SrcRockSource>,
566    constraint: LockConstraint,
567    behaviour: BuildBehaviour,
568    pin: PinnedState,
569    opt: OptState,
570    entry_type: tree::EntryType,
571    lua: &LuaInstallation,
572    tree: &T,
573    config: &Config,
574) -> Result<LocalPackage, InstallError>
575where
576    T: InstallTree + Sync,
577{
578    let package = rockspec_download.rockspec.package().clone();
579    let rockspec = rockspec_download.rockspec;
580    let span = info_span!(
581        "Installing",
582        package = package.to_string(),
583        version = rockspec.version().to_string(),
584    );
585    let _enter = span.enter();
586    let source = rockspec_download.source;
587
588    if let Some(BuildBackendSpec::LuaRock(_)) = &rockspec.build().current_platform().build_backend {
589        let luarocks_tree = tree.build_tree(config)?;
590        let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
591        luarocks.ensure_installed(lua).await?;
592    }
593
594    let source_spec = match src_rock_source {
595        Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
596        None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
597    };
598
599    let pkg = Build::new()
600        .rockspec(&rockspec)
601        .lua(lua)
602        .tree(tree)
603        .entry_type(entry_type)
604        .config(config)
605        .pin(pin)
606        .opt(opt)
607        .constraint(constraint)
608        .behaviour(behaviour)
609        .source(source)
610        .source_spec(source_spec)
611        .build()
612        .await
613        .map_err(|err| InstallError::Build(package, err))?;
614    Ok(pkg)
615}
616
617#[allow(clippy::too_many_arguments)]
618async fn install_binary_rock(
619    rockspec_download: DownloadedRockspec,
620    packed_rock: Bytes,
621    constraint: LockConstraint,
622    behaviour: BuildBehaviour,
623    pin: PinnedState,
624    opt: OptState,
625    entry_type: tree::EntryType,
626    config: &Config,
627    tree: &impl InstallTree,
628) -> Result<LocalPackage, InstallError> {
629    let rockspec = rockspec_download.rockspec;
630    let package = rockspec.package().clone();
631    let span = span!(
632        tracing::Level::INFO,
633        "Installing (pre-built)",
634        package = package.to_string(),
635        version = rockspec.version().to_string(),
636    );
637    let _enter = span.enter();
638    let pkg = BinaryRockInstall::new(
639        &rockspec,
640        rockspec_download.source,
641        packed_rock,
642        entry_type,
643        config,
644        tree,
645    )
646    .pin(pin)
647    .opt(opt)
648    .constraint(constraint)
649    .behaviour(behaviour)
650    .install()
651    .await
652    .map_err(|err| InstallError::InstallBinaryRock(package, err))?;
653    Ok(pkg)
654}