lux_lib/operations/
build_project.rs1use std::sync::Arc;
2
3use bon::Builder;
4use itertools::Itertools;
5use thiserror::Error;
6
7use crate::{
8 build::{Build, BuildBehaviour, BuildError},
9 config::Config,
10 lockfile::LocalPackage,
11 lua_installation::{LuaInstallation, LuaInstallationError},
12 luarocks::luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
13 progress::{MultiProgress, Progress},
14 project::{project_toml::LocalProjectTomlValidationError, Project, ProjectTreeError},
15 rockspec::Rockspec,
16 tree::{self, TreeError},
17};
18
19use super::{Install, InstallError, PackageInstallSpec, Sync, SyncError};
20
21#[derive(Debug, Error)]
22pub enum BuildProjectError {
23 #[error(transparent)]
24 LocalProjectTomlValidation(#[from] LocalProjectTomlValidationError),
25 #[error(transparent)]
26 ProjectTree(#[from] ProjectTreeError),
27 #[error(transparent)]
28 LuaInstallation(#[from] LuaInstallationError),
29 #[error(transparent)]
30 Tree(#[from] TreeError),
31 #[error(transparent)]
32 LuaRocks(#[from] LuaRocksError),
33 #[error(transparent)]
34 LuaRocksInstall(#[from] LuaRocksInstallError),
35 #[error("error installind dependencies:\n{0}")]
36 InstallDependencies(InstallError),
37 #[error("error installind build dependencies:\n{0}")]
38 InstallBuildDependencies(InstallError),
39 #[error("syncing dependencies with the project lockfile failed.\nUse --no-lock to force a new build.\n\n{0}")]
40 SyncDependencies(SyncError),
41 #[error("syncing build dependencies with the project lockfile failed.\nUse --no-lock to force a new build.\n\n{0}")]
42 SyncBuildDependencies(SyncError),
43 #[error("error building project:\n{0}")]
44 Build(#[from] BuildError),
45}
46
47#[derive(Builder)]
48#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
49pub struct BuildProject<'a> {
50 #[builder(start_fn)]
51 project: &'a Project,
52
53 #[builder(start_fn)]
54 config: &'a Config,
55
56 no_lock: bool,
58
59 only_deps: bool,
61
62 progress: Option<Arc<Progress<MultiProgress>>>,
63}
64
65impl<State: build_project_builder::State + build_project_builder::IsComplete>
66 BuildProjectBuilder<'_, State>
67{
68 pub async fn build(self) -> Result<Option<LocalPackage>, BuildProjectError> {
70 let args = self._build();
71 let project = args.project;
72 let config = args.config;
73 let progress_arc = args
74 .progress
75 .unwrap_or_else(|| MultiProgress::new_arc(config));
76 let progress = Arc::clone(&progress_arc);
77
78 let project_toml = project.toml().into_local()?;
79 let project_tree = project.tree(config)?;
80
81 let dependencies = project_toml
82 .dependencies()
83 .current_platform()
84 .iter()
85 .cloned()
86 .collect_vec();
87
88 let build_dependencies = project_toml
89 .build_dependencies()
90 .current_platform()
91 .iter()
92 .cloned()
93 .collect_vec();
94
95 let build_tree = project.build_tree(config)?;
96 let lua =
97 LuaInstallation::new_from_config(config, &progress.map(|progress| progress.new_bar()))
98 .await?;
99 let luarocks = LuaRocksInstallation::new(config, build_tree.clone())?;
100
101 if args.no_lock {
102 let dependencies_to_install = dependencies
103 .into_iter()
104 .filter(|dep| {
105 project_tree
106 .match_rocks(dep.package_req())
107 .is_ok_and(|rock_match| !rock_match.is_found())
108 })
109 .map(|dep| {
110 PackageInstallSpec::new(
111 dep.clone().into_package_req(),
112 tree::EntryType::Entrypoint,
113 )
114 .pin(*dep.pin())
115 .opt(*dep.opt())
116 .maybe_source(dep.source().clone())
117 .build()
118 })
119 .collect();
120
121 Install::new(config)
122 .packages(dependencies_to_install)
123 .project(project)?
124 .progress(progress.clone())
125 .install()
126 .await
127 .map_err(BuildProjectError::InstallDependencies)?;
128
129 let build_dependencies_to_install = build_dependencies
130 .into_iter()
131 .filter(|dep| {
132 project_tree
133 .match_rocks(dep.package_req())
134 .is_ok_and(|rock_match| !rock_match.is_found())
135 })
136 .map(|dep| {
137 PackageInstallSpec::new(
138 dep.clone().into_package_req(),
139 tree::EntryType::Entrypoint,
140 )
141 .pin(*dep.pin())
142 .opt(*dep.opt())
143 .maybe_source(dep.source().clone())
144 .build()
145 })
146 .collect_vec();
147
148 if !build_dependencies_to_install.is_empty() {
149 let bar = progress.map(|p| p.new_bar());
150 luarocks.ensure_installed(&lua, &bar).await?;
151 Install::new(config)
152 .packages(build_dependencies_to_install)
153 .tree(build_tree)
154 .progress(progress.clone())
155 .install()
156 .await
157 .map_err(BuildProjectError::InstallBuildDependencies)?;
158 }
159 } else {
160 Sync::new(project, config)
161 .progress(progress.clone())
162 .sync_dependencies()
163 .await
164 .map_err(BuildProjectError::SyncDependencies)?;
165
166 Sync::new(project, config)
167 .progress(progress.clone())
168 .sync_build_dependencies()
169 .await
170 .map_err(BuildProjectError::SyncBuildDependencies)?;
171 }
172
173 if !args.only_deps {
174 let package = Build::new()
175 .rockspec(&project_toml)
176 .lua(&lua)
177 .tree(&project_tree)
178 .entry_type(tree::EntryType::Entrypoint)
179 .config(config)
180 .progress(&progress.map(|p| p.new_bar()))
181 .behaviour(BuildBehaviour::Force)
182 .build()
183 .await?;
184
185 let lockfile = project_tree.lockfile()?;
186 let dependencies = lockfile
187 .rocks()
188 .iter()
189 .filter_map(|(pkg_id, value)| {
190 if lockfile.is_entrypoint(pkg_id) {
191 Some(value)
192 } else {
193 None
194 }
195 })
196 .cloned()
197 .collect_vec();
198 let mut lockfile = lockfile.write_guard();
199 lockfile.add_entrypoint(&package);
200 for dep in dependencies {
201 lockfile.add_dependency(&package, &dep);
202 lockfile.remove_entrypoint(&dep);
203 }
204 Ok(Some(package))
205 } else {
206 Ok(None)
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 use crate::{
216 config::{ConfigBuilder, LuaVersion},
217 lua_installation::detect_installed_lua_version,
218 project::Project,
219 };
220 use assert_fs::prelude::PathCopy;
221 use std::path::PathBuf;
222
223 #[tokio::test]
224 async fn builtin_build_autodetect_bin_scripts() {
226 let project_root =
227 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
228 let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
229 let temp_dir = assert_fs::TempDir::new().unwrap();
230 temp_dir.copy_from(&project_root, &["**"]).unwrap();
231 let project_root = temp_dir.path();
232 let foo_bin_dir = project_root.join("src").join("bin");
233 tokio::fs::create_dir_all(&foo_bin_dir).await.unwrap();
234 let foo_bin_file = foo_bin_dir.join("foo");
235 tokio::fs::write(&foo_bin_file, "print('hello')")
236 .await
237 .unwrap();
238 let bar_bin_dir = project_root.join("bin");
239 tokio::fs::create_dir_all(&bar_bin_dir).await.unwrap();
240 let bar_bin_file = bar_bin_dir.join("bar");
241 tokio::fs::write(&bar_bin_file, "print('hello')")
242 .await
243 .unwrap();
244 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
245 let config = ConfigBuilder::new()
246 .unwrap()
247 .data_dir(Some(data_dir))
248 .lua_version(lua_version)
249 .build()
250 .unwrap();
251 let project = Project::from_exact(project_root).unwrap().unwrap();
252 let tree = project.tree(&config).unwrap();
253 let package = BuildProject::new(&project, &config)
254 .no_lock(false)
255 .only_deps(false)
256 .build()
257 .await
258 .unwrap()
259 .unwrap();
260 let layout = tree.installed_rock_layout(&package).unwrap();
261 let bin_dir = layout.bin;
262 assert!(bin_dir.join("foo").is_file());
263 assert!(bin_dir.join("bar").is_file());
264 }
265}