Skip to main content

lux_lib/operations/
install_project.rs

1use bon::Builder;
2use miette::Diagnostic;
3use thiserror::Error;
4
5use crate::{
6    build::{BuildBehaviour, BuildError},
7    config::Config,
8    drivers::install_packages::{InstallPackages, InstallPackagesError},
9    hash::HasIntegrity,
10    lockfile::LockedPackage,
11    operations::PackageInstallSpec,
12    package::{PackageName, PackageReq},
13    package_db::{PackageDB, PackageDBError},
14    pipeline::discover::FoundPackage,
15    project::{IntoLocalRockspecError, Project, ProjectError},
16    rockspec::Rockspec,
17    tree::{self, InstallTree, TreeError},
18    workspace::{Workspace, WorkspaceError},
19};
20
21#[derive(Debug, Error, Diagnostic)]
22pub enum InstallProjectError {
23    #[error(transparent)]
24    #[diagnostic(transparent)]
25    Project(#[from] ProjectError),
26    #[error(transparent)]
27    #[diagnostic(transparent)]
28    LocalRockspec(#[from] IntoLocalRockspecError),
29    #[error(transparent)]
30    #[diagnostic(transparent)]
31    PackageDB(#[from] PackageDBError),
32    #[error(transparent)]
33    #[diagnostic(transparent)]
34    Tree(#[from] TreeError),
35    #[error(transparent)]
36    #[diagnostic(transparent)]
37    Install(#[from] Box<InstallPackagesError>),
38    #[error(transparent)]
39    #[diagnostic(transparent)]
40    Build(#[from] Box<BuildError>),
41    #[error("package '{0}' was not installed")]
42    PackageNotInstalled(PackageName),
43    #[error("failed to hash the project's sources")]
44    Hash(#[source] std::io::Error),
45    #[error(transparent)]
46    #[diagnostic(transparent)]
47    Workspace(#[from] WorkspaceError),
48}
49
50impl From<InstallPackagesError> for InstallProjectError {
51    fn from(source: InstallPackagesError) -> Self {
52        Self::Install(Box::new(source))
53    }
54}
55
56impl From<BuildError> for InstallProjectError {
57    fn from(source: BuildError) -> Self {
58        Self::Build(Box::new(source))
59    }
60}
61
62/// Installs a project into a [`Tree`].
63/// Typically, you will want to use [`crate::operations::BuildWorkspace`].
64/// Useful for installing a project and its dependencies outside of a workspace tree.
65#[derive(Builder)]
66#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
67pub struct InstallProject<'a, T>
68where
69    T: InstallTree,
70{
71    project: &'a Project,
72
73    config: &'a Config,
74
75    tree: &'a T,
76
77    #[builder(default = BuildBehaviour::Force)]
78    behaviour: BuildBehaviour,
79
80    /// Workspace whose lockfile is preferred during dependency resolution.
81    workspace: Option<&'a Workspace>,
82}
83
84impl<
85        T: InstallTree + Sync + Send,
86        State: install_project_builder::State + install_project_builder::IsComplete,
87    > InstallProjectBuilder<'_, T, State>
88{
89    /// Builds the project's root package, installing its dependencies through
90    /// the pipeline. Returns the installed root package.
91    pub async fn build(self) -> Result<LockedPackage, InstallProjectError> {
92        let args = self._build();
93        let config = args.config;
94        let project = args.project;
95        let tree = args.tree;
96
97        let rockspec = project.local_remote_rockspec()?;
98        let name = rockspec.package().clone();
99
100        let behaviour = if matches!(args.behaviour, BuildBehaviour::Ignore) {
101            match tree.lockfile()?.entrypoint(&name) {
102                Some(existing) => {
103                    let source_hash = project
104                        .root()
105                        .hash()
106                        .await
107                        .map_err(InstallProjectError::Hash)?;
108                    if existing.version() == rockspec.version()
109                        && existing.hashes().source == source_hash
110                    {
111                        return Ok(existing.clone());
112                    }
113                    BuildBehaviour::Force
114                }
115                None => args.behaviour,
116            }
117        } else {
118            args.behaviour
119        };
120
121        let root = FoundPackage::from_project_root(rockspec, project.root().to_path_buf());
122        let package_db = match args.workspace {
123            Some(workspace) => PackageDB::open(config, workspace).await?,
124            None => PackageDB::from_config(config).await?,
125        }
126        .with_local(vec![root]);
127
128        let install_spec =
129            PackageInstallSpec::new(PackageReq::from(name.clone()), tree::EntryType::Entrypoint)
130                .build_behaviour(behaviour)
131                .build();
132
133        InstallPackages::new(config, tree)
134            .package_db(package_db)
135            .package(install_spec)
136            .install()
137            .await?
138            .0
139            .into_iter()
140            .find(|package| package.name() == &name)
141            .ok_or(InstallProjectError::PackageNotInstalled(name))
142    }
143}