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