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