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