Skip to main content

lux_lib/operations/
build_project.rs

1use std::sync::Arc;
2
3use bon::Builder;
4use itertools::Itertools;
5use thiserror::Error;
6
7use crate::{
8    build::{Build, BuildBehaviour, BuildError},
9    config::Config,
10    lockfile::LocalPackage,
11    lua_installation::{LuaInstallation, LuaInstallationError},
12    luarocks::luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
13    progress::{MultiProgress, Progress},
14    project::{project_toml::LocalProjectTomlValidationError, Project, ProjectTreeError},
15    rockspec::Rockspec,
16    tree::{self, TreeError},
17};
18
19use super::{Install, InstallError, PackageInstallSpec, Sync, SyncError};
20
21#[derive(Debug, Error)]
22pub enum BuildProjectError {
23    #[error(transparent)]
24    LocalProjectTomlValidation(#[from] LocalProjectTomlValidationError),
25    #[error(transparent)]
26    ProjectTree(#[from] ProjectTreeError),
27    #[error(transparent)]
28    LuaInstallation(#[from] LuaInstallationError),
29    #[error(transparent)]
30    Tree(#[from] TreeError),
31    #[error(transparent)]
32    LuaRocks(#[from] LuaRocksError),
33    #[error(transparent)]
34    LuaRocksInstall(#[from] LuaRocksInstallError),
35    #[error("error installind dependencies:\n{0}")]
36    InstallDependencies(InstallError),
37    #[error("error installind build dependencies:\n{0}")]
38    InstallBuildDependencies(InstallError),
39    #[error("syncing dependencies with the project lockfile failed.\nUse --no-lock to force a new build.\n\n{0}")]
40    SyncDependencies(SyncError),
41    #[error("syncing build dependencies with the project lockfile failed.\nUse --no-lock to force a new build.\n\n{0}")]
42    SyncBuildDependencies(SyncError),
43    #[error("error building project:\n{0}")]
44    Build(#[from] BuildError),
45}
46
47#[derive(Builder)]
48#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
49pub struct BuildProject<'a> {
50    #[builder(start_fn)]
51    project: &'a Project,
52
53    #[builder(start_fn)]
54    config: &'a Config,
55
56    /// Ignore the project's lockfile and don't create one
57    no_lock: bool,
58
59    /// Build only the dependencies
60    only_deps: bool,
61
62    progress: Option<Arc<Progress<MultiProgress>>>,
63}
64
65impl<State: build_project_builder::State + build_project_builder::IsComplete>
66    BuildProjectBuilder<'_, State>
67{
68    /// Returns `Some` if the `only_deps` option is set to `false`.
69    pub async fn build(self) -> Result<Option<LocalPackage>, BuildProjectError> {
70        let args = self._build();
71        let project = args.project;
72        let config = args.config;
73        let progress_arc = args
74            .progress
75            .unwrap_or_else(|| MultiProgress::new_arc(config));
76        let progress = Arc::clone(&progress_arc);
77
78        let project_toml = project.toml().into_local()?;
79        let project_tree = project.tree(config)?;
80
81        let dependencies = project_toml
82            .dependencies()
83            .current_platform()
84            .iter()
85            .cloned()
86            .collect_vec();
87
88        let build_dependencies = project_toml
89            .build_dependencies()
90            .current_platform()
91            .iter()
92            .cloned()
93            .collect_vec();
94
95        let build_tree = project.build_tree(config)?;
96        let lua =
97            LuaInstallation::new_from_config(config, &progress.map(|progress| progress.new_bar()))
98                .await?;
99        let luarocks = LuaRocksInstallation::new(config, build_tree.clone())?;
100
101        if args.no_lock {
102            let dependencies_to_install = dependencies
103                .into_iter()
104                .filter(|dep| {
105                    project_tree
106                        .match_rocks(dep.package_req())
107                        .is_ok_and(|rock_match| !rock_match.is_found())
108                })
109                .map(|dep| {
110                    PackageInstallSpec::new(
111                        dep.clone().into_package_req(),
112                        tree::EntryType::Entrypoint,
113                    )
114                    .pin(*dep.pin())
115                    .opt(*dep.opt())
116                    .maybe_source(dep.source().clone())
117                    .build()
118                })
119                .collect();
120
121            Install::new(config)
122                .packages(dependencies_to_install)
123                .project(project)?
124                .progress(progress.clone())
125                .install()
126                .await
127                .map_err(BuildProjectError::InstallDependencies)?;
128
129            let build_dependencies_to_install = build_dependencies
130                .into_iter()
131                .filter(|dep| {
132                    project_tree
133                        .match_rocks(dep.package_req())
134                        .is_ok_and(|rock_match| !rock_match.is_found())
135                })
136                .map(|dep| {
137                    PackageInstallSpec::new(
138                        dep.clone().into_package_req(),
139                        tree::EntryType::Entrypoint,
140                    )
141                    .pin(*dep.pin())
142                    .opt(*dep.opt())
143                    .maybe_source(dep.source().clone())
144                    .build()
145                })
146                .collect_vec();
147
148            if !build_dependencies_to_install.is_empty() {
149                let bar = progress.map(|p| p.new_bar());
150                luarocks.ensure_installed(&lua, &bar).await?;
151                Install::new(config)
152                    .packages(build_dependencies_to_install)
153                    .tree(build_tree)
154                    .progress(progress.clone())
155                    .install()
156                    .await
157                    .map_err(BuildProjectError::InstallBuildDependencies)?;
158            }
159        } else {
160            Sync::new(project, config)
161                .progress(progress.clone())
162                .sync_dependencies()
163                .await
164                .map_err(BuildProjectError::SyncDependencies)?;
165
166            Sync::new(project, config)
167                .progress(progress.clone())
168                .sync_build_dependencies()
169                .await
170                .map_err(BuildProjectError::SyncBuildDependencies)?;
171        }
172
173        if !args.only_deps {
174            let package = Build::new()
175                .rockspec(&project_toml)
176                .lua(&lua)
177                .tree(&project_tree)
178                .entry_type(tree::EntryType::Entrypoint)
179                .config(config)
180                .progress(&progress.map(|p| p.new_bar()))
181                .behaviour(BuildBehaviour::Force)
182                .build()
183                .await?;
184
185            let lockfile = project_tree.lockfile()?;
186            let dependencies = lockfile
187                .rocks()
188                .iter()
189                .filter_map(|(pkg_id, value)| {
190                    if lockfile.is_entrypoint(pkg_id) {
191                        Some(value)
192                    } else {
193                        None
194                    }
195                })
196                .cloned()
197                .collect_vec();
198            let mut lockfile = lockfile.write_guard();
199            lockfile.add_entrypoint(&package);
200            for dep in dependencies {
201                lockfile.add_dependency(&package, &dep);
202                lockfile.remove_entrypoint(&dep);
203            }
204            Ok(Some(package))
205        } else {
206            Ok(None)
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    use crate::{
216        config::{ConfigBuilder, LuaVersion},
217        lua_installation::detect_installed_lua_version,
218        project::Project,
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        tokio::fs::create_dir_all(&foo_bin_dir).await.unwrap();
234        let foo_bin_file = foo_bin_dir.join("foo");
235        tokio::fs::write(&foo_bin_file, "print('hello')")
236            .await
237            .unwrap();
238        let bar_bin_dir = project_root.join("bin");
239        tokio::fs::create_dir_all(&bar_bin_dir).await.unwrap();
240        let bar_bin_file = bar_bin_dir.join("bar");
241        tokio::fs::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 project = Project::from_exact(project_root).unwrap().unwrap();
252        let tree = project.tree(&config).unwrap();
253        let package = BuildProject::new(&project, &config)
254            .no_lock(false)
255            .only_deps(false)
256            .build()
257            .await
258            .unwrap()
259            .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}