Skip to main content

lux_lib/lua_rockspec/build/
mod.rs

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