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