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