Skip to main content

lux_lib/lua_rockspec/build/
mod.rs

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