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