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