Skip to main content

lux_lib/operations/
build_workspace.rs

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