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