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 tree::EntryType::Entrypoint,
123 );
124 } else {
125 for project in workspace.members() {
126 let project_toml = project.toml().into_local()?;
127 prepare_dependencies_for_build(
128 &project_toml,
129 &workspace_tree,
130 &mut dependencies_to_install,
131 &mut build_dependencies_to_install,
132 tree::EntryType::Entrypoint,
133 );
134 }
135 }
136
137 let tree = workspace.tree(config)?;
138
139 InstallDependencies::new()
140 .dependencies(dependencies_to_install.into_iter().unique().collect_vec())
141 .build_dependencies(
142 build_dependencies_to_install
143 .into_iter()
144 .unique()
145 .collect_vec(),
146 )
147 .tree(&tree)
148 .lua(&lua)
149 .luarocks(&luarocks)
150 .config(config)
151 .build()
152 .await
153 .map_err(BuildWorkspaceError::InstallBuildDependencies)?;
154 }
155
156 let mut packages = Vec::new();
157 if !args.only_deps {
158 if let Some(package) = &args.package {
159 let project = workspace.select_member(package)?;
160 let pkg = build_project(project, workspace, &lua, config).await?;
161 packages.push(pkg);
162 } else {
163 for project in workspace.members() {
164 let pkg = build_project(project, workspace, &lua, config).await?;
165 packages.push(pkg);
166 }
167 }
168 }
169 Ok(packages)
170}
171
172async fn build_project(
173 project: &Project,
174 workspace: &Workspace,
175 lua: &LuaInstallation,
176 config: &Config,
177) -> Result<LocalPackage, BuildWorkspaceError> {
178 let workspace_tree = workspace.tree(config)?;
179 let project_toml = project.toml().into_local()?;
180
181 let package = Build::new()
182 .rockspec(&project_toml)
183 .lua(lua)
184 .tree(&workspace_tree)
185 .entry_type(tree::EntryType::Entrypoint)
186 .config(config)
187 .behaviour(BuildBehaviour::Force)
188 .build()
189 .await?;
190
191 let lockfile = workspace_tree.lockfile()?;
192 let dependencies = lockfile
193 .rocks()
194 .iter()
195 .filter_map(|(pkg_id, value)| {
196 if lockfile.is_entrypoint(pkg_id) {
197 Some(value)
198 } else {
199 None
200 }
201 })
202 .cloned()
203 .collect_vec();
204 let mut lockfile = lockfile.write_guard();
205 lockfile.add_entrypoint(&package);
206 for dep in dependencies {
207 lockfile.add_dependency(&package, &dep);
208 lockfile.remove_entrypoint(&dep);
209 }
210 Ok(package)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 use crate::{
218 config::ConfigBuilder, fs, lua_installation::detect_installed_lua_version,
219 lua_version::LuaVersion,
220 };
221 use assert_fs::prelude::PathCopy;
222 use std::path::PathBuf;
223
224 #[tokio::test]
225 async fn builtin_build_autodetect_bin_scripts() {
227 let project_root =
228 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
229 let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
230 let temp_dir = assert_fs::TempDir::new().unwrap();
231 temp_dir.copy_from(&project_root, &["**"]).unwrap();
232 let project_root = temp_dir.path();
233 let foo_bin_dir = project_root.join("src").join("bin");
234 fs::tokio::create_dir_all(&foo_bin_dir).await.unwrap();
235 let foo_bin_file = foo_bin_dir.join("foo");
236 fs::tokio::write(&foo_bin_file, "print('hello')")
237 .await
238 .unwrap();
239 let bar_bin_dir = project_root.join("bin");
240 fs::tokio::create_dir_all(&bar_bin_dir).await.unwrap();
241 let bar_bin_file = bar_bin_dir.join("bar");
242 fs::tokio::write(&bar_bin_file, "print('hello')")
243 .await
244 .unwrap();
245 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
246 let config = ConfigBuilder::new()
247 .unwrap()
248 .data_dir(Some(data_dir))
249 .lua_version(lua_version)
250 .build()
251 .unwrap();
252 let workspace = Workspace::from_exact(project_root).unwrap().unwrap();
253 let tree = workspace.tree(&config).unwrap();
254 let package = BuildWorkspace::new(&workspace, &config)
255 .no_lock(false)
256 .only_deps(false)
257 .build()
258 .await
259 .unwrap();
260 let package = package.first().unwrap();
261 let layout = tree.installed_rock_layout(package).unwrap();
262 let bin_dir = layout.bin;
263 assert!(bin_dir.join("foo").is_file());
264 assert!(bin_dir.join("bar").is_file());
265 }
266
267 #[tokio::test]
268 async fn builtin_build_support_src_init_lua() {
270 let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
271 let project_root =
272 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
273 let temp_dir = assert_fs::TempDir::new().unwrap();
274 temp_dir.copy_from(&project_root, &["**"]).unwrap();
275 let project_root = temp_dir.path();
276 let src_dir = project_root.join("src");
277 fs::tokio::create_dir_all(&src_dir).await.unwrap();
278 let init_lua_file = src_dir.join("init.lua");
279 fs::tokio::write(&init_lua_file, "print('hello')")
280 .await
281 .unwrap();
282 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
283 let config = ConfigBuilder::new()
284 .unwrap()
285 .data_dir(Some(data_dir))
286 .lua_version(lua_version)
287 .build()
288 .unwrap();
289 let workspace = Workspace::from_exact(project_root).unwrap().unwrap();
290 let package = BuildWorkspace::new(&workspace, &config)
291 .no_lock(false)
292 .only_deps(false)
293 .build()
294 .await
295 .unwrap();
296 let package = package.first().unwrap();
297 let tree = workspace.tree(&config).unwrap();
298 let layout = tree.installed_rock_layout(package).unwrap();
299 let src_dir = layout.src;
300 assert!(src_dir.join("init.lua").is_file());
301 }
302}