Skip to main content

lux_lib/operations/
build_workspace.rs

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    progress::{MultiProgress, Progress},
10    project::{project_toml::LocalProjectTomlValidationError, Project},
11    tree::{self, InstallTree, TreeError},
12    workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
13};
14use bon::Builder;
15use itertools::Itertools;
16use miette::Diagnostic;
17use std::sync::Arc;
18use thiserror::Error;
19
20use super::{InstallError, Sync, SyncError};
21
22#[derive(Debug, Error, Diagnostic)]
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:\n{0}")]
46    #[diagnostic(forward(0))]
47    InstallDependencies(InstallError),
48    #[error("error installind build dependencies:\n{0}")]
49    #[diagnostic(forward(0))]
50    InstallBuildDependencies(InstallError),
51    #[error("syncing dependencies with the project lockfile failed.\nUse --no-lock to force a new build.\n\n{0}")]
52    #[diagnostic(forward(0))]
53    SyncDependencies(SyncError),
54    #[error("syncing build dependencies with the project lockfile failed.\nUse --no-lock to force a new build.\n\n{0}")]
55    #[diagnostic(forward(0))]
56    SyncBuildDependencies(SyncError),
57    #[error("error building project:\n{0}")]
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 to build
72    package: Option<PackageName>,
73
74    /// Ignore the project's lockfile and don't create one
75    no_lock: bool,
76
77    /// Build only the dependencies
78    only_deps: bool,
79
80    progress: Option<Arc<Progress<MultiProgress>>>,
81}
82
83impl<State: build_workspace_builder::State + build_workspace_builder::IsComplete>
84    BuildWorkspaceBuilder<'_, State>
85{
86    /// Returns `Some` if the `only_deps` option is set to `false`.
87    pub async fn build(self) -> Result<Vec<LocalPackage>, BuildWorkspaceError> {
88        let args = self._build();
89        let config = args.config;
90        let workspace = args.workspace;
91        let workspace_tree = workspace.tree(config)?;
92        let build_tree = workspace.build_tree(config)?;
93        let progress_arc = args
94            .progress
95            .clone()
96            .unwrap_or_else(|| MultiProgress::new_arc(args.config));
97        let lua = LuaInstallation::new_from_config(
98            config,
99            &progress_arc.map(|progress| progress.new_bar()),
100        )
101        .await?;
102        if !args.no_lock {
103            Sync::new(workspace, config)
104                .progress(progress_arc.clone())
105                .sync_dependencies()
106                .await
107                .map_err(BuildWorkspaceError::SyncDependencies)?;
108
109            Sync::new(workspace, config)
110                .progress(progress_arc.clone())
111                .sync_build_dependencies()
112                .await
113                .map_err(BuildWorkspaceError::SyncBuildDependencies)?;
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                );
127            } else {
128                for project in workspace.members() {
129                    let project_toml = project.toml().into_local()?;
130                    prepare_dependencies_for_build(
131                        &project_toml,
132                        &workspace_tree,
133                        &mut dependencies_to_install,
134                        &mut build_dependencies_to_install,
135                    );
136                }
137            }
138
139            let tree = workspace.tree(config)?;
140
141            InstallDependencies::new()
142                .dependencies(dependencies_to_install.into_iter().unique().collect_vec())
143                .build_dependencies(
144                    build_dependencies_to_install
145                        .into_iter()
146                        .unique()
147                        .collect_vec(),
148                )
149                .tree(&tree)
150                .lua(&lua)
151                .luarocks(&luarocks)
152                .config(config)
153                .progress(progress_arc.clone())
154                .build()
155                .await
156                .map_err(BuildWorkspaceError::InstallBuildDependencies)?;
157        }
158
159        let mut packages = Vec::new();
160        if !args.only_deps {
161            if let Some(package) = &args.package {
162                let project = workspace.select_member(package)?;
163                let pkg =
164                    build_project(project, workspace, &lua, config, progress_arc.clone()).await?;
165                packages.push(pkg);
166            } else {
167                for project in workspace.members() {
168                    let pkg = build_project(project, workspace, &lua, config, progress_arc.clone())
169                        .await?;
170                    packages.push(pkg);
171                }
172            }
173        }
174        Ok(packages)
175    }
176}
177
178async fn build_project(
179    project: &Project,
180    workspace: &Workspace,
181    lua: &LuaInstallation,
182    config: &Config,
183    progress: Arc<Progress<MultiProgress>>,
184) -> Result<LocalPackage, BuildWorkspaceError> {
185    let workspace_tree = workspace.tree(config)?;
186    let project_toml = project.toml().into_local()?;
187
188    let package = Build::new()
189        .rockspec(&project_toml)
190        .lua(lua)
191        .tree(&workspace_tree)
192        .entry_type(tree::EntryType::Entrypoint)
193        .config(config)
194        .progress(&progress.map(|p| p.new_bar()))
195        .behaviour(BuildBehaviour::Force)
196        .build()
197        .await?;
198
199    let lockfile = workspace_tree.lockfile()?;
200    let dependencies = lockfile
201        .rocks()
202        .iter()
203        .filter_map(|(pkg_id, value)| {
204            if lockfile.is_entrypoint(pkg_id) {
205                Some(value)
206            } else {
207                None
208            }
209        })
210        .cloned()
211        .collect_vec();
212    let mut lockfile = lockfile.write_guard();
213    lockfile.add_entrypoint(&package);
214    for dep in dependencies {
215        lockfile.add_dependency(&package, &dep);
216        lockfile.remove_entrypoint(&dep);
217    }
218    Ok(package)
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    use crate::{
226        config::ConfigBuilder, lua_installation::detect_installed_lua_version,
227        lua_version::LuaVersion,
228    };
229    use assert_fs::prelude::PathCopy;
230    use std::path::PathBuf;
231
232    #[tokio::test]
233    /// Non-regression for #980
234    async fn builtin_build_autodetect_bin_scripts() {
235        let project_root =
236            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
237        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
238        let temp_dir = assert_fs::TempDir::new().unwrap();
239        temp_dir.copy_from(&project_root, &["**"]).unwrap();
240        let project_root = temp_dir.path();
241        let foo_bin_dir = project_root.join("src").join("bin");
242        tokio::fs::create_dir_all(&foo_bin_dir).await.unwrap();
243        let foo_bin_file = foo_bin_dir.join("foo");
244        tokio::fs::write(&foo_bin_file, "print('hello')")
245            .await
246            .unwrap();
247        let bar_bin_dir = project_root.join("bin");
248        tokio::fs::create_dir_all(&bar_bin_dir).await.unwrap();
249        let bar_bin_file = bar_bin_dir.join("bar");
250        tokio::fs::write(&bar_bin_file, "print('hello')")
251            .await
252            .unwrap();
253        let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
254        let config = ConfigBuilder::new()
255            .unwrap()
256            .data_dir(Some(data_dir))
257            .lua_version(lua_version)
258            .build()
259            .unwrap();
260        let workspace = Workspace::from_exact(project_root).unwrap().unwrap();
261        let tree = workspace.tree(&config).unwrap();
262        let package = BuildWorkspace::new(&workspace, &config)
263            .no_lock(false)
264            .only_deps(false)
265            .build()
266            .await
267            .unwrap();
268        let package = package.first().unwrap();
269        let layout = tree.installed_rock_layout(package).unwrap();
270        let bin_dir = layout.bin;
271        assert!(bin_dir.join("foo").is_file());
272        assert!(bin_dir.join("bar").is_file());
273    }
274
275    #[tokio::test]
276    /// Non-regression for #1563
277    async fn builtin_build_support_src_init_lua() {
278        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
279        let project_root =
280            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
281        let temp_dir = assert_fs::TempDir::new().unwrap();
282        temp_dir.copy_from(&project_root, &["**"]).unwrap();
283        let project_root = temp_dir.path();
284        let src_dir = project_root.join("src");
285        tokio::fs::create_dir_all(&src_dir).await.unwrap();
286        let init_lua_file = src_dir.join("init.lua");
287        tokio::fs::write(&init_lua_file, "print('hello')")
288            .await
289            .unwrap();
290        let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
291        let config = ConfigBuilder::new()
292            .unwrap()
293            .data_dir(Some(data_dir))
294            .lua_version(lua_version)
295            .build()
296            .unwrap();
297        let workspace = Workspace::from_exact(project_root).unwrap().unwrap();
298        let package = BuildWorkspace::new(&workspace, &config)
299            .no_lock(false)
300            .only_deps(false)
301            .build()
302            .await
303            .unwrap();
304        let package = package.first().unwrap();
305        let tree = workspace.tree(&config).unwrap();
306        let layout = tree.installed_rock_layout(package).unwrap();
307        let src_dir = layout.src;
308        assert!(src_dir.join("init.lua").is_file());
309    }
310}