Skip to main content

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