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