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