lux_lib/pipeline/
build.rs1use 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#[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 {
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}