Skip to main content

lux_lib/pipeline/
build.rs

1use std::collections::{HashMap, HashSet};
2
3use bon::Builder;
4use miette::Diagnostic;
5use thiserror::Error;
6
7use crate::{
8    build::Deploy,
9    config::Config,
10    lockfile::{LockedPackage, LockedPackageId},
11    lua_installation::{LuaInstallation, LuaInstallationError},
12    lua_rockspec::BuildBackendSpec,
13    luarocks::{
14        install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
15        luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
16    },
17    rockspec::Rockspec,
18    tree::{InstallTree, TreeError},
19};
20
21use super::materialise::{MaterialisedPackage, PackageSource};
22
23#[derive(Error, Debug, Diagnostic)]
24#[non_exhaustive]
25pub enum BuildError {
26    #[error("failed to build '{0}'")]
27    Build(String, #[source] Box<crate::build::BuildError>),
28    #[error(transparent)]
29    #[diagnostic(transparent)]
30    Tree(#[from] TreeError),
31    #[error("failed to install pre-built rock '{0}'")]
32    InstallBinaryRock(String, #[source] Box<InstallBinaryRockError>),
33    #[error(transparent)]
34    #[diagnostic(transparent)]
35    LuaInstallation(#[from] LuaInstallationError),
36    #[error("error instantiating LuaRocks compatibility layer")]
37    #[diagnostic(forward(0))]
38    LuaRocks(#[from] LuaRocksError),
39    #[error("error installing LuaRocks compatibility layer")]
40    #[diagnostic(forward(0))]
41    LuaRocksInstall(#[from] Box<LuaRocksInstallError>),
42}
43
44impl From<LuaRocksInstallError> for BuildError {
45    fn from(source: LuaRocksInstallError) -> Self {
46        Self::LuaRocksInstall(Box::new(source))
47    }
48}
49
50/// A rocks package builder, providing fine-grained control
51/// over how a package should be built.
52#[derive(Builder)]
53#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
54pub(crate) struct Build<'a, T: InstallTree + Send + Sync> {
55    #[builder(start_fn)]
56    pub(crate) config: &'a Config,
57    #[builder(start_fn)]
58    pub(crate) tree: &'a T,
59    #[builder(field)]
60    pub(crate) packages: Vec<MaterialisedPackage>,
61}
62
63impl<T, State> BuildBuilder<'_, T, State>
64where
65    T: InstallTree + Send + Sync,
66    State: build_builder::State,
67{
68    pub(crate) fn packages(mut self, packages: Vec<MaterialisedPackage>) -> Self {
69        self.packages = packages;
70        self
71    }
72}
73
74impl<T, State> BuildBuilder<'_, T, State>
75where
76    T: InstallTree + Send + Sync,
77    State: build_builder::State + build_builder::IsComplete,
78{
79    pub(crate) async fn build(self) -> Result<Vec<LockedPackage>, BuildError> {
80        let args = self._build();
81        let lua = LuaInstallation::new_from_config(args.config).await?;
82
83        let needs_luarocks = args.packages.iter().any(|package| {
84            matches!(
85                package.rockspec.build().current_platform().build_backend,
86                Some(BuildBackendSpec::LuaRock(_))
87            )
88        });
89        if needs_luarocks {
90            LuaRocksInstallation::new(args.config, args.tree.build_tree(args.config)?)?
91                .ensure_installed(&lua)
92                .await?;
93        }
94
95        let mut installed = Vec::new();
96
97        for downloaded in order_by_build_dependencies(args.packages) {
98            let MaterialisedPackage {
99                package,
100                rockspec,
101                entry_type,
102                build_behaviour: package_behaviour,
103                artifact,
104                temp_dir: _temp_dir,
105            } = downloaded;
106
107            let is_entrypoint = entry_type.is_entrypoint();
108            let constraint = package.spec.constraint();
109            let pin = package.spec.pinned();
110            let opt = package.spec.opt();
111
112            let pkg = match artifact {
113                PackageSource::SourceDir(path) => Deploy::new(&rockspec, args.tree)
114                    .package(package)
115                    .lua(&lua)
116                    .source_root(&path)
117                    .entry_type(entry_type)
118                    .config(args.config)
119                    .behaviour(package_behaviour)
120                    .deploy()
121                    .await
122                    .map_err(|err| {
123                        BuildError::Build(rockspec.package().to_string(), Box::new(err))
124                    })?,
125                PackageSource::Archive(bytes) => BinaryRockInstall::new(
126                    &rockspec,
127                    package.source().clone(),
128                    bytes,
129                    entry_type,
130                    args.config,
131                    args.tree,
132                )
133                .pin(pin)
134                .opt(opt)
135                .constraint(constraint)
136                .behaviour(package_behaviour)
137                .install()
138                .await
139                .map_err(|err| {
140                    BuildError::InstallBinaryRock(rockspec.package().to_string(), Box::new(err))
141                })?,
142            };
143
144            // Record the installed package in the tree lockfile so that its paths are visible to
145            // the packages built after it (e.g. LuaRocks build backends), as well as future builds.
146            {
147                let mut lockfile = args.tree.lockfile()?.write_guard();
148                if is_entrypoint {
149                    lockfile.add_entrypoint(&pkg);
150                } else {
151                    lockfile.add(&pkg);
152                }
153            }
154
155            installed.push(pkg);
156        }
157
158        Ok(installed)
159    }
160}
161
162fn order_by_build_dependencies(packages: Vec<MaterialisedPackage>) -> Vec<MaterialisedPackage> {
163    fn visit(
164        id: LockedPackageId,
165        by_id: &mut HashMap<LockedPackageId, MaterialisedPackage>,
166        visited: &mut HashSet<LockedPackageId>,
167        ordered: &mut Vec<MaterialisedPackage>,
168    ) {
169        if !visited.insert(id.clone()) {
170            return;
171        }
172
173        let build_dependencies: Vec<LockedPackageId> = by_id
174            .get(&id)
175            .map(|package| {
176                package
177                    .package
178                    .spec
179                    .build_dependencies()
180                    .into_iter()
181                    .cloned()
182                    .collect()
183            })
184            .unwrap_or_default();
185
186        for dependency in build_dependencies {
187            visit(dependency, by_id, visited, ordered);
188        }
189
190        if let Some(package) = by_id.remove(&id) {
191            ordered.push(package);
192        }
193    }
194
195    let mut by_id: HashMap<LockedPackageId, MaterialisedPackage> = packages
196        .into_iter()
197        .map(|package| (package.package.id(), package))
198        .collect();
199
200    let mut ordered = Vec::new();
201    let mut visited = HashSet::new();
202
203    let ids: Vec<LockedPackageId> = by_id.keys().cloned().collect();
204    for id in ids {
205        visit(id, &mut by_id, &mut visited, &mut ordered);
206    }
207
208    ordered
209}