Skip to main content

lux_lib/lua_rockspec/build/
mod.rs

1mod builtin;
2mod cmake;
3mod make;
4mod rust_binary;
5mod rust_mlua;
6mod tree_sitter;
7
8use crate::package;
9pub use builtin::{BuiltinBuildSpec, LuaModule, ModulePaths, ModuleSpec, ParseLuaModuleError};
10pub use cmake::*;
11pub use make::*;
12use path_slash::PathBufExt;
13pub use rust_binary::*;
14pub use rust_mlua::*;
15pub use tree_sitter::*;
16
17pub(crate) use builtin::ModuleSpecInternal;
18
19use builtin::{ModulePathsMissingSources, ModuleSpecAmbiguousPlatformOverride};
20
21use itertools::Itertools;
22
23use miette::Diagnostic;
24use serde::{de, de::IntoDeserializer, Deserialize, Deserializer};
25use std::{
26    collections::HashMap, convert::Infallible, env::consts::DLL_EXTENSION, env::consts::DLL_PREFIX,
27    fmt::Display, path::PathBuf, str::FromStr,
28};
29use thiserror::Error;
30
31use crate::{
32    lua_rockspec::per_platform_from_intermediate, 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 `build = { ... }`.
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, Diagnostic)]
69#[non_exhaustive]
70pub enum BuildSpecInternalError {
71    #[error("'builtin' modules should not have list elements")]
72    ModulesHaveListElements,
73    #[error("no 'modules' specified for the 'rust-mlua' build backend")]
74    NoModulesSpecified,
75    #[error("no 'lang' specified for 'treesitter-parser' build backend")]
76    NoTreesitterParserLanguageSpecified,
77    #[error("invalid 'rust-mlua' modules format")]
78    InvalidRustMLuaFormat,
79    #[error(transparent)]
80    #[diagnostic(transparent)]
81    ModulePathsMissingSources(#[from] ModulePathsMissingSources),
82    #[error(transparent)]
83    #[diagnostic(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::RustBinary => Some(BuildBackendSpec::RustBinary(RustBinaryBuildSpec {
180                package: internal.package,
181                features: internal.features.unwrap_or_default(),
182            })),
183            BuildType::TreesitterParser => Some(BuildBackendSpec::TreesitterParser(
184                TreesitterParserBuildSpec {
185                    lang: internal
186                        .lang
187                        .ok_or(BuildSpecInternalError::NoTreesitterParserLanguageSpecified)?,
188                    parser: internal.parser.unwrap_or(true),
189                    generate: internal.generate.unwrap_or(false),
190                    location: internal.location,
191                    queries: internal.queries.unwrap_or_default(),
192                },
193            )),
194            BuildType::Source => Some(BuildBackendSpec::Source),
195        };
196        Ok(Self {
197            build_backend,
198            install: internal.install.unwrap_or_default(),
199            copy_directories: internal.copy_directories.unwrap_or_default(),
200            patches: internal.patches.unwrap_or_default(),
201        })
202    }
203}
204
205impl TryFrom<BuildSpecInternal> for BuildSpec {
206    type Error = BuildSpecInternalError;
207
208    fn try_from(internal: BuildSpecInternal) -> Result<Self, Self::Error> {
209        BuildSpec::from_internal_spec(internal)
210    }
211}
212
213impl<'de> Deserialize<'de> for BuildSpec {
214    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
215    where
216        D: Deserializer<'de>,
217    {
218        let internal = BuildSpecInternal::deserialize(deserializer)?;
219        BuildSpec::from_internal_spec(internal).map_err(de::Error::custom)
220    }
221}
222
223// TODO(vhyrro): Remove this when we migrate to deepmerge.
224// This is a hacky implementation that would work normally with just the above deserialization
225// strategy however since there is no PlatformOevrridable implemented for this struct this is
226// necessary.
227impl<'de> Deserialize<'de> for PerPlatform<BuildSpec> {
228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229    where
230        D: Deserializer<'de>,
231    {
232        per_platform_from_intermediate::<_, BuildSpecInternal, _>(deserializer)
233    }
234}
235
236impl Default for BuildBackendSpec {
237    fn default() -> Self {
238        Self::Builtin(BuiltinBuildSpec::default())
239    }
240}
241
242/// Encodes extra information about each backend.
243/// When selecting a backend, one may provide extra parameters
244/// to `build = { ... }` in order to further customize the behaviour of the build step.
245///
246/// Luarocks provides several default build types, these are also reflected in `lux`
247/// for compatibility.
248#[derive(Debug, PartialEq, Clone)]
249pub enum BuildBackendSpec {
250    Builtin(BuiltinBuildSpec),
251    Make(MakeBuildSpec),
252    CMake(CMakeBuildSpec),
253    Command(CommandBuildSpec),
254    LuaRock(String),
255    RustMlua(RustMluaBuildSpec),
256    RustBinary(RustBinaryBuildSpec),
257    TreesitterParser(TreesitterParserBuildSpec),
258    /// Build from the source rockspec, if present.
259    /// Otherwise, fall back to the builtin build and copy all directories.
260    /// This is currently unimplemented by luarocks, but we don't ever publish rockspecs
261    /// that implement this.
262    /// It could be implemented as a custom build backend.
263    Source,
264}
265
266impl BuildBackendSpec {
267    pub(crate) fn can_use_build_dependencies(&self) -> bool {
268        match self {
269            Self::Make(_) | Self::CMake(_) | Self::Command(_) | Self::LuaRock(_) => true,
270            Self::Builtin(_)
271            | Self::RustMlua(_)
272            | Self::RustBinary(_)
273            | Self::TreesitterParser(_)
274            | Self::Source => false,
275        }
276    }
277}
278
279/// Specification for building a rock with the `command` build backend
280#[derive(Debug, PartialEq, Clone)]
281pub struct CommandBuildSpec {
282    pub build_command: Option<String>,
283    pub install_command: Option<String>,
284}
285
286#[derive(Clone, Debug)]
287struct LuaPathBufTable(HashMap<LuaTableKey, PathBuf>);
288
289impl<'de> Deserialize<'de> for LuaPathBufTable {
290    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
291        Ok(LuaPathBufTable(
292            deserialize_map_or_seq(deserializer)?.unwrap_or_default(),
293        ))
294    }
295}
296
297impl LuaPathBufTable {
298    fn coerce<S>(self) -> Result<HashMap<S, PathBuf>, S::Err>
299    where
300        S: FromStr + Eq + std::hash::Hash,
301    {
302        self.0
303            .into_iter()
304            .map(|(key, value)| {
305                let key = match key {
306                    LuaTableKey::IntKey(_) => value
307                        .with_extension("")
308                        .file_name()
309                        .unwrap_or_default()
310                        .to_string_lossy()
311                        .to_string(),
312                    LuaTableKey::StringKey(key) => key,
313                };
314                Ok((S::from_str(&key)?, value))
315            })
316            .try_collect()
317    }
318}
319
320#[derive(Clone, Debug)]
321struct LibPathBufTable(HashMap<LuaTableKey, PathBuf>);
322
323impl<'de> Deserialize<'de> for LibPathBufTable {
324    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325        Ok(LibPathBufTable(
326            deserialize_map_or_seq(deserializer)?.unwrap_or_default(),
327        ))
328    }
329}
330
331impl LibPathBufTable {
332    fn coerce<S>(self) -> Result<HashMap<S, PathBuf>, S::Err>
333    where
334        S: FromStr + Eq + std::hash::Hash,
335    {
336        self.0
337            .into_iter()
338            .map(|(key, value)| {
339                let key = match key {
340                    LuaTableKey::IntKey(_) => value
341                        .file_name()
342                        .unwrap_or_default()
343                        .to_string_lossy()
344                        .to_string(),
345                    LuaTableKey::StringKey(key) => key,
346                };
347                Ok((S::from_str(&key)?, value))
348            })
349            .try_collect()
350    }
351}
352
353/// For packages which don't provide means to install modules
354/// and expect the user to copy the .lua or library files by hand to the proper locations.
355/// This struct contains categories of files. Each category is itself a table,
356/// where the array part is a list of filenames to be copied.
357/// For module directories only, in the hash part, other keys are identifiers in Lua module format,
358/// to indicate which subdirectory the file should be copied to.
359/// For example, lua = {["foo.bar"] = "src/bar.lua"} will copy src/bar.lua
360/// to the foo directory under the rock's Lua files directory.
361#[derive(Debug, PartialEq, Default, Deserialize, Clone, lux_macros::DisplayAsLuaKV)]
362#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
363#[display_lua(key = "install")]
364pub struct InstallSpec {
365    /// Lua modules written in Lua.
366    #[serde(default, deserialize_with = "deserialize_module_path_map")]
367    #[cfg_attr(
368        feature = "schema",
369        schemars(with = "std::collections::BTreeMap<String, String>")
370    )]
371    pub lua: HashMap<LuaModule, PathBuf>,
372    /// Dynamic libraries implemented compiled Lua modules.
373    #[serde(default, deserialize_with = "deserialize_file_name_path_map")]
374    #[cfg_attr(
375        feature = "schema",
376        schemars(with = "std::collections::BTreeMap<String, String>")
377    )]
378    pub lib: HashMap<String, PathBuf>,
379    /// Configuration files.
380    #[serde(default)]
381    pub conf: HashMap<String, PathBuf>,
382    /// Lua command-line scripts.
383    // TODO(vhyrro): The String component should be checked to ensure that it consists of a single
384    // path component, such that targets like `my.binary` are not allowed.
385    #[serde(default, deserialize_with = "deserialize_file_name_path_map")]
386    #[cfg_attr(
387        feature = "schema",
388        schemars(with = "std::collections::BTreeMap<String, String>")
389    )]
390    pub bin: HashMap<String, PathBuf>,
391}
392
393fn deserialize_module_path_map<'de, D>(
394    deserializer: D,
395) -> Result<HashMap<LuaModule, PathBuf>, D::Error>
396where
397    D: Deserializer<'de>,
398{
399    let modules = LuaPathBufTable::deserialize(deserializer)?;
400    modules.coerce().map_err(de::Error::custom)
401}
402
403fn deserialize_file_name_path_map<'de, D>(
404    deserializer: D,
405) -> Result<HashMap<String, PathBuf>, D::Error>
406where
407    D: Deserializer<'de>,
408{
409    let binaries = LibPathBufTable::deserialize(deserializer)?;
410    binaries.coerce().map_err(de::Error::custom)
411}
412
413fn deserialize_copy_directories<'de, D>(deserializer: D) -> Result<Option<Vec<PathBuf>>, D::Error>
414where
415    D: Deserializer<'de>,
416{
417    let value: Option<serde_value::Value> = Option::deserialize(deserializer)?;
418    let copy_directories: Option<Vec<String>> = match value {
419        Some(value) => Some(value.deserialize_into().map_err(de::Error::custom)?),
420        None => None,
421    };
422    let special_directories: Vec<String> = vec!["lua".into(), "lib".into(), "rock_manifest".into()];
423    match special_directories
424        .into_iter()
425        .find(|dir| copy_directories.clone().unwrap_or_default().contains(dir))
426    {
427        // NOTE(mrcjkb): There also shouldn't be a directory named the same as the rockspec,
428        // but I'm not sure how to (or if it makes sense to) enforce this here.
429        Some(d) => Err(format!(
430            "directory '{d}' in copy_directories clashes with the .rock format", // TODO(vhyrro): More informative error message.
431        )),
432        _ => Ok(copy_directories.map(|vec| vec.into_iter().map(PathBuf::from).collect())),
433    }
434    .map_err(de::Error::custom)
435}
436
437/// Deserializes a map that may be represented as a sequence (integer-indexed Lua array).
438fn deserialize_map_or_seq<'de, D, V>(
439    deserializer: D,
440) -> Result<Option<HashMap<LuaTableKey, V>>, D::Error>
441where
442    D: Deserializer<'de>,
443    V: de::DeserializeOwned,
444{
445    match de::DeserializeSeed::deserialize(LuaValueSeed, deserializer).map_err(de::Error::custom)? {
446        serde_value::Value::Map(map) => map
447            .into_iter()
448            .map(|(k, v)| {
449                let key = match k {
450                    serde_value::Value::I64(i) => LuaTableKey::IntKey(i as u64),
451                    serde_value::Value::U64(u) => LuaTableKey::IntKey(u),
452                    serde_value::Value::String(s) => LuaTableKey::StringKey(s),
453                    other => {
454                        return Err(de::Error::custom(format!("unexpected map key: {other:?}")))
455                    }
456                };
457                let val = v.deserialize_into::<V>().map_err(de::Error::custom)?;
458                Ok((key, val))
459            })
460            .try_collect()
461            .map(Some),
462        serde_value::Value::Seq(seq) => seq
463            .into_iter()
464            .enumerate()
465            .map(|(i, v)| {
466                let val = v.deserialize_into::<V>().map_err(de::Error::custom)?;
467                Ok((LuaTableKey::IntKey(i as u64 + 1), val))
468            })
469            .try_collect()
470            .map(Some),
471        serde_value::Value::Unit => Ok(None),
472        other => Err(de::Error::custom(format!(
473            "expected a table or nil, got {other:?}"
474        ))),
475    }
476}
477
478fn display_builtin_spec(spec: &HashMap<LuaTableKey, ModuleSpecInternal>) -> DisplayLuaValue {
479    DisplayLuaValue::Table(
480        spec.iter()
481            .map(|(key, value)| DisplayLuaKV {
482                key: match key {
483                    LuaTableKey::StringKey(s) => s.clone(),
484                    LuaTableKey::IntKey(_) => unreachable!("integer key in modules"),
485                },
486                value: value.display_lua_value(),
487            })
488            .collect(),
489    )
490}
491
492fn display_path_string_map(map: &HashMap<PathBuf, String>) -> DisplayLuaValue {
493    DisplayLuaValue::Table(
494        map.iter()
495            .map(|(k, v)| DisplayLuaKV {
496                key: k.to_slash_lossy().into_owned(),
497                value: DisplayLuaValue::String(v.clone()),
498            })
499            .collect(),
500    )
501}
502
503fn display_include(include: &HashMap<LuaTableKey, PathBuf>) -> DisplayLuaValue {
504    DisplayLuaValue::Table(
505        include
506            .iter()
507            .map(|(key, value)| DisplayLuaKV {
508                key: match key {
509                    LuaTableKey::StringKey(s) => s.clone(),
510                    LuaTableKey::IntKey(_) => unreachable!("integer key in include"),
511                },
512                value: DisplayLuaValue::String(value.to_slash_lossy().into_owned()),
513            })
514            .collect(),
515    )
516}
517
518#[derive(Debug, PartialEq, Deserialize, Default, Clone, lux_macros::DisplayAsLuaKV)]
519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
520#[display_lua(key = "build")]
521pub(crate) struct BuildSpecInternal {
522    /// The build backend to use: "builtin", "make", "cmake",
523    /// "command", "none", "rust-mlua", "rust-binary", or "treesitter-parser".
524    /// Any other value is treated as a custom LuaRocks build backend.
525    /// Default: "builtin".
526    ///
527    /// `make` requires Make, `cmake` requires CMake, and `rust-mlua`/`rust-binary`
528    /// require a Rust toolchain. Set `type = "none"` to copy source files
529    /// without building.
530    ///
531    /// For Luau projects, `builtin` auto-detects `.luau` modules. Native
532    /// dependencies are not supported. Luau modules are transpiled to Lua when
533    /// the project targets a Lua runtime.
534    #[serde(rename = "type", default)]
535    #[display_lua(rename = "type")]
536    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
537    #[cfg_attr(
538        feature = "schema",
539        schemars(extend("x-lux-toml-example" = r#"
540[build]
541type = "builtin"
542"#))
543    )]
544    pub(crate) build_type: Option<BuildType>,
545    /// Modules to build, mapping module names (as used by `require`) to a source
546    /// path, a list of C sources, or a table with `sources`, `libraries`,
547    /// `defines`, `incdirs`, and `libdirs`.
548    #[serde(
549        rename = "modules",
550        default,
551        deserialize_with = "deserialize_map_or_seq"
552    )]
553    #[display_lua(rename = "modules", convert_with = "display_builtin_spec")]
554    #[cfg_attr(
555        feature = "schema",
556        schemars(with = "Option<std::collections::BTreeMap<String, crate::schema::ModuleSpec>>")
557    )]
558    #[cfg_attr(
559        feature = "schema",
560        schemars(extend("x-lux-toml-example" = r#"
561[build.modules]
562"foo.main" = "lua/foo.lua"
563"foo.bla" = ["src/bla.c"]
564
565[build.modules."foo.core"]
566sources = ["src/core.c", "src/extra.c"]
567libraries = ["ssl"]
568defines = ["FOO=bar", "USE_BLA"]
569incdirs = []
570libdirs = []
571"#))
572    )]
573    pub(crate) builtin_spec: Option<HashMap<LuaTableKey, ModuleSpecInternal>>,
574    /// Makefile to use. Default: "Makefile" ("Makefile.win" on Windows).
575    #[serde(default)]
576    #[cfg_attr(
577        feature = "schema",
578        schemars(extend("x-lux-toml-example" = r#"
579[build]
580type = "make"
581makefile = "Makefile"
582build_target = ""
583build_pass = true
584install_target = "install"
585install_pass = true
586build_variables = {}
587install_variables = {}
588variables = {}
589"#))
590    )]
591    pub(crate) makefile: Option<PathBuf>,
592    /// Target passed to `make` during the build pass. Default: empty.
593    #[serde(rename = "build_target", default)]
594    #[display_lua(rename = "build_target")]
595    pub(crate) make_build_target: Option<String>,
596    /// Whether to perform a build pass. Default: `true`.
597    #[serde(default)]
598    pub(crate) build_pass: Option<bool>,
599    /// Target passed to `make` during the install pass. Default: "install".
600    #[serde(rename = "install_target", default)]
601    #[display_lua(rename = "install_target")]
602    pub(crate) make_install_target: Option<String>,
603    /// Whether to perform an install pass. Default: `true`.
604    #[serde(default)]
605    pub(crate) install_pass: Option<bool>,
606    /// Make variable assignments for the build pass.
607    #[serde(rename = "build_variables", default)]
608    #[display_lua(rename = "build_variables")]
609    pub(crate) make_build_variables: Option<HashMap<String, String>>,
610    /// Make variable assignments for the install pass.
611    #[serde(rename = "install_variables", default)]
612    #[display_lua(rename = "install_variables")]
613    pub(crate) make_install_variables: Option<HashMap<String, String>>,
614    /// Make or CMake variable assignments applied to both passes.
615    #[serde(default)]
616    pub(crate) variables: Option<HashMap<String, String>>,
617    /// Contents of the generated "CMakeLists.txt". If unset, the project's own
618    /// file is used.
619    ///
620    /// Environment variables: `CMAKE_MODULE_PATH`, `CMAKE_INCLUDE_PATH`,
621    /// `CMAKE_LIBRARY_PATH`.
622    #[serde(rename = "cmake", default)]
623    #[display_lua(rename = "cmake")]
624    #[cfg_attr(
625        feature = "schema",
626        schemars(extend("x-lux-toml-example" = r#"
627[build]
628type = "cmake"
629cmake = """
630<content>
631"""
632"#))
633    )]
634    pub(crate) cmake_lists_content: Option<String>,
635    /// Command to run to build the package.
636    #[serde(default)]
637    #[cfg_attr(
638        feature = "schema",
639        schemars(extend("x-lux-toml-example" = r#"
640[build]
641type = "command"
642build_command = "make"
643install_command = "make install"
644"#))
645    )]
646    pub(crate) build_command: Option<String>,
647    /// Command to run to install the package.
648    #[serde(default)]
649    pub(crate) install_command: Option<String>,
650    /// Files to install, by category ("lua", "lib", "conf", "bin").
651    #[serde(default)]
652    #[cfg_attr(
653        feature = "schema",
654        schemars(extend("x-lux-toml-example" = r#"
655[build.install.lua]
656"foo.bar" = "src/bar.lua"
657
658[build.install.lib]
659"foo.bar" = "lib/bar.so"
660
661[build.install.conf]
662
663[build.install.bin]
664"#))
665    )]
666    pub(crate) install: Option<InstallSpec>,
667    /// Directories in the source tree to copy as-is into the rock's "etc" tree.
668    /// Do not use "lua", "lib", "rock_manifest", or the rockspec file name.
669    #[serde(default, deserialize_with = "deserialize_copy_directories")]
670    #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<String>>"))]
671    #[cfg_attr(
672        feature = "schema",
673        schemars(extend("x-lux-toml-example" = r#"
674[build]
675copy_directories = ["plugin", "ftplugin"]
676"#))
677    )]
678    pub(crate) copy_directories: Option<Vec<PathBuf>>,
679    /// Patches to apply before building, mapping file names to patch contents.
680    /// Lux accepts any format supported by the [`diffy`](https://docs.rs/diffy/latest/diffy/) crate.
681    /// LuaRocks only supports unified diff.
682    #[serde(default)]
683    #[display_lua(convert_with = "display_path_string_map")]
684    #[cfg_attr(
685        feature = "schema",
686        schemars(with = "Option<std::collections::BTreeMap<String, String>>")
687    )]
688    #[cfg_attr(
689        feature = "schema",
690        schemars(extend("x-lux-toml-example" = r#"
691[build.patches]
692"lua51-support.diff" = """
693--- old/mymodule.c....
694"""
695"#))
696    )]
697    pub(crate) patches: Option<HashMap<PathBuf, String>>,
698    /// The cargo target directory, if it is not `<source>/target`.
699    #[serde(default)]
700    #[cfg_attr(
701        feature = "schema",
702        schemars(extend("x-lux-toml-example" = r#"
703[build]
704type = "rust-mlua"
705target_path = "target"
706default_features = false
707features = []
708cargo_extra_args = []
709
710[build.modules]
711"foo.main" = "alt_name"
712
713[build.include]
714"relative/path/to/file.lua" = "target-file.lua"
715"#))
716    )]
717    pub(crate) target_path: Option<PathBuf>,
718    /// Whether to build a cargo package with default features. Default: `true`.
719    #[serde(default)]
720    pub(crate) default_features: Option<bool>,
721    /// Cargo features to enable.
722    #[serde(default)]
723    pub(crate) features: Option<Vec<String>>,
724    /// Additional arguments to pass to cargo.
725    pub(crate) cargo_extra_args: Option<Vec<String>>,
726    /// The cargo package to build.
727    /// Required for "rust-binary" in multi-package workspaces.
728    #[serde(default)]
729    pub(crate) package: Option<String>,
730    /// Additional files to copy into the rock's "lua" directory, mapping source
731    /// paths to destinations.
732    #[serde(default, deserialize_with = "deserialize_map_or_seq")]
733    #[display_lua(convert_with = "display_include")]
734    #[cfg_attr(
735        feature = "schema",
736        schemars(with = "Option<std::collections::BTreeMap<String, String>>")
737    )]
738    pub(crate) include: Option<HashMap<LuaTableKey, PathBuf>>,
739    /// The parser language, e.g. "haskell". Required for "treesitter-parser".
740    #[serde(default)]
741    #[cfg_attr(
742        feature = "schema",
743        schemars(extend("x-lux-toml-example" = r#"
744[build]
745type = "treesitter-parser"
746lang = "haskell"
747parser = true
748generate = true
749location = "libs/tree-sitter-haskell"
750
751[build.queries."highlights.scm"] = """
752(signature
753  name: (variable) @function)
754"""
755"#))
756    )]
757    pub(crate) lang: Option<String>,
758    /// Whether to build the tree-sitter parser. Set to `false` for query-only packages.
759    /// Default: `false`.
760    #[serde(default)]
761    pub(crate) parser: Option<bool>,
762    /// Whether to generate the tree-sitter parser sources before building.
763    /// Default: `false`.
764    #[serde(default)]
765    pub(crate) generate: Option<bool>,
766    /// Location of the tree-sitter grammar, relative to the source root.
767    #[serde(default)]
768    pub(crate) location: Option<PathBuf>,
769    /// Embedded queries to install into "etc/queries", mapping file names to
770    /// contents.
771    #[serde(default)]
772    #[display_lua(convert_with = "display_path_string_map")]
773    #[cfg_attr(
774        feature = "schema",
775        schemars(with = "Option<std::collections::BTreeMap<String, String>>")
776    )]
777    pub(crate) queries: Option<HashMap<PathBuf, String>>,
778}
779
780impl PartialOverride for BuildSpecInternal {
781    type Err = ModuleSpecAmbiguousPlatformOverride;
782
783    fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
784        override_build_spec_internal(self, override_spec)
785    }
786}
787
788impl PlatformOverridable for BuildSpecInternal {
789    type Err = Infallible;
790
791    fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
792    where
793        T: PlatformOverridable,
794        T: Default,
795    {
796        Ok(PerPlatform::default())
797    }
798}
799
800fn override_build_spec_internal(
801    base: &BuildSpecInternal,
802    override_spec: &BuildSpecInternal,
803) -> Result<BuildSpecInternal, ModuleSpecAmbiguousPlatformOverride> {
804    Ok(BuildSpecInternal {
805        build_type: override_opt(&override_spec.build_type, &base.build_type),
806        builtin_spec: match (
807            override_spec.builtin_spec.clone(),
808            base.builtin_spec.clone(),
809        ) {
810            (Some(override_val), Some(base_spec_map)) => {
811                Some(base_spec_map.into_iter().chain(override_val).try_fold(
812                    HashMap::default(),
813                    |mut acc: HashMap<LuaTableKey, ModuleSpecInternal>,
814                     (k, module_spec_override)|
815                     -> Result<
816                        HashMap<LuaTableKey, ModuleSpecInternal>,
817                        ModuleSpecAmbiguousPlatformOverride,
818                    > {
819                        let overridden = match acc.get(&k) {
820                            None => module_spec_override,
821                            Some(base_module_spec) => {
822                                base_module_spec.apply_overrides(&module_spec_override)?
823                            }
824                        };
825                        acc.insert(k, overridden);
826                        Ok(acc)
827                    },
828                )?)
829            }
830            (override_val @ Some(_), _) => override_val,
831            (_, base_val @ Some(_)) => base_val,
832            _ => None,
833        },
834        makefile: override_opt(&override_spec.makefile, &base.makefile),
835        make_build_target: override_opt(&override_spec.make_build_target, &base.make_build_target),
836        build_pass: override_opt(&override_spec.build_pass, &base.build_pass),
837        make_install_target: override_opt(
838            &override_spec.make_install_target,
839            &base.make_install_target,
840        ),
841        install_pass: override_opt(&override_spec.install_pass, &base.install_pass),
842        make_build_variables: merge_map_opts(
843            &override_spec.make_build_variables,
844            &base.make_build_variables,
845        ),
846        make_install_variables: merge_map_opts(
847            &override_spec.make_install_variables,
848            &base.make_build_variables,
849        ),
850        variables: merge_map_opts(&override_spec.variables, &base.variables),
851        cmake_lists_content: override_opt(
852            &override_spec.cmake_lists_content,
853            &base.cmake_lists_content,
854        ),
855        build_command: override_opt(&override_spec.build_command, &base.build_command),
856        install_command: override_opt(&override_spec.install_command, &base.install_command),
857        install: override_opt(&override_spec.install, &base.install),
858        copy_directories: match (
859            override_spec.copy_directories.clone(),
860            base.copy_directories.clone(),
861        ) {
862            (Some(override_vec), Some(base_vec)) => {
863                let merged: Vec<PathBuf> =
864                    base_vec.into_iter().chain(override_vec).unique().collect();
865                Some(merged)
866            }
867            (None, base_vec @ Some(_)) => base_vec,
868            (override_vec @ Some(_), None) => override_vec,
869            _ => None,
870        },
871        patches: override_opt(&override_spec.patches, &base.patches),
872        target_path: override_opt(&override_spec.target_path, &base.target_path),
873        default_features: override_opt(&override_spec.default_features, &base.default_features),
874        features: override_opt(&override_spec.features, &base.features),
875        cargo_extra_args: override_opt(&override_spec.cargo_extra_args, &base.cargo_extra_args),
876        package: override_opt(&override_spec.package, &base.package),
877        include: merge_map_opts(&override_spec.include, &base.include),
878        lang: override_opt(&override_spec.lang, &base.lang),
879        parser: override_opt(&override_spec.parser, &base.parser),
880        generate: override_opt(&override_spec.generate, &base.generate),
881        location: override_opt(&override_spec.location, &base.location),
882        queries: merge_map_opts(&override_spec.queries, &base.queries),
883    })
884}
885
886fn override_opt<T: Clone>(override_opt: &Option<T>, base: &Option<T>) -> Option<T> {
887    match override_opt.clone() {
888        override_val @ Some(_) => override_val,
889        None => base.clone(),
890    }
891}
892
893fn merge_map_opts<K, V>(
894    override_map: &Option<HashMap<K, V>>,
895    base_map: &Option<HashMap<K, V>>,
896) -> Option<HashMap<K, V>>
897where
898    K: Clone,
899    K: Eq,
900    K: std::hash::Hash,
901    V: Clone,
902{
903    match (override_map.clone(), base_map.clone()) {
904        (Some(override_map), Some(base_map)) => {
905            Some(base_map.into_iter().chain(override_map).collect())
906        }
907        (_, base_map @ Some(_)) => base_map,
908        (override_map @ Some(_), _) => override_map,
909        _ => None,
910    }
911}
912
913/// Maps `build.type` to an enum.
914#[derive(Debug, PartialEq, Deserialize, Clone)]
915#[serde(rename_all = "lowercase", remote = "BuildType")]
916#[derive(Default)]
917pub(crate) enum BuildType {
918    /// "builtin" or "module"
919    #[default]
920    Builtin,
921    /// "make"
922    Make,
923    /// "cmake"
924    CMake,
925    /// "command"
926    Command,
927    /// "none"
928    None,
929    /// external Lua rock
930    LuaRock(String),
931    #[serde(rename = "rust-mlua")]
932    RustMlua,
933    #[serde(rename = "rust-binary")]
934    RustBinary,
935    #[serde(rename = "treesitter-parser")]
936    TreesitterParser,
937    Source,
938}
939
940impl BuildType {
941    pub(crate) fn luarocks_build_backend(&self) -> Option<LuaDependencySpec> {
942        match self {
943            &BuildType::Builtin
944            | &BuildType::Make
945            | &BuildType::CMake
946            | &BuildType::Command
947            | &BuildType::None
948            | &BuildType::Source => None,
949            BuildType::LuaRock(s) => Some(package::Name::new(format!("luarocks-build-{s}")).into()),
950            &BuildType::RustMlua => unsafe {
951                Some(
952                    package::Req::parse("luarocks-build-rust-mlua >= 0.2.6")
953                        .unwrap_unchecked()
954                        .into(),
955                )
956            },
957            &BuildType::RustBinary => unsafe {
958                Some(
959                    package::Req::parse("luarocks-build-rust-binary >= 4.0.0")
960                        .unwrap_unchecked()
961                        .into(),
962                )
963            },
964            &BuildType::TreesitterParser => {
965                Some(package::Name::new("luarocks-build-treesitter-parser >= 6.1.0".into()).into())
966            } // IMPORTANT: If adding another luarocks build backend,
967              // make sure to also add it to the filters in `operations::resolve::do_get_all_dependencies`.
968        }
969    }
970}
971
972// Special Deserialize case for BuildType:
973// Both "module" and "builtin" map to `Builtin`
974impl<'de> Deserialize<'de> for BuildType {
975    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
976    where
977        D: Deserializer<'de>,
978    {
979        let s = String::deserialize(deserializer)?;
980        if s == "builtin" || s == "module" {
981            Ok(Self::Builtin)
982        } else {
983            match Self::deserialize(s.clone().into_deserializer()) {
984                Err(_) => Ok(Self::LuaRock(s)),
985                ok => ok,
986            }
987        }
988    }
989}
990
991impl Display for BuildType {
992    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
993        match self {
994            BuildType::Builtin => write!(f, "builtin"),
995            BuildType::Make => write!(f, "make"),
996            BuildType::CMake => write!(f, "cmake"),
997            BuildType::Command => write!(f, "command"),
998            BuildType::None => write!(f, "none"),
999            BuildType::LuaRock(s) => write!(f, "{s}"),
1000            BuildType::RustMlua => write!(f, "rust-mlua"),
1001            BuildType::RustBinary => write!(f, "rust-binary"),
1002            BuildType::TreesitterParser => write!(f, "treesitter-parser"),
1003            BuildType::Source => write!(f, "source"),
1004        }
1005    }
1006}
1007
1008impl DisplayAsLuaValue for BuildType {
1009    fn display_lua_value(&self) -> DisplayLuaValue {
1010        DisplayLuaValue::String(self.to_string())
1011    }
1012}
1013
1014impl DisplayAsLuaValue for InstallSpec {
1015    fn display_lua_value(&self) -> DisplayLuaValue {
1016        self.display_lua().value
1017    }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022
1023    use super::*;
1024
1025    fn eval_lua_global<T: serde::de::DeserializeOwned>(code: &str, key: &'static str) -> T {
1026        use ottavino::{Closure, Executor, Fuel, Lua};
1027        use ottavino_util::serde::from_value;
1028        Lua::core()
1029            .try_enter(|ctx| {
1030                let closure = Closure::load(ctx, None, code.as_bytes())?;
1031                let executor = Executor::start(ctx, closure.into(), ());
1032                executor.step(ctx, &mut Fuel::with(i32::MAX))?;
1033                from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
1034            })
1035            .unwrap()
1036    }
1037
1038    #[tokio::test]
1039    pub async fn deserialize_build_type() {
1040        let build_type: BuildType = serde_json::from_str("\"builtin\"").unwrap();
1041        assert_eq!(build_type, BuildType::Builtin);
1042        let build_type: BuildType = serde_json::from_str("\"module\"").unwrap();
1043        assert_eq!(build_type, BuildType::Builtin);
1044        let build_type: BuildType = serde_json::from_str("\"make\"").unwrap();
1045        assert_eq!(build_type, BuildType::Make);
1046        let build_type: BuildType = serde_json::from_str("\"custom_build_backend\"").unwrap();
1047        assert_eq!(
1048            build_type,
1049            BuildType::LuaRock("custom_build_backend".into())
1050        );
1051        let build_type: BuildType = serde_json::from_str("\"rust-mlua\"").unwrap();
1052        assert_eq!(build_type, BuildType::RustMlua);
1053    }
1054
1055    #[test]
1056    pub fn install_spec_roundtrip() {
1057        let spec = InstallSpec {
1058            lua: HashMap::from([(
1059                "mymod".parse::<LuaModule>().unwrap(),
1060                "src/mymod.lua".into(),
1061            )]),
1062            lib: HashMap::from([("mylib".into(), "lib/mylib.so".into())]),
1063            conf: HashMap::from([("myconf".into(), "conf/myconf.cfg".into())]),
1064            bin: HashMap::from([("mybinary".into(), "bin/mybinary".into())]),
1065        };
1066        let lua = spec.display_lua().to_string();
1067        let restored: InstallSpec = eval_lua_global(&lua, "install");
1068        assert_eq!(spec, restored);
1069    }
1070
1071    #[test]
1072    pub fn install_spec_empty_roundtrip() {
1073        let spec = InstallSpec::default();
1074        let lua = spec.display_lua().to_string();
1075        let lua = if lua.trim().is_empty() {
1076            "install = {}".to_string()
1077        } else {
1078            lua
1079        };
1080        let restored: InstallSpec = eval_lua_global(&lua, "install");
1081        assert_eq!(spec, restored);
1082    }
1083
1084    #[test]
1085    pub fn build_spec_internal_builtin_roundtrip() {
1086        let spec = BuildSpecInternal {
1087            build_type: Some(BuildType::Builtin),
1088            builtin_spec: Some(HashMap::from([(
1089                LuaTableKey::StringKey("mymod".into()),
1090                ModuleSpecInternal::SourcePath("src/mymod.lua".into()),
1091            )])),
1092            install: Some(InstallSpec {
1093                lua: HashMap::from([(
1094                    "extra".parse::<LuaModule>().unwrap(),
1095                    "src/extra.lua".into(),
1096                )]),
1097                bin: HashMap::from([("mytool".into(), "bin/mytool".into())]),
1098                ..Default::default()
1099            }),
1100            copy_directories: Some(vec!["docs".into()]),
1101            ..Default::default()
1102        };
1103        let lua = spec.display_lua().to_string();
1104        let restored: BuildSpecInternal = eval_lua_global(&lua, "build");
1105        assert_eq!(spec, restored);
1106    }
1107
1108    #[test]
1109    pub fn build_spec_internal_make_roundtrip() {
1110        let spec = BuildSpecInternal {
1111            build_type: Some(BuildType::Make),
1112            makefile: Some("GNUmakefile".into()),
1113            make_build_target: Some("all".into()),
1114            make_install_target: Some("install".into()),
1115            make_build_variables: Some(HashMap::from([("CFLAGS".into(), "-O2".into())])),
1116            make_install_variables: Some(HashMap::from([("PREFIX".into(), "/usr/local".into())])),
1117            variables: Some(HashMap::from([("LUA_LIBDIR".into(), "/usr/lib".into())])),
1118            ..Default::default()
1119        };
1120        let lua = spec.display_lua().to_string();
1121        let restored: BuildSpecInternal = eval_lua_global(&lua, "build");
1122        assert_eq!(spec, restored);
1123    }
1124}