Skip to main content

lux_lib/drivers/
install_packages.rs

1use crate::package;
2use std::{collections::HashMap, io};
3
4use bon::Builder;
5use itertools::Itertools;
6use miette::Diagnostic;
7use thiserror::Error;
8use tracing::Instrument;
9
10use crate::{
11    build::BuildBehaviour,
12    config::Config,
13    lockfile::{
14        FlushLockfileError, LockedPackage, LockedPackageId, LockedPackageLockType, Lockfile,
15        ReadWrite,
16    },
17    lua_installation,
18    luarocks::luarocks_installation::{LuaRocksError, LuaRocksInstallError},
19    package_db::{self, PackageDB},
20    tree::{self, InstallTree},
21    workspace::TreeError,
22};
23
24use crate::pipeline::{
25    build::{Build as PipelineBuild, Error as PipelineBuildError},
26    emit_lockfile::LockfileHandle,
27    materialise::{Error as MaterialiseError, Materialise},
28    resolve::{self, ResolvePackageDependencies},
29};
30
31use crate::operations::PackageInstallSpec;
32
33/// Installs a set of [`PackageInstallSpec`]s into a tree.
34///
35/// Returns a [`LockfileHandle`] so changes can be committed into
36/// the correct lockfile.
37#[derive(Builder)]
38#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
39pub struct InstallPackages<'a, T>
40where
41    T: InstallTree + Send + Sync,
42{
43    #[builder(start_fn)]
44    config: &'a Config,
45    #[builder(start_fn)]
46    tree: &'a T,
47    #[builder(field)]
48    build_packages: Vec<PackageInstallSpec>,
49    #[builder(field)]
50    test_packages: Option<Vec<PackageInstallSpec>>,
51    #[builder(field)]
52    packages: Vec<PackageInstallSpec>,
53    package_db: PackageDB,
54}
55
56impl<'a, T, State> InstallPackagesBuilder<'a, T, State>
57where
58    T: InstallTree + Send + Sync,
59    State: install_packages_builder::State,
60{
61    pub fn packages(mut self, packages: Vec<PackageInstallSpec>) -> Self {
62        self.packages = packages;
63        self
64    }
65
66    pub fn package(mut self, package: PackageInstallSpec) -> Self {
67        self.packages.push(package);
68        self
69    }
70
71    pub fn build_packages(mut self, packages: Vec<PackageInstallSpec>) -> Self {
72        self.build_packages = packages;
73        self
74    }
75
76    pub fn test_packages(mut self, packages: Vec<PackageInstallSpec>) -> Self {
77        self.test_packages = Some(packages);
78        self
79    }
80}
81
82impl<T, State> InstallPackagesBuilder<'_, T, State>
83where
84    State: install_packages_builder::State + install_packages_builder::IsComplete,
85    T: InstallTree + Send + Sync,
86{
87    pub async fn install(self) -> Result<(Vec<LockedPackage>, LockfileHandle), Error> {
88        let args = self._build();
89        if args.packages.is_empty()
90            && args.build_packages.is_empty()
91            && args.test_packages.is_none()
92        {
93            return Ok((Vec::new(), LockfileHandle::default()));
94        }
95        let span = match args.packages.as_slice() {
96            [install_spec] => {
97                tracing::info_span!("Installing", package = install_spec.package.to_string())
98            }
99            packages => tracing::info_span!("Installing", count = packages.len()),
100        };
101        install_packages(args).instrument(span).await
102    }
103}
104
105async fn install_packages<T>(
106    install: InstallPackages<'_, T>,
107) -> Result<(Vec<LockedPackage>, LockfileHandle), Error>
108where
109    T: InstallTree + Send + Sync,
110{
111    let package_db = install.package_db;
112
113    let duplicate_entrypoints = install
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(Error::DuplicateEntrypoints(package::List::new(
124            duplicate_entrypoints,
125        )));
126    }
127
128    let packages = install.packages;
129    let config = install.config;
130    let tree = install.tree;
131
132    let lockfile = tree.lockfile()?;
133    let build_tree = tree.build_tree(config)?;
134    let build_lockfile = build_tree.lockfile()?;
135
136    let mut installed: HashMap<LockedPackageLockType, Vec<LockedPackage>> = HashMap::new();
137    installed.insert(
138        LockedPackageLockType::Regular,
139        lockfile.rocks().values().cloned().collect_vec(),
140    );
141    installed.insert(
142        LockedPackageLockType::Build,
143        build_lockfile.rocks().values().cloned().collect_vec(),
144    );
145    if install.test_packages.is_some() {
146        installed.insert(
147            LockedPackageLockType::Test,
148            tree.test_tree(config)?
149                .lockfile()?
150                .rocks()
151                .values()
152                .cloned()
153                .collect_vec(),
154        );
155    }
156
157    let mut resolve = ResolvePackageDependencies::new(package_db, config)
158        .installed(installed)
159        .packages(packages)
160        .build_packages(install.build_packages);
161    if let Some(test_packages) = install.test_packages {
162        resolve = resolve.test_packages(test_packages);
163    }
164    let resolved = resolve.resolve().await?;
165    let artifacts = Materialise::new(config)
166        .resolved(resolved)
167        .materialise()
168        .await?;
169    let handle = LockfileHandle::from_artifacts(&artifacts);
170
171    let build_packages = artifacts
172        .build
173        .unwrap_or_default()
174        .into_values()
175        .collect_vec();
176    let regular_packages = artifacts
177        .regular
178        .unwrap_or_default()
179        .into_values()
180        .collect_vec();
181
182    // Entrypoints that are already installed. The requested build behaviour decides how to
183    // react to them.
184    let mut conflicting_entrypoints: HashMap<LockedPackageId, LockedPackage> = HashMap::new();
185    let mut unforced_conflicts = Vec::new();
186    for package in regular_packages
187        .iter()
188        .filter(|package| package.entry_type == tree::EntryType::Entrypoint)
189    {
190        match lockfile.entrypoint(package.package.name()) {
191            None => continue,
192            Some(existing) => match package.build_behaviour {
193                // Forced installs replace the existing entrypoint.
194                BuildBehaviour::Force => {
195                    conflicting_entrypoints.insert(existing.id(), existing.clone());
196                }
197                // `Ignore` tolerates re-installing the exact same version.
198                BuildBehaviour::Ignore if existing.version() == package.package.version() => {}
199                // Any other existing entrypoint is a conflict.
200                BuildBehaviour::Ignore | BuildBehaviour::Conflict => {
201                    unforced_conflicts.push(format!("{}@{}", existing.name(), existing.version()));
202                }
203            },
204        }
205    }
206
207    if !unforced_conflicts.is_empty() {
208        return Err(Error::ConflictingEntrypoints(unforced_conflicts.join("\n")));
209    }
210
211    // FIXME(vhyrro): non-transactional. If an error occurs this removes the conflicting package
212    // without installing the substitute. Implement transactions at some point.
213    let conflicting_entrypoints = conflicting_entrypoints.into_values().collect_vec();
214    for package in &conflicting_entrypoints {
215        tree.cleanup(package, tree::EntryType::Entrypoint)?;
216    }
217
218    let regular_entry_types: HashMap<LockedPackageId, tree::EntryType> = regular_packages
219        .iter()
220        .map(|pkg| (pkg.package.spec.id(), pkg.entry_type))
221        .collect();
222
223    // Build dependencies first, then the packages themselves.
224    let built_build_deps = PipelineBuild::new(config, &build_tree)
225        .packages(build_packages)
226        .build()
227        .await?;
228    let built = PipelineBuild::new(config, tree)
229        .packages(regular_packages)
230        .build()
231        .await?;
232    if let Some(test_packages) = artifacts.test {
233        let test_tree = tree.test_tree(config)?;
234        PipelineBuild::new(config, &test_tree)
235            .packages(test_packages.into_values().collect_vec())
236            .build()
237            .await?;
238    }
239
240    let mut installed_packages: HashMap<LockedPackageId, LockedPackage> = lockfile
241        .rocks()
242        .iter()
243        .map(|(id, package)| (id.clone(), package.clone()))
244        .collect();
245    installed_packages.extend(built.iter().map(|pkg| (pkg.spec().id(), pkg.clone())));
246    let mut installed_build_deps: HashMap<LockedPackageId, LockedPackage> = build_lockfile
247        .rocks()
248        .iter()
249        .map(|(id, package)| (id.clone(), package.clone()))
250        .collect();
251    installed_build_deps.extend(
252        built_build_deps
253            .iter()
254            .map(|pkg| (pkg.spec().id(), pkg.clone())),
255    );
256
257    // NOTE: re-read the lockfile here. `lockfile` was loaded before the builds ran,
258    // and flushing that snapshot would drop the binaries each build recorded.
259    tree.lockfile()?.map_then_flush(|lockfile| {
260        for package in &conflicting_entrypoints {
261            // A rebuilt package already recorded its own binaries during the build.
262            // Removing it here would drop them again.
263            if regular_entry_types.contains_key(&package.id()) {
264                continue;
265            }
266            lockfile.remove_by_id(&package.id());
267        }
268        for package in &built {
269            let entry_type = regular_entry_types[&package.spec().id()];
270            lockfile.add_dependencies(package, entry_type, &installed_packages)?;
271            lockfile.add_build_dependencies(package, &installed_build_deps)?;
272        }
273        Ok::<_, io::Error>(())
274    })?;
275
276    Ok((built, handle))
277}
278
279#[derive(Error, Debug, Diagnostic)]
280pub enum Error {
281    #[error(transparent)]
282    #[diagnostic(transparent)]
283    LuaVersionUnset(#[from] crate::lua_version::LuaVersionUnset),
284    #[error(transparent)]
285    #[diagnostic(transparent)]
286    LuaInstallation(#[from] lua_installation::Error),
287    #[error(transparent)]
288    #[diagnostic(transparent)]
289    FlushLockfile(#[from] FlushLockfileError),
290    #[error(transparent)]
291    #[diagnostic(transparent)]
292    Tree(#[from] tree::Error),
293    #[error(transparent)]
294    #[diagnostic(transparent)]
295    WorkspaceTree(#[from] TreeError),
296    #[error("error instantiating LuaRocks compatibility layer")]
297    #[diagnostic(forward(0))]
298    LuaRocks(#[from] LuaRocksError),
299    #[error("error installing LuaRocks compatibility layer")]
300    #[diagnostic(forward(0))]
301    LuaRocksInstall(#[from] Box<LuaRocksInstallError>),
302    #[error("error initialising remote package DB")]
303    #[diagnostic(forward(0))]
304    PackageDB(#[from] package_db::Error),
305    #[error("cannot install duplicate entrypoints:\n{0}")]
306    DuplicateEntrypoints(package::List),
307    #[error("cannot install conflicting entrypoints:\n{0}")]
308    #[diagnostic(help(
309        "only a single entrypoint per package is allowed.\nif you would like to replace the existing entrypoints, retry with `--force` to overwrite the existing entrypoint"
310    ))]
311    ConflictingEntrypoints(String),
312    #[error(transparent)]
313    #[diagnostic(transparent)]
314    Resolve(#[from] resolve::Error),
315    #[error(transparent)]
316    #[diagnostic(transparent)]
317    Download(#[from] MaterialiseError),
318    #[error(transparent)]
319    #[diagnostic(transparent)]
320    Build(#[from] PipelineBuildError),
321}
322
323impl From<LuaRocksInstallError> for Error {
324    fn from(source: LuaRocksInstallError) -> Self {
325        Self::LuaRocksInstall(Box::new(source))
326    }
327}
328
329trait LockfileExt {
330    fn add_dependencies(
331        self,
332        pkg: &LockedPackage,
333        entry_type: tree::EntryType,
334        installed_packages: &HashMap<LockedPackageId, LockedPackage>,
335    ) -> io::Result<()>;
336
337    fn add_build_dependencies(
338        self,
339        pkg: &LockedPackage,
340        build_dependencies: &HashMap<LockedPackageId, LockedPackage>,
341    ) -> io::Result<()>;
342}
343
344impl LockfileExt for &mut Lockfile<ReadWrite> {
345    fn add_dependencies(
346        self,
347        pkg: &LockedPackage,
348        entry_type: tree::EntryType,
349        installed_packages: &HashMap<LockedPackageId, LockedPackage>,
350    ) -> io::Result<()> {
351        if entry_type == tree::EntryType::Entrypoint {
352            self.add_entrypoint(pkg);
353        }
354
355        for dependency_id in pkg.spec().dependencies() {
356            self.add_dependency(
357                pkg,
358                installed_packages
359                    .get(dependency_id)
360                    .ok_or(io::Error::other(
361                        r#"
362error writing dependencies to the lockfile.
363A required dependency was not installed correctly.
364This is likely a bug in Lux.
365"#,
366                    ))?,
367            );
368        }
369        Ok(())
370    }
371
372    fn add_build_dependencies(
373        self,
374        pkg: &LockedPackage,
375        build_dependencies: &HashMap<LockedPackageId, LockedPackage>,
376    ) -> io::Result<()> {
377        for dependency_id in pkg.spec().build_dependencies() {
378            self.add_build_dependency(
379                pkg,
380                build_dependencies
381                    .get(dependency_id)
382                    .ok_or(io::Error::other(
383                        r#"
384error writing build dependencies to the lockfile.
385A required build dependency was not installed correctly.
386This is likely a bug in Lux.
387"#,
388                    ))?,
389            );
390        }
391        Ok(())
392    }
393}