lux_lib/lua_rockspec/build/
mod.rs

1mod builtin;
2mod cmake;
3mod make;
4mod rust_mlua;
5mod tree_sitter;
6
7pub use builtin::{BuiltinBuildSpec, LuaModule, ModulePaths, ModuleSpec};
8pub use cmake::*;
9pub use make::*;
10pub use rust_mlua::*;
11pub use tree_sitter::*;
12
13use builtin::{
14    ModulePathsMissingSources, ModuleSpecAmbiguousPlatformOverride, ModuleSpecInternal,
15    ParseLuaModuleError,
16};
17
18use itertools::Itertools;
19
20use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, UserData, Value};
21use std::{
22    collections::HashMap,
23    env::consts::DLL_EXTENSION,
24    fmt::Display,
25    future::Future,
26    path::{Path, PathBuf},
27    str::FromStr,
28};
29use thiserror::Error;
30
31use serde::{de, de::IntoDeserializer, Deserialize, Deserializer};
32
33use crate::{
34    build::external_dependency::ExternalDependencyInfo,
35    config::Config,
36    lua_installation::LuaInstallation,
37    progress::{Progress, ProgressBar},
38    tree::{RockLayout, Tree},
39};
40
41use super::{
42    mlua_json_value_to_map, mlua_json_value_to_vec, DisplayAsLuaKV, DisplayAsLuaValue,
43    DisplayLuaKV, DisplayLuaValue, LuaTableKey, PartialOverride, PerPlatform, PlatformIdentifier,
44};
45
46/// The build specification for a given rock, serialized from `rockspec.build = { ... }`.
47///
48/// See [the rockspec format](https://github.com/luarocks/luarocks/wiki/Rockspec-format) for more
49/// info.
50#[derive(Clone, Debug, PartialEq)]
51pub struct BuildSpec {
52    /// Determines the build backend to use.
53    pub build_backend: Option<BuildBackendSpec>,
54    /// A set of instructions on how/where to copy files from the project.
55    // TODO(vhyrro): While we may want to support this, we also may want to supercede this in our
56    // new Lua project rewrite.
57    pub install: InstallSpec,
58    /// A list of directories that should be copied as-is into the resulting rock.
59    pub copy_directories: Vec<PathBuf>,
60    /// A list of patches to apply to the project before packaging it.
61    // NOTE: This cannot be a diffy::Patch<'a, str>
62    // because Lua::from_value requires a DeserializeOwned
63    pub patches: HashMap<PathBuf, String>,
64}
65
66impl Default for BuildSpec {
67    fn default() -> Self {
68        Self {
69            build_backend: Some(BuildBackendSpec::default()),
70            install: InstallSpec::default(),
71            copy_directories: Vec::default(),
72            patches: HashMap::default(),
73        }
74    }
75}
76
77impl UserData for BuildSpec {
78    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
79        methods.add_method("build_backend", |_, this, _: ()| {
80            Ok(this.build_backend.clone())
81        });
82        methods.add_method("install", |_, this, _: ()| Ok(this.install.clone()));
83        methods.add_method("copy_directories", |_, this, _: ()| {
84            Ok(this.copy_directories.clone())
85        });
86        methods.add_method("patches", |_, this, _: ()| Ok(this.patches.clone()));
87    }
88}
89
90#[derive(Error, Debug)]
91pub enum BuildSpecInternalError {
92    #[error("'builtin' modules should not have list elements")]
93    ModulesHaveListElements,
94    #[error("no 'build_command' specified for the 'command' build backend")]
95    NoBuildCommand,
96    #[error("no 'install_command' specified for the 'command' build backend")]
97    NoInstallCommand,
98    #[error("no 'modules' specified for the 'rust-mlua' build backend")]
99    NoModulesSpecified,
100    #[error("no 'lang' specified for 'treesitter-parser' build backend")]
101    NoTreesitterParserLanguageSpecified,
102    #[error("invalid 'rust-mlua' modules format")]
103    InvalidRustMLuaFormat,
104    #[error(transparent)]
105    ModulePathsMissingSources(#[from] ModulePathsMissingSources),
106    #[error(transparent)]
107    ParseLuaModuleError(#[from] ParseLuaModuleError),
108}
109
110impl BuildSpec {
111    pub(crate) fn from_internal_spec(
112        internal: BuildSpecInternal,
113    ) -> Result<Self, BuildSpecInternalError> {
114        let build_backend = match internal.build_type.unwrap_or_default() {
115            BuildType::Builtin => Some(BuildBackendSpec::Builtin(BuiltinBuildSpec {
116                modules: internal
117                    .builtin_spec
118                    .unwrap_or_default()
119                    .into_iter()
120                    .map(|(key, module_spec_internal)| {
121                        let key_str = match key {
122                            LuaTableKey::IntKey(_) => {
123                                Err(BuildSpecInternalError::ModulesHaveListElements)
124                            }
125                            LuaTableKey::StringKey(str) => Ok(LuaModule::from_str(str.as_str())?),
126                        }?;
127                        match ModuleSpec::from_internal(module_spec_internal) {
128                            Ok(module_spec) => Ok((key_str, module_spec)),
129                            Err(err) => Err(err.into()),
130                        }
131                    })
132                    .collect::<Result<HashMap<LuaModule, ModuleSpec>, BuildSpecInternalError>>()?,
133            })),
134            BuildType::Make => {
135                let default = MakeBuildSpec::default();
136                Some(BuildBackendSpec::Make(MakeBuildSpec {
137                    makefile: internal.makefile.unwrap_or(default.makefile),
138                    build_target: internal.make_build_target,
139                    build_pass: internal.build_pass.unwrap_or(default.build_pass),
140                    install_target: internal
141                        .make_install_target
142                        .unwrap_or(default.install_target),
143                    install_pass: internal.install_pass.unwrap_or(default.install_pass),
144                    build_variables: internal.make_build_variables.unwrap_or_default(),
145                    install_variables: internal.make_install_variables.unwrap_or_default(),
146                    variables: internal.variables.unwrap_or_default(),
147                }))
148            }
149            BuildType::CMake => {
150                let default = CMakeBuildSpec::default();
151                Some(BuildBackendSpec::CMake(CMakeBuildSpec {
152                    cmake_lists_content: internal.cmake_lists_content,
153                    build_pass: internal.build_pass.unwrap_or(default.build_pass),
154                    install_pass: internal.install_pass.unwrap_or(default.install_pass),
155                    variables: internal.variables.unwrap_or_default(),
156                }))
157            }
158            BuildType::Command => {
159                let build_command = internal
160                    .build_command
161                    .ok_or(BuildSpecInternalError::NoBuildCommand)?;
162                let install_command = internal
163                    .install_command
164                    .ok_or(BuildSpecInternalError::NoInstallCommand)?;
165                Some(BuildBackendSpec::Command(CommandBuildSpec {
166                    build_command,
167                    install_command,
168                }))
169            }
170            BuildType::None => None,
171            BuildType::LuaRock(s) => Some(BuildBackendSpec::LuaRock(s)),
172            BuildType::RustMlua => Some(BuildBackendSpec::RustMlua(RustMluaBuildSpec {
173                modules: internal
174                    .builtin_spec
175                    .ok_or(BuildSpecInternalError::NoModulesSpecified)?
176                    .into_iter()
177                    .map(|(key, value)| match (key, value) {
178                        (LuaTableKey::IntKey(_), ModuleSpecInternal::SourcePath(module)) => {
179                            let mut rust_lib: PathBuf = format!("lib{}", module.display()).into();
180                            rust_lib.set_extension(DLL_EXTENSION);
181                            Ok((module.to_string_lossy().to_string(), rust_lib))
182                        }
183                        (
184                            LuaTableKey::StringKey(module_name),
185                            ModuleSpecInternal::SourcePath(module),
186                        ) => {
187                            let mut rust_lib: PathBuf = format!("lib{}", module.display()).into();
188                            rust_lib.set_extension(DLL_EXTENSION);
189                            Ok((module_name, rust_lib))
190                        }
191                        _ => Err(BuildSpecInternalError::InvalidRustMLuaFormat),
192                    })
193                    .try_collect()?,
194                target_path: internal.target_path.unwrap_or("target".into()),
195                default_features: internal.default_features.unwrap_or(true),
196                include: internal
197                    .include
198                    .unwrap_or_default()
199                    .into_iter()
200                    .map(|(key, dest)| match key {
201                        LuaTableKey::IntKey(_) => (dest.clone(), dest),
202                        LuaTableKey::StringKey(src) => (src.into(), dest),
203                    })
204                    .collect(),
205                features: internal.features.unwrap_or_default(),
206            })),
207            BuildType::TreesitterParser => Some(BuildBackendSpec::TreesitterParser(
208                TreesitterParserBuildSpec {
209                    lang: internal
210                        .lang
211                        .ok_or(BuildSpecInternalError::NoTreesitterParserLanguageSpecified)?,
212                    parser: internal.parser.unwrap_or(false),
213                    generate: internal.generate.unwrap_or(false),
214                    location: internal.location,
215                    queries: internal.queries.unwrap_or_default(),
216                },
217            )),
218            BuildType::Source => Some(BuildBackendSpec::Source),
219        };
220        Ok(Self {
221            build_backend,
222            install: internal.install.unwrap_or_default(),
223            copy_directories: internal.copy_directories.unwrap_or_default(),
224            patches: internal.patches.unwrap_or_default(),
225        })
226    }
227}
228
229impl<'de> Deserialize<'de> for BuildSpec {
230    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231    where
232        D: Deserializer<'de>,
233    {
234        let internal = BuildSpecInternal::deserialize(deserializer)?;
235        BuildSpec::from_internal_spec(internal).map_err(de::Error::custom)
236    }
237}
238
239impl FromLua for PerPlatform<BuildSpec> {
240    fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
241        let internal = PerPlatform::from_lua(value, lua)?;
242        let mut per_platform = HashMap::new();
243        for (platform, internal_override) in internal.per_platform {
244            let override_spec = BuildSpec::from_internal_spec(internal_override)
245                .map_err(|err| mlua::Error::DeserializeError(err.to_string()))?;
246            per_platform.insert(platform, override_spec);
247        }
248        let result = PerPlatform {
249            default: BuildSpec::from_internal_spec(internal.default)
250                .map_err(|err| mlua::Error::DeserializeError(err.to_string()))?,
251            per_platform,
252        };
253        Ok(result)
254    }
255}
256
257impl Default for BuildBackendSpec {
258    fn default() -> Self {
259        Self::Builtin(BuiltinBuildSpec::default())
260    }
261}
262
263/// Encodes extra information about each backend.
264/// When selecting a backend, one may provide extra parameters
265/// to `build = { ... }` in order to further customize the behaviour of the build step.
266///
267/// Luarocks provides several default build types, these are also reflected in `lux`
268/// for compatibility.
269#[derive(Debug, PartialEq, Clone)]
270pub enum BuildBackendSpec {
271    Builtin(BuiltinBuildSpec),
272    Make(MakeBuildSpec),
273    CMake(CMakeBuildSpec),
274    Command(CommandBuildSpec),
275    LuaRock(String),
276    RustMlua(RustMluaBuildSpec),
277    TreesitterParser(TreesitterParserBuildSpec),
278    /// Build from the source rockspec, if present.
279    /// Otherwise, fall back to the builtin build and copy all directories.
280    /// This is currently unimplemented by luarocks, but we don't ever publish rockspecs
281    /// that implement this.
282    /// It could be implemented as a custom build backend.
283    Source,
284}
285
286impl IntoLua for BuildBackendSpec {
287    fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
288        match self {
289            BuildBackendSpec::Builtin(spec) => spec.into_lua(lua),
290            BuildBackendSpec::Make(spec) => spec.into_lua(lua),
291            BuildBackendSpec::CMake(spec) => spec.into_lua(lua),
292            BuildBackendSpec::Command(spec) => spec.into_lua(lua),
293            BuildBackendSpec::LuaRock(s) => s.into_lua(lua),
294            BuildBackendSpec::RustMlua(spec) => spec.into_lua(lua),
295            BuildBackendSpec::TreesitterParser(spec) => spec.into_lua(lua),
296            BuildBackendSpec::Source => "source".into_lua(lua),
297        }
298    }
299}
300
301#[derive(Debug, PartialEq, Clone)]
302pub struct CommandBuildSpec {
303    pub build_command: String,
304    pub install_command: String,
305}
306
307impl UserData for CommandBuildSpec {
308    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
309        methods.add_method("build_command", |_, this, _: ()| {
310            Ok(this.build_command.clone())
311        });
312        methods.add_method("install_command", |_, this, _: ()| {
313            Ok(this.install_command.clone())
314        });
315    }
316}
317
318#[derive(Clone, Debug)]
319pub(crate) enum InstallBinaries {
320    Array(Vec<PathBuf>),
321    Table(HashMap<String, PathBuf>),
322}
323
324impl<'de> Deserialize<'de> for InstallBinaries {
325    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
326    where
327        D: Deserializer<'de>,
328    {
329        let value: serde_json::Value = Deserialize::deserialize(deserializer)?;
330        if value.is_array() {
331            let array = mlua_json_value_to_vec(value).map_err(de::Error::custom)?;
332            Ok(InstallBinaries::Array(array))
333        } else {
334            let table: HashMap<String, PathBuf> =
335                mlua_json_value_to_map(value).map_err(de::Error::custom)?;
336            Ok(InstallBinaries::Table(table))
337        }
338    }
339}
340
341impl InstallBinaries {
342    pub(crate) fn coerce(self) -> HashMap<String, PathBuf> {
343        match self {
344            InstallBinaries::Array(array) => array
345                .into_iter()
346                .map(|path| {
347                    (
348                        path.file_stem().unwrap().to_str().unwrap().to_string(),
349                        path,
350                    )
351                })
352                .collect(),
353            InstallBinaries::Table(table) => table,
354        }
355    }
356}
357
358/// For packages which don't provide means to install modules
359/// and expect the user to copy the .lua or library files by hand to the proper locations.
360/// This struct contains categories of files. Each category is itself a table,
361/// where the array part is a list of filenames to be copied.
362/// For module directories only, in the hash part, other keys are identifiers in Lua module format,
363/// to indicate which subdirectory the file should be copied to.
364/// For example, build.install.lua = {["foo.bar"] = {"src/bar.lua"}} will copy src/bar.lua
365/// to the foo directory under the rock's Lua files directory.
366#[derive(Debug, PartialEq, Default, Deserialize, Clone)]
367pub struct InstallSpec {
368    /// Lua modules written in Lua.
369    #[serde(default)]
370    pub lua: HashMap<LuaModule, PathBuf>,
371    /// Dynamic libraries implemented compiled Lua modules.
372    #[serde(default)]
373    pub lib: HashMap<LuaModule, PathBuf>,
374    /// Configuration files.
375    #[serde(default)]
376    pub conf: HashMap<String, PathBuf>,
377    /// Lua command-line scripts.
378    // TODO(vhyrro): The String component should be checked to ensure that it consists of a single
379    // path component, such that targets like `my.binary` are not allowed.
380    #[serde(default, deserialize_with = "deserialize_binaries")]
381    pub bin: HashMap<String, PathBuf>,
382}
383
384impl UserData for InstallSpec {
385    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
386        methods.add_method("lua", |_, this, _: ()| Ok(this.lua.clone()));
387        methods.add_method("lib", |_, this, _: ()| Ok(this.lib.clone()));
388        methods.add_method("conf", |_, this, _: ()| Ok(this.conf.clone()));
389        methods.add_method("bin", |_, this, _: ()| Ok(this.bin.clone()));
390    }
391}
392
393fn deserialize_binaries<'de, D>(deserializer: D) -> Result<HashMap<String, PathBuf>, D::Error>
394where
395    D: Deserializer<'de>,
396{
397    let binaries = InstallBinaries::deserialize(deserializer)?;
398    Ok(binaries.coerce())
399}
400
401fn deserialize_copy_directories<'de, D>(deserializer: D) -> Result<Option<Vec<PathBuf>>, D::Error>
402where
403    D: Deserializer<'de>,
404{
405    let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
406    let copy_directories: Option<Vec<String>> = match value {
407        Some(json_value) => Some(mlua_json_value_to_vec(json_value).map_err(de::Error::custom)?),
408        None => None,
409    };
410    let special_directories: Vec<String> = vec!["lua".into(), "lib".into(), "rock_manifest".into()];
411    match special_directories
412        .into_iter()
413        .find(|dir| copy_directories.clone().unwrap_or_default().contains(dir))
414    {
415        // NOTE(mrcjkb): There also shouldn't be a directory named the same as the rockspec,
416        // but I'm not sure how to (or if it makes sense to) enforce this here.
417        Some(d) => Err(format!(
418            "directory '{}' in copy_directories clashes with the .rock format", // TODO(vhyrro): More informative error message.
419            d
420        )),
421        _ => Ok(copy_directories.map(|vec| vec.into_iter().map(PathBuf::from).collect())),
422    }
423    .map_err(de::Error::custom)
424}
425
426impl DisplayAsLuaKV for InstallSpec {
427    fn display_lua(&self) -> DisplayLuaKV {
428        let mut result = Vec::new();
429
430        self.lua
431            .iter()
432            .chain(self.lib.iter())
433            .for_each(|(key, value)| {
434                result.push(DisplayLuaKV {
435                    key: key.to_string(),
436                    value: DisplayLuaValue::String(value.to_string_lossy().to_string()),
437                });
438            });
439
440        self.conf
441            .iter()
442            .chain(self.bin.iter())
443            .for_each(|(key, value)| {
444                result.push(DisplayLuaKV {
445                    key: key.clone(),
446                    value: DisplayLuaValue::String(value.to_string_lossy().to_string()),
447                });
448            });
449
450        DisplayLuaKV {
451            key: "install".to_string(),
452            value: DisplayLuaValue::Table(result),
453        }
454    }
455}
456
457#[derive(Debug, PartialEq, Deserialize, Default, Clone)]
458pub(crate) struct BuildSpecInternal {
459    #[serde(rename = "type", default)]
460    pub(crate) build_type: Option<BuildType>,
461    #[serde(rename = "modules", default)]
462    pub(crate) builtin_spec: Option<HashMap<LuaTableKey, ModuleSpecInternal>>,
463    #[serde(default)]
464    pub(crate) makefile: Option<PathBuf>,
465    #[serde(rename = "build_target", default)]
466    pub(crate) make_build_target: Option<String>,
467    #[serde(default)]
468    pub(crate) build_pass: Option<bool>,
469    #[serde(rename = "install_target", default)]
470    pub(crate) make_install_target: Option<String>,
471    #[serde(default)]
472    pub(crate) install_pass: Option<bool>,
473    #[serde(rename = "build_variables", default)]
474    pub(crate) make_build_variables: Option<HashMap<String, String>>,
475    #[serde(rename = "install_variables", default)]
476    pub(crate) make_install_variables: Option<HashMap<String, String>>,
477    #[serde(default)]
478    pub(crate) variables: Option<HashMap<String, String>>,
479    #[serde(rename = "cmake", default)]
480    pub(crate) cmake_lists_content: Option<String>,
481    #[serde(default)]
482    pub(crate) build_command: Option<String>,
483    #[serde(default)]
484    pub(crate) install_command: Option<String>,
485    #[serde(default)]
486    pub(crate) install: Option<InstallSpec>,
487    #[serde(default, deserialize_with = "deserialize_copy_directories")]
488    pub(crate) copy_directories: Option<Vec<PathBuf>>,
489    #[serde(default)]
490    pub(crate) patches: Option<HashMap<PathBuf, String>>,
491    // rust-mlua fields
492    #[serde(default)]
493    pub(crate) target_path: Option<PathBuf>,
494    #[serde(default)]
495    pub(crate) default_features: Option<bool>,
496    #[serde(default)]
497    pub(crate) include: Option<HashMap<LuaTableKey, PathBuf>>,
498    #[serde(default)]
499    pub(crate) features: Option<Vec<String>>,
500    // treesitter-parser fields
501    #[serde(default)]
502    pub(crate) lang: Option<String>,
503    #[serde(default)]
504    pub(crate) parser: Option<bool>,
505    #[serde(default)]
506    pub(crate) generate: Option<bool>,
507    #[serde(default)]
508    pub(crate) location: Option<PathBuf>,
509    #[serde(default)]
510    pub(crate) queries: Option<HashMap<PathBuf, String>>,
511}
512
513impl FromLua for PerPlatform<BuildSpecInternal> {
514    fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
515        match &value {
516            list @ Value::Table(tbl) => {
517                let mut per_platform = match tbl.get("platforms")? {
518                    Value::Table(overrides) => Ok(lua.from_value(Value::Table(overrides))?),
519                    Value::Nil => Ok(HashMap::default()),
520                    val => Err(mlua::Error::DeserializeError(format!(
521                        "Expected rockspec 'build' to be table or nil, but got {}",
522                        val.type_name()
523                    ))),
524                }?;
525                let _ = tbl.raw_remove("platforms");
526                let default = lua.from_value(list.clone())?;
527                override_platform_specs(&mut per_platform, &default)
528                    .map_err(|err| mlua::Error::DeserializeError(err.to_string()))?;
529                Ok(PerPlatform {
530                    default,
531                    per_platform,
532                })
533            }
534            Value::Nil => Ok(PerPlatform::default()),
535            val => Err(mlua::Error::DeserializeError(format!(
536                "Expected rockspec 'build' to be a table or nil, but got {}",
537                val.type_name()
538            ))),
539        }
540    }
541}
542
543/// For each platform in `per_platform`, add the base specs,
544/// and apply overrides to the extended platforms of each platform override.
545fn override_platform_specs(
546    per_platform: &mut HashMap<PlatformIdentifier, BuildSpecInternal>,
547    base: &BuildSpecInternal,
548) -> Result<(), ModuleSpecAmbiguousPlatformOverride> {
549    let per_platform_raw = per_platform.clone();
550    for (platform, build_spec) in per_platform.clone() {
551        // Add base dependencies for each platform
552        per_platform.insert(platform, override_build_spec_internal(base, &build_spec)?);
553    }
554    for (platform, build_spec) in per_platform_raw {
555        for extended_platform in &platform.get_extended_platforms() {
556            let extended_spec = per_platform
557                .get(extended_platform)
558                .unwrap_or(&base.to_owned())
559                .to_owned();
560            per_platform.insert(
561                extended_platform.to_owned(),
562                override_build_spec_internal(&extended_spec, &build_spec)?,
563            );
564        }
565    }
566    Ok(())
567}
568
569fn override_build_spec_internal(
570    base: &BuildSpecInternal,
571    override_spec: &BuildSpecInternal,
572) -> Result<BuildSpecInternal, ModuleSpecAmbiguousPlatformOverride> {
573    Ok(BuildSpecInternal {
574        build_type: override_opt(&override_spec.build_type, &base.build_type),
575        builtin_spec: match (
576            override_spec.builtin_spec.clone(),
577            base.builtin_spec.clone(),
578        ) {
579            (Some(override_val), Some(base_spec_map)) => {
580                Some(base_spec_map.into_iter().chain(override_val).try_fold(
581                    HashMap::default(),
582                    |mut acc: HashMap<LuaTableKey, ModuleSpecInternal>,
583                     (k, module_spec_override)|
584                     -> Result<
585                        HashMap<LuaTableKey, ModuleSpecInternal>,
586                        ModuleSpecAmbiguousPlatformOverride,
587                    > {
588                        let overridden = match acc.get(&k) {
589                            None => module_spec_override,
590                            Some(base_module_spec) => {
591                                base_module_spec.apply_overrides(&module_spec_override)?
592                            }
593                        };
594                        acc.insert(k, overridden);
595                        Ok(acc)
596                    },
597                )?)
598            }
599            (override_val @ Some(_), _) => override_val,
600            (_, base_val @ Some(_)) => base_val,
601            _ => None,
602        },
603        makefile: override_opt(&override_spec.makefile, &base.makefile),
604        make_build_target: override_opt(&override_spec.make_build_target, &base.make_build_target),
605        build_pass: override_opt(&override_spec.build_pass, &base.build_pass),
606        make_install_target: override_opt(
607            &override_spec.make_install_target,
608            &base.make_install_target,
609        ),
610        install_pass: override_opt(&override_spec.install_pass, &base.install_pass),
611        make_build_variables: merge_map_opts(
612            &override_spec.make_build_variables,
613            &base.make_build_variables,
614        ),
615        make_install_variables: merge_map_opts(
616            &override_spec.make_install_variables,
617            &base.make_build_variables,
618        ),
619        variables: merge_map_opts(&override_spec.variables, &base.variables),
620        cmake_lists_content: override_opt(
621            &override_spec.cmake_lists_content,
622            &base.cmake_lists_content,
623        ),
624        build_command: override_opt(&override_spec.build_command, &base.build_command),
625        install_command: override_opt(&override_spec.install_command, &base.install_command),
626        install: override_opt(&override_spec.install, &base.install),
627        copy_directories: match (
628            override_spec.copy_directories.clone(),
629            base.copy_directories.clone(),
630        ) {
631            (Some(override_vec), Some(base_vec)) => {
632                let merged: Vec<PathBuf> =
633                    base_vec.into_iter().chain(override_vec).unique().collect();
634                Some(merged)
635            }
636            (None, base_vec @ Some(_)) => base_vec,
637            (override_vec @ Some(_), None) => override_vec,
638            _ => None,
639        },
640        patches: override_opt(&override_spec.patches, &base.patches),
641        target_path: override_opt(&override_spec.target_path, &base.target_path),
642        default_features: override_opt(&override_spec.default_features, &base.default_features),
643        features: override_opt(&override_spec.features, &base.features),
644        include: merge_map_opts(&override_spec.include, &base.include),
645        lang: override_opt(&override_spec.lang, &base.lang),
646        parser: override_opt(&override_spec.parser, &base.parser),
647        generate: override_opt(&override_spec.generate, &base.generate),
648        location: override_opt(&override_spec.location, &base.location),
649        queries: merge_map_opts(&override_spec.queries, &base.queries),
650    })
651}
652
653fn override_opt<T: Clone>(override_opt: &Option<T>, base: &Option<T>) -> Option<T> {
654    match override_opt.clone() {
655        override_val @ Some(_) => override_val,
656        None => base.clone(),
657    }
658}
659
660fn merge_map_opts<K, V>(
661    override_map: &Option<HashMap<K, V>>,
662    base_map: &Option<HashMap<K, V>>,
663) -> Option<HashMap<K, V>>
664where
665    K: Clone,
666    K: Eq,
667    K: std::hash::Hash,
668    V: Clone,
669{
670    match (override_map.clone(), base_map.clone()) {
671        (Some(override_map), Some(base_map)) => {
672            Some(base_map.into_iter().chain(override_map).collect())
673        }
674        (_, base_map @ Some(_)) => base_map,
675        (override_map @ Some(_), _) => override_map,
676        _ => None,
677    }
678}
679
680impl DisplayAsLuaKV for BuildSpecInternal {
681    fn display_lua(&self) -> DisplayLuaKV {
682        let mut result = Vec::new();
683
684        if let Some(build_type) = &self.build_type {
685            result.push(DisplayLuaKV {
686                key: "type".to_string(),
687                value: DisplayLuaValue::String(build_type.to_string()),
688            });
689        }
690        if let Some(builtin_spec) = &self.builtin_spec {
691            result.push(DisplayLuaKV {
692                key: "modules".to_string(),
693                value: DisplayLuaValue::Table(
694                    builtin_spec
695                        .iter()
696                        .map(|(key, value)| DisplayLuaKV {
697                            key: match key {
698                                LuaTableKey::StringKey(s) => s.clone(),
699                                LuaTableKey::IntKey(_) => unreachable!("integer key in modules"),
700                            },
701                            value: value.display_lua_value(),
702                        })
703                        .collect(),
704                ),
705            });
706        }
707        if let Some(makefile) = &self.makefile {
708            result.push(DisplayLuaKV {
709                key: "makefile".to_string(),
710                value: DisplayLuaValue::String(makefile.to_string_lossy().to_string()),
711            });
712        }
713        if let Some(make_build_target) = &self.make_build_target {
714            result.push(DisplayLuaKV {
715                key: "build_target".to_string(),
716                value: DisplayLuaValue::String(make_build_target.clone()),
717            });
718        }
719        if let Some(build_pass) = &self.build_pass {
720            result.push(DisplayLuaKV {
721                key: "build_pass".to_string(),
722                value: DisplayLuaValue::Boolean(*build_pass),
723            });
724        }
725        if let Some(make_install_target) = &self.make_install_target {
726            result.push(DisplayLuaKV {
727                key: "install_target".to_string(),
728                value: DisplayLuaValue::String(make_install_target.clone()),
729            });
730        }
731        if let Some(install_pass) = &self.install_pass {
732            result.push(DisplayLuaKV {
733                key: "install_pass".to_string(),
734                value: DisplayLuaValue::Boolean(*install_pass),
735            });
736        }
737        if let Some(make_build_variables) = &self.make_build_variables {
738            result.push(DisplayLuaKV {
739                key: "build_variables".to_string(),
740                value: DisplayLuaValue::Table(
741                    make_build_variables
742                        .iter()
743                        .map(|(key, value)| DisplayLuaKV {
744                            key: key.clone(),
745                            value: DisplayLuaValue::String(value.clone()),
746                        })
747                        .collect(),
748                ),
749            });
750        }
751        if let Some(make_install_variables) = &self.make_install_variables {
752            result.push(DisplayLuaKV {
753                key: "install_variables".to_string(),
754                value: DisplayLuaValue::Table(
755                    make_install_variables
756                        .iter()
757                        .map(|(key, value)| DisplayLuaKV {
758                            key: key.clone(),
759                            value: DisplayLuaValue::String(value.clone()),
760                        })
761                        .collect(),
762                ),
763            });
764        }
765        if let Some(variables) = &self.variables {
766            result.push(DisplayLuaKV {
767                key: "variables".to_string(),
768                value: DisplayLuaValue::Table(
769                    variables
770                        .iter()
771                        .map(|(key, value)| DisplayLuaKV {
772                            key: key.clone(),
773                            value: DisplayLuaValue::String(value.clone()),
774                        })
775                        .collect(),
776                ),
777            });
778        }
779        if let Some(cmake_lists_content) = &self.cmake_lists_content {
780            result.push(DisplayLuaKV {
781                key: "cmake".to_string(),
782                value: DisplayLuaValue::String(cmake_lists_content.clone()),
783            });
784        }
785        if let Some(build_command) = &self.build_command {
786            result.push(DisplayLuaKV {
787                key: "build_command".to_string(),
788                value: DisplayLuaValue::String(build_command.clone()),
789            });
790        }
791        if let Some(install_command) = &self.install_command {
792            result.push(DisplayLuaKV {
793                key: "install_command".to_string(),
794                value: DisplayLuaValue::String(install_command.clone()),
795            });
796        }
797        if let Some(install) = &self.install {
798            result.push(install.display_lua());
799        }
800        if let Some(copy_directories) = &self.copy_directories {
801            result.push(DisplayLuaKV {
802                key: "copy_directories".to_string(),
803                value: DisplayLuaValue::List(
804                    copy_directories
805                        .iter()
806                        .map(|path_buf| {
807                            DisplayLuaValue::String(path_buf.to_string_lossy().to_string())
808                        })
809                        .collect(),
810                ),
811            });
812        }
813        if let Some(patches) = &self.patches {
814            result.push(DisplayLuaKV {
815                key: "patches".to_string(),
816                value: DisplayLuaValue::Table(
817                    patches
818                        .iter()
819                        .map(|(key, value)| DisplayLuaKV {
820                            key: key.to_string_lossy().to_string(),
821                            value: DisplayLuaValue::String(value.clone()),
822                        })
823                        .collect(),
824                ),
825            });
826        }
827        if let Some(target_path) = &self.target_path {
828            result.push(DisplayLuaKV {
829                key: "target_path".to_string(),
830                value: DisplayLuaValue::String(target_path.to_string_lossy().to_string()),
831            });
832        }
833        if let Some(default_features) = &self.default_features {
834            result.push(DisplayLuaKV {
835                key: "default_features".to_string(),
836                value: DisplayLuaValue::Boolean(*default_features),
837            });
838        }
839        if let Some(include) = &self.include {
840            result.push(DisplayLuaKV {
841                key: "include".to_string(),
842                value: DisplayLuaValue::Table(
843                    include
844                        .iter()
845                        .map(|(key, value)| DisplayLuaKV {
846                            key: match key {
847                                LuaTableKey::StringKey(s) => s.clone(),
848                                LuaTableKey::IntKey(_) => unreachable!("integer key in include"),
849                            },
850                            value: DisplayLuaValue::String(value.to_string_lossy().to_string()),
851                        })
852                        .collect(),
853                ),
854            });
855        }
856        if let Some(features) = &self.features {
857            result.push(DisplayLuaKV {
858                key: "features".to_string(),
859                value: DisplayLuaValue::List(
860                    features
861                        .iter()
862                        .map(|feature| DisplayLuaValue::String(feature.clone()))
863                        .collect(),
864                ),
865            });
866        }
867        if let Some(lang) = &self.lang {
868            result.push(DisplayLuaKV {
869                key: "lang".to_string(),
870                value: DisplayLuaValue::String(lang.to_string()),
871            });
872        }
873        if let Some(parser) = &self.parser {
874            result.push(DisplayLuaKV {
875                key: "parser".to_string(),
876                value: DisplayLuaValue::Boolean(*parser),
877            });
878        }
879        if let Some(generate) = &self.generate {
880            result.push(DisplayLuaKV {
881                key: "generate".to_string(),
882                value: DisplayLuaValue::Boolean(*generate),
883            });
884        }
885        if let Some(location) = &self.location {
886            result.push(DisplayLuaKV {
887                key: "location".to_string(),
888                value: DisplayLuaValue::String(location.to_string_lossy().to_string()),
889            });
890        }
891        if let Some(queries) = &self.queries {
892            result.push(DisplayLuaKV {
893                key: "queries".to_string(),
894                value: DisplayLuaValue::Table(
895                    queries
896                        .iter()
897                        .map(|(key, value)| DisplayLuaKV {
898                            key: key.to_string_lossy().to_string(),
899                            value: DisplayLuaValue::String(value.to_string()),
900                        })
901                        .collect(),
902                ),
903            });
904        }
905
906        DisplayLuaKV {
907            key: "build".to_string(),
908            value: DisplayLuaValue::Table(result),
909        }
910    }
911}
912
913/// Maps `build.type` to an enum.
914#[derive(Debug, PartialEq, Deserialize, Clone)]
915#[serde(rename_all = "lowercase", remote = "BuildType")]
916pub(crate) enum BuildType {
917    /// "builtin" or "module"
918    Builtin,
919    /// "make"
920    Make,
921    /// "cmake"
922    CMake,
923    /// "command"
924    Command,
925    /// "none"
926    None,
927    /// external Lua rock
928    LuaRock(String),
929    #[serde(rename = "rust-mlua")]
930    RustMlua,
931    #[serde(rename = "treesitter-parser")]
932    TreesitterParser,
933    Source,
934}
935
936// Special Deserialize case for BuildType:
937// Both "module" and "builtin" map to `Builtin`
938impl<'de> Deserialize<'de> for BuildType {
939    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
940    where
941        D: Deserializer<'de>,
942    {
943        let s = String::deserialize(deserializer)?;
944        if s == "builtin" || s == "module" {
945            Ok(Self::Builtin)
946        } else {
947            match Self::deserialize(s.clone().into_deserializer()) {
948                Err(_) => Ok(Self::LuaRock(s)),
949                ok => ok,
950            }
951        }
952    }
953}
954
955impl Display for BuildType {
956    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
957        match self {
958            BuildType::Builtin => write!(f, "builtin"),
959            BuildType::Make => write!(f, "make"),
960            BuildType::CMake => write!(f, "cmake"),
961            BuildType::Command => write!(f, "command"),
962            BuildType::None => write!(f, "none"),
963            BuildType::LuaRock(s) => write!(f, "{}", s),
964            BuildType::RustMlua => write!(f, "rust-mlua"),
965            BuildType::TreesitterParser => write!(f, "treesitter-parser"),
966            BuildType::Source => write!(f, "source"),
967        }
968    }
969}
970
971impl Default for BuildType {
972    fn default() -> Self {
973        Self::Builtin
974    }
975}
976
977#[derive(Default)]
978pub struct BuildInfo {
979    pub binaries: Vec<PathBuf>,
980}
981
982// TODO(vhyrro): Move this to the dedicated build.rs module
983pub trait Build {
984    type Err: std::error::Error;
985
986    #[allow(clippy::too_many_arguments)]
987    fn run(
988        self,
989        output_paths: &RockLayout,
990        no_install: bool,
991        lua: &LuaInstallation,
992        external_dependencies: &HashMap<String, ExternalDependencyInfo>,
993        config: &Config,
994        tree: &Tree,
995        build_dir: &Path,
996        progress: &Progress<ProgressBar>,
997    ) -> impl Future<Output = Result<BuildInfo, Self::Err>> + Send;
998}
999
1000#[cfg(test)]
1001mod tests {
1002
1003    use super::*;
1004
1005    #[tokio::test]
1006    pub async fn deserialize_build_type() {
1007        let build_type: BuildType = serde_json::from_str("\"builtin\"").unwrap();
1008        assert_eq!(build_type, BuildType::Builtin);
1009        let build_type: BuildType = serde_json::from_str("\"module\"").unwrap();
1010        assert_eq!(build_type, BuildType::Builtin);
1011        let build_type: BuildType = serde_json::from_str("\"make\"").unwrap();
1012        assert_eq!(build_type, BuildType::Make);
1013        let build_type: BuildType = serde_json::from_str("\"custom_build_backend\"").unwrap();
1014        assert_eq!(
1015            build_type,
1016            BuildType::LuaRock("custom_build_backend".into())
1017        );
1018        let build_type: BuildType = serde_json::from_str("\"rust-mlua\"").unwrap();
1019        assert_eq!(build_type, BuildType::RustMlua);
1020    }
1021}