Skip to main content

lux_lib/drivers/
install_packages.rs

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