Skip to main content

lux_lib/lua_rockspec/build/
mod.rs

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