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