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(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 {
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}