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            );
123        } else {
124            for project in workspace.members() {
125                let project_toml = project.toml().into_local()?;
126                prepare_dependencies_for_build(
127                    &project_toml,
128                    &workspace_tree,
129                    &mut dependencies_to_install,
130                    &mut build_dependencies_to_install,
131                );
132            }
133        }
134
135        let tree = workspace.tree(config)?;
136
137        InstallDependencies::new()
138            .dependencies(dependencies_to_install.into_iter().unique().collect_vec())
139            .build_dependencies(
140                build_dependencies_to_install
141                    .into_iter()
142                    .unique()
143                    .collect_vec(),
144            )
145            .tree(&tree)
146            .lua(&lua)
147            .luarocks(&luarocks)
148            .config(config)
149            .build()
150            .await
151            .map_err(BuildWorkspaceError::InstallBuildDependencies)?;
152    }
153
154    let mut packages = Vec::new();
155    if !args.only_deps {
156        if let Some(package) = &args.package {
157            let project = workspace.select_member(package)?;
158            let pkg = build_project(project, workspace, &lua, config).await?;
159            packages.push(pkg);
160        } else {
161            for project in workspace.members() {
162                let pkg = build_project(project, workspace, &lua, config).await?;
163                packages.push(pkg);
164            }
165        }
166    }
167    Ok(packages)
168}
169
170async fn build_project(
171    project: &Project,
172    workspace: &Workspace,
173    lua: &LuaInstallation,
174    config: &Config,
175) -> Result<LocalPackage, BuildWorkspaceError> {
176    let workspace_tree = workspace.tree(config)?;
177    let project_toml = project.toml().into_local()?;
178
179    let package = Build::new()
180        .rockspec(&project_toml)
181        .lua(lua)
182        .tree(&workspace_tree)
183        .entry_type(tree::EntryType::Entrypoint)
184        .config(config)
185        .behaviour(BuildBehaviour::Force)
186        .build()
187        .await?;
188
189    let lockfile = workspace_tree.lockfile()?;
190    let dependencies = lockfile
191        .rocks()
192        .iter()
193        .filter_map(|(pkg_id, value)| {
194            if lockfile.is_entrypoint(pkg_id) {
195                Some(value)
196            } else {
197                None
198            }
199        })
200        .cloned()
201        .collect_vec();
202    let mut lockfile = lockfile.write_guard();
203    lockfile.add_entrypoint(&package);
204    for dep in dependencies {
205        lockfile.add_dependency(&package, &dep);
206        lockfile.remove_entrypoint(&dep);
207    }
208    Ok(package)
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    use crate::{
216        config::ConfigBuilder, fs, lua_installation::detect_installed_lua_version,
217        lua_version::LuaVersion,
218    };
219    use assert_fs::prelude::PathCopy;
220    use std::path::PathBuf;
221
222    #[tokio::test]
223    /// Non-regression for #980
224    async fn builtin_build_autodetect_bin_scripts() {
225        let project_root =
226            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
227        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
228        let temp_dir = assert_fs::TempDir::new().unwrap();
229        temp_dir.copy_from(&project_root, &["**"]).unwrap();
230        let project_root = temp_dir.path();
231        let foo_bin_dir = project_root.join("src").join("bin");
232        fs::tokio::create_dir_all(&foo_bin_dir).await.unwrap();
233        let foo_bin_file = foo_bin_dir.join("foo");
234        fs::tokio::write(&foo_bin_file, "print('hello')")
235            .await
236            .unwrap();
237        let bar_bin_dir = project_root.join("bin");
238        fs::tokio::create_dir_all(&bar_bin_dir).await.unwrap();
239        let bar_bin_file = bar_bin_dir.join("bar");
240        fs::tokio::write(&bar_bin_file, "print('hello')")
241            .await
242            .unwrap();
243        let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
244        let config = ConfigBuilder::new()
245            .unwrap()
246            .data_dir(Some(data_dir))
247            .lua_version(lua_version)
248            .build()
249            .unwrap();
250        let workspace = Workspace::from_exact(project_root).unwrap().unwrap();
251        let tree = workspace.tree(&config).unwrap();
252        let package = BuildWorkspace::new(&workspace, &config)
253            .no_lock(false)
254            .only_deps(false)
255            .build()
256            .await
257            .unwrap();
258        let package = package.first().unwrap();
259        let layout = tree.installed_rock_layout(package).unwrap();
260        let bin_dir = layout.bin;
261        assert!(bin_dir.join("foo").is_file());
262        assert!(bin_dir.join("bar").is_file());
263    }
264
265    #[tokio::test]
266    /// Non-regression for #1563
267    async fn builtin_build_support_src_init_lua() {
268        let data_dir: PathBuf = assert_fs::TempDir::new().unwrap().path().into();
269        let project_root =
270            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
271        let temp_dir = assert_fs::TempDir::new().unwrap();
272        temp_dir.copy_from(&project_root, &["**"]).unwrap();
273        let project_root = temp_dir.path();
274        let src_dir = project_root.join("src");
275        fs::tokio::create_dir_all(&src_dir).await.unwrap();
276        let init_lua_file = src_dir.join("init.lua");
277        fs::tokio::write(&init_lua_file, "print('hello')")
278            .await
279            .unwrap();
280        let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
281        let config = ConfigBuilder::new()
282            .unwrap()
283            .data_dir(Some(data_dir))
284            .lua_version(lua_version)
285            .build()
286            .unwrap();
287        let workspace = Workspace::from_exact(project_root).unwrap().unwrap();
288        let package = BuildWorkspace::new(&workspace, &config)
289            .no_lock(false)
290            .only_deps(false)
291            .build()
292            .await
293            .unwrap();
294        let package = package.first().unwrap();
295        let tree = workspace.tree(&config).unwrap();
296        let layout = tree.installed_rock_layout(package).unwrap();
297        let src_dir = layout.src;
298        assert!(src_dir.join("init.lua").is_file());
299    }
300}