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 workspace lockfile failed")]
52 #[diagnostic(forward(0))]
53 SyncDependencies(#[source] SyncError),
54 #[error("error building the workspace")]
55 #[diagnostic(forward(0))]
56 Build(#[from] BuildError),
57}
58
59#[derive(Builder)]
60#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
61pub struct BuildWorkspace<'a> {
62 #[builder(start_fn)]
63 workspace: &'a Workspace,
64
65 #[builder(start_fn)]
66 config: &'a Config,
67
68 package: Option<PackageName>,
70
71 no_lock: bool,
73
74 only_deps: bool,
76}
77
78impl<State: build_workspace_builder::State + build_workspace_builder::IsComplete>
79 BuildWorkspaceBuilder<'_, State>
80{
81 pub async fn build(self) -> Result<Vec<LocalPackage>, BuildWorkspaceError> {
82 let build = self._build();
83 let span = match &build.package {
84 Some(package) => info_span!("Building workspace", package = package.to_string()),
85 None => info_span!("Building workspace"),
86 };
87 do_build(build).instrument(span).await
88 }
89}
90
91async fn do_build(args: BuildWorkspace<'_>) -> Result<Vec<LocalPackage>, BuildWorkspaceError> {
92 let config = args.config;
93 let workspace = args.workspace;
94 let workspace_tree = workspace.tree(config)?;
95 let build_tree = workspace.build_tree(config)?;
96 let lua = LuaInstallation::new_from_config(config).await?;
97 if !args.no_lock {
98 Sync::new(workspace, config)
99 .sync()
100 .await
101 .map_err(BuildWorkspaceError::SyncDependencies)?;
102 } else {
103 let luarocks = LuaRocksInstallation::new(config, build_tree.clone())?;
104 let mut dependencies_to_install = Vec::new();
105 let mut build_dependencies_to_install = Vec::new();
106 if let Some(package) = &args.package {
107 let project = workspace.select_member(package)?;
108 let project_toml = project.toml().into_local()?;
109 prepare_dependencies_for_build(
110 &project_toml,
111 &workspace_tree,
112 &mut dependencies_to_install,
113 &mut build_dependencies_to_install,
114 tree::EntryType::Entrypoint,
115 );
116 } else {
117 for project in workspace.members() {
118 let project_toml = project.toml().into_local()?;
119 prepare_dependencies_for_build(
120 &project_toml,
121 &workspace_tree,
122 &mut dependencies_to_install,
123 &mut build_dependencies_to_install,
124 tree::EntryType::Entrypoint,
125 );
126 }
127 }
128
129 let tree = workspace.tree(config)?;
130
131 InstallDependencies::new()
132 .dependencies(dependencies_to_install.into_iter().unique().collect_vec())
133 .build_dependencies(
134 build_dependencies_to_install
135 .into_iter()
136 .unique()
137 .collect_vec(),
138 )
139 .tree(&tree)
140 .lua(&lua)
141 .luarocks(&luarocks)
142 .config(config)
143 .build()
144 .await
145 .map_err(BuildWorkspaceError::InstallBuildDependencies)?;
146 }
147
148 let mut packages = Vec::new();
149 if !args.only_deps {
150 if let Some(package) = &args.package {
151 let project = workspace.select_member(package)?;
152 let pkg = build_project(project, workspace, &lua, config).await?;
153 packages.push(pkg);
154 } else {
155 for project in workspace.members() {
156 let pkg = build_project(project, workspace, &lua, config).await?;
157 packages.push(pkg);
158 }
159 }
160 }
161 Ok(packages)
162}
163
164async fn build_project(
165 project: &Project,
166 workspace: &Workspace,
167 lua: &LuaInstallation,
168 config: &Config,
169) -> Result<LocalPackage, BuildWorkspaceError> {
170 let workspace_tree = workspace.tree(config)?;
171 let project_toml = project.toml().into_local()?;
172
173 let package = Build::new()
174 .rockspec(&project_toml)
175 .lua(lua)
176 .tree(&workspace_tree)
177 .entry_type(tree::EntryType::Entrypoint)
178 .config(config)
179 .behaviour(BuildBehaviour::Force)
180 .build()
181 .await?;
182
183 let lockfile = workspace_tree.lockfile()?;
184 let dependencies = lockfile
185 .rocks()
186 .iter()
187 .filter_map(|(pkg_id, value)| {
188 if lockfile.is_entrypoint(pkg_id) {
189 Some(value)
190 } else {
191 None
192 }
193 })
194 .cloned()
195 .collect_vec();
196
197 let build_lockfile = workspace.build_tree(config)?.lockfile()?;
198
199 let mut lockfile = lockfile.write_guard();
200 lockfile.add_entrypoint(&package);
201 for dep in dependencies {
202 lockfile.add_dependency(&package, &dep);
203 }
204 for dep in build_lockfile.rocks().values() {
205 lockfile.add_build_dependency(&package, dep);
206 }
207 Ok(package)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 use crate::{
215 config::ConfigBuilder, fs, 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 fs::tokio::create_dir_all(&foo_bin_dir).await.unwrap();
232 let foo_bin_file = foo_bin_dir.join("foo");
233 fs::tokio::write(&foo_bin_file, "print('hello')")
234 .await
235 .unwrap();
236 let bar_bin_dir = project_root.join("bin");
237 fs::tokio::create_dir_all(&bar_bin_dir).await.unwrap();
238 let bar_bin_file = bar_bin_dir.join("bar");
239 fs::tokio::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 fs::tokio::create_dir_all(&src_dir).await.unwrap();
275 let init_lua_file = src_dir.join("init.lua");
276 fs::tokio::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}