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