lux_lib/build/
builtin.rs

1use itertools::Itertools;
2use std::{
3    collections::{HashMap, HashSet},
4    io,
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8use thiserror::Error;
9use walkdir::WalkDir;
10
11use crate::{
12    build::utils,
13    config::Config,
14    lua_installation::LuaInstallation,
15    lua_rockspec::{Build, BuildInfo, BuiltinBuildSpec, DeploySpec, LuaModule, ModuleSpec},
16    progress::{Progress, ProgressBar},
17    tree::{RockLayout, Tree, TreeError},
18};
19
20use super::{
21    external_dependency::ExternalDependencyInfo,
22    utils::{CompileCFilesError, CompileCModulesError, InstallBinaryError},
23};
24
25#[derive(Error, Debug)]
26pub enum BuiltinBuildError {
27    #[error(transparent)]
28    CompileCFiles(#[from] CompileCFilesError),
29    #[error(transparent)]
30    CompileCModules(#[from] CompileCModulesError),
31    #[error("failed to install binary {0}: {1}")]
32    InstallBinary(String, InstallBinaryError),
33    #[error(transparent)]
34    Io(#[from] io::Error),
35    #[error(transparent)]
36    Tree(#[from] TreeError),
37}
38
39impl Build for BuiltinBuildSpec {
40    type Err = BuiltinBuildError;
41
42    async fn run(
43        self,
44        output_paths: &RockLayout,
45        _no_install: bool,
46        lua: &LuaInstallation,
47        external_dependencies: &HashMap<String, ExternalDependencyInfo>,
48        config: &Config,
49        tree: &Tree,
50        build_dir: &Path,
51        progress: &Progress<ProgressBar>,
52    ) -> Result<BuildInfo, Self::Err> {
53        // Detect all Lua modules
54        let modules = autodetect_modules(build_dir, source_paths(build_dir, &self.modules))
55            .into_iter()
56            .chain(self.modules)
57            .collect::<HashMap<_, _>>();
58
59        progress.map(|p| p.set_position(modules.len() as u64));
60
61        for (destination_path, module_type) in modules.iter() {
62            match module_type {
63                ModuleSpec::SourcePath(source) => {
64                    if source.extension().map(|ext| ext == "c").unwrap_or(false) {
65                        progress.map(|p| {
66                            p.set_message(format!(
67                                "Compiling {} -> {}...",
68                                &source.to_string_lossy(),
69                                &destination_path
70                            ))
71                        });
72                        let absolute_source_paths = vec![build_dir.join(source)];
73                        utils::compile_c_files(
74                            &absolute_source_paths,
75                            destination_path,
76                            &output_paths.lib,
77                            lua,
78                            external_dependencies,
79                        )?
80                    } else {
81                        progress.map(|p| {
82                            p.set_message(format!(
83                                "Copying {} to {}...",
84                                &source.to_string_lossy(),
85                                &destination_path
86                            ))
87                        });
88                        let absolute_source_path = build_dir.join(source);
89                        utils::copy_lua_to_module_path(
90                            &absolute_source_path,
91                            destination_path,
92                            &output_paths.src,
93                        )?
94                    }
95                }
96                ModuleSpec::SourcePaths(files) => {
97                    progress.map(|p| p.set_message("Compiling C files..."));
98                    let absolute_source_paths =
99                        files.iter().map(|file| build_dir.join(file)).collect();
100                    utils::compile_c_files(
101                        &absolute_source_paths,
102                        destination_path,
103                        &output_paths.lib,
104                        lua,
105                        external_dependencies,
106                    )?
107                }
108                ModuleSpec::ModulePaths(data) => {
109                    progress.map(|p| p.set_message("Compiling C modules..."));
110                    utils::compile_c_modules(
111                        data,
112                        build_dir,
113                        destination_path,
114                        &output_paths.lib,
115                        lua,
116                        external_dependencies,
117                    )?
118                }
119            }
120        }
121
122        let mut binaries = Vec::new();
123        for target in autodetect_src_bin_scripts(build_dir) {
124            std::fs::create_dir_all(target.parent().unwrap())?;
125            let target = target.to_string_lossy().to_string();
126            let source = build_dir.join("src").join("bin").join(&target);
127            // Let's not care about the rockspec's deploy field for auto-detected bin scripts
128            // If package maintainers want to disable wrapping via the rockspec, they should
129            // specify binaries in the rockspec.
130            let installed_bin_script =
131                utils::install_binary(&source, &target, tree, lua, &DeploySpec::default(), config)
132                    .await
133                    .map_err(|err| BuiltinBuildError::InstallBinary(target.clone(), err))?;
134            binaries.push(
135                installed_bin_script
136                    .file_name()
137                    .expect("no file name")
138                    .into(),
139            );
140        }
141
142        Ok(BuildInfo { binaries })
143    }
144}
145
146fn source_paths(build_dir: &Path, modules: &HashMap<LuaModule, ModuleSpec>) -> HashSet<PathBuf> {
147    modules
148        .iter()
149        .flat_map(|(_, spec)| match spec {
150            ModuleSpec::SourcePath(path_buf) => vec![path_buf],
151            ModuleSpec::SourcePaths(vec) => vec.iter().collect_vec(),
152            ModuleSpec::ModulePaths(module_paths) => module_paths.sources.iter().collect_vec(),
153        })
154        .map(|path| build_dir.join(path))
155        .collect()
156}
157
158fn autodetect_modules(
159    build_dir: &Path,
160    exclude: HashSet<PathBuf>,
161) -> HashMap<LuaModule, ModuleSpec> {
162    WalkDir::new(build_dir.join("src"))
163        .into_iter()
164        .chain(WalkDir::new(build_dir.join("lua")))
165        .chain(WalkDir::new(build_dir.join("lib")))
166        .filter_map(|file| {
167            file.ok().and_then(|file| {
168                let is_lua_file = PathBuf::from(file.file_name())
169                    .extension()
170                    .map(|ext| ext == "lua")
171                    .unwrap_or(false);
172                if is_lua_file && !exclude.contains(&file.clone().into_path()) {
173                    Some(file)
174                } else {
175                    None
176                }
177            })
178        })
179        .map(|file| {
180            let diff: PathBuf =
181                pathdiff::diff_paths(build_dir.join(file.clone().into_path()), build_dir)
182                    .expect("failed to autodetect modules");
183
184            // NOTE(vhyrro): You may ask why we convert all paths to Lua module paths
185            // just to convert them back later in the `run()` stage.
186            //
187            // The rockspec requires the format to be like this, and representing our
188            // data in this form allows us to respect any overrides made by the user (which follow
189            // the `module.name` format, not our internal one).
190            let pathbuf = diff.components().skip(1).collect::<PathBuf>();
191            let mut lua_module = LuaModule::from_pathbuf(pathbuf);
192
193            // NOTE(mrcjkb): `LuaModule` does not parse as "<module>.init" from files named "init.lua"
194            // To make sure we don't change the file structure when installing, we append it here.
195            if file.file_name().to_string_lossy().as_bytes() == b"init.lua" {
196                lua_module = lua_module.join(&LuaModule::from_str("init").unwrap())
197            }
198
199            (lua_module, ModuleSpec::SourcePath(diff))
200        })
201        .collect()
202}
203
204fn autodetect_src_bin_scripts(build_dir: &Path) -> Vec<PathBuf> {
205    WalkDir::new(build_dir.join("src").join("bin"))
206        .into_iter()
207        .filter_map(|file| file.ok())
208        .filter(|file| file.clone().into_path().is_file())
209        .map(|file| {
210            let diff = pathdiff::diff_paths(build_dir.join(file.into_path()), build_dir)
211                .expect("failed to autodetect bin scripts");
212            diff.components().skip(2).collect::<PathBuf>()
213        })
214        .collect()
215}