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(
124                            format!("{}-{}", rockspec.package(), rockspec.version()),
125                            Box::new(err),
126                        )
127                    })?,
128                PackageSource::Archive(bytes) => BinaryRockInstall::new(
129                    &rockspec,
130                    package.source().clone(),
131                    bytes,
132                    entry_type,
133                    args.config,
134                    args.tree,
135                )
136                .pin(pin)
137                .opt(opt)
138                .constraint(constraint)
139                .behaviour(package_behaviour)
140                .install()
141                .await
142                .map_err(|err| {
143                    BuildError::InstallBinaryRock(rockspec.package().to_string(), Box::new(err))
144                })?,
145            };
146
147            // Record the installed package in the tree lockfile so that its paths are visible to
148            // the packages built after it (e.g. LuaRocks build backends), as well as future builds.
149            {
150                let mut lockfile = args.tree.lockfile()?.write_guard();
151                if is_entrypoint {
152                    lockfile.add_entrypoint(&pkg);
153                } else {
154                    lockfile.add(&pkg);
155                }
156            }
157
158            installed.push(pkg);
159        }
160
161        Ok(installed)
162    }
163}
164
165fn order_by_build_dependencies(packages: Vec<MaterialisedPackage>) -> Vec<MaterialisedPackage> {
166    fn visit(
167        id: LockedPackageId,
168        by_id: &mut HashMap<LockedPackageId, MaterialisedPackage>,
169        visited: &mut HashSet<LockedPackageId>,
170        ordered: &mut Vec<MaterialisedPackage>,
171    ) {
172        if !visited.insert(id.clone()) {
173            return;
174        }
175
176        let build_dependencies: Vec<LockedPackageId> = by_id
177            .get(&id)
178            .map(|package| {
179                package
180                    .package
181                    .spec
182                    .build_dependencies()
183                    .into_iter()
184                    .cloned()
185                    .collect()
186            })
187            .unwrap_or_default();
188
189        for dependency in build_dependencies {
190            visit(dependency, by_id, visited, ordered);
191        }
192
193        if let Some(package) = by_id.remove(&id) {
194            ordered.push(package);
195        }
196    }
197
198    let mut by_id: HashMap<LockedPackageId, MaterialisedPackage> = packages
199        .into_iter()
200        .map(|package| (package.package.id(), package))
201        .collect();
202
203    let mut ordered = Vec::new();
204    let mut visited = HashSet::new();
205
206    let ids: Vec<LockedPackageId> = by_id.keys().cloned().collect();
207    for id in ids {
208        visit(id, &mut by_id, &mut visited, &mut ordered);
209    }
210
211    ordered
212}