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