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