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::{self, LuaInstallation},
12    lua_rockspec::BuildBackendSpec,
13    luarocks::{
14        install_binary_rock::{self, BinaryRockInstall},
15        luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
16    },
17    rockspec::Rockspec,
18    tree::{self, InstallTree},
19};
20
21use super::materialise::{MaterialisedPackage, PackageSource};
22
23#[derive(Error, Debug, Diagnostic)]
24#[non_exhaustive]
25pub enum Error {
26    #[error("failed to build '{0}'")]
27    Build(String, #[source] Box<crate::build::Error>),
28    #[error(transparent)]
29    #[diagnostic(transparent)]
30    Tree(#[from] tree::Error),
31    #[error("failed to install pre-built rock '{0}'")]
32    InstallBinaryRock(String, #[source] Box<install_binary_rock::Error>),
33    #[error(transparent)]
34    #[diagnostic(transparent)]
35    LuaInstallation(#[from] lua_installation::Error),
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 Error {
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>, Error> {
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
109            let pkg = match artifact {
110                PackageSource::SourceDir(path) => Deploy::new(&rockspec, args.tree)
111                    .package(package)
112                    .lua(&lua)
113                    .source_root(&path)
114                    .entry_type(entry_type)
115                    .config(args.config)
116                    .behaviour(package_behaviour)
117                    .deploy()
118                    .await
119                    .map_err(|err| {
120                        Error::Build(
121                            format!("{}-{}", rockspec.package(), rockspec.version()),
122                            Box::new(err),
123                        )
124                    })?,
125                PackageSource::Archive(bytes) => BinaryRockInstall::new(
126                    &rockspec,
127                    package.spec().clone(),
128                    bytes,
129                    package.source().clone(),
130                    entry_type,
131                    args.config,
132                    args.tree,
133                )
134                .behaviour(package_behaviour)
135                .install()
136                .await
137                .map_err(|err| {
138                    Error::InstallBinaryRock(rockspec.package().to_string(), Box::new(err))
139                })?,
140            };
141
142            // Record the installed package in the tree lockfile so that its paths are visible to
143            // the packages built after it (e.g. LuaRocks build backends), as well as future builds.
144            {
145                let mut lockfile = args.tree.lockfile()?.write_guard();
146                if is_entrypoint {
147                    lockfile.add_entrypoint(&pkg);
148                } else {
149                    lockfile.add(&pkg);
150                }
151            }
152
153            installed.push(pkg);
154        }
155
156        Ok(installed)
157    }
158}
159
160fn order_by_build_dependencies(packages: Vec<MaterialisedPackage>) -> Vec<MaterialisedPackage> {
161    fn visit(
162        id: LockedPackageId,
163        by_id: &mut HashMap<LockedPackageId, MaterialisedPackage>,
164        visited: &mut HashSet<LockedPackageId>,
165        ordered: &mut Vec<MaterialisedPackage>,
166    ) {
167        if !visited.insert(id.clone()) {
168            return;
169        }
170
171        let build_dependencies: Vec<LockedPackageId> = by_id
172            .get(&id)
173            .map(|package| {
174                package
175                    .package
176                    .spec
177                    .build_dependencies()
178                    .into_iter()
179                    .cloned()
180                    .collect()
181            })
182            .unwrap_or_default();
183
184        for dependency in build_dependencies {
185            visit(dependency, by_id, visited, ordered);
186        }
187
188        if let Some(package) = by_id.remove(&id) {
189            ordered.push(package);
190        }
191    }
192
193    let mut by_id: HashMap<LockedPackageId, MaterialisedPackage> = packages
194        .into_iter()
195        .map(|package| (package.package.id(), package))
196        .collect();
197
198    let mut ordered = Vec::new();
199    let mut visited = HashSet::new();
200
201    let ids: Vec<LockedPackageId> = by_id.keys().cloned().collect();
202    for id in ids {
203        visit(id, &mut by_id, &mut visited, &mut ordered);
204    }
205
206    ordered
207}