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