Skip to main content

lux_lib/lua_rockspec/build/
builtin.rs

1use crate::{
2    build::utils::c_dylib_extension,
3    lua_rockspec::{
4        deserialize_vec_from_lua_array_or_string, normalize_lua_value, DisplayAsLuaValue,
5        PartialOverride, PerPlatform, PlatformOverridable,
6    },
7};
8use itertools::Itertools;
9use miette::Diagnostic;
10use path_slash::PathBufExt;
11use serde::{de, Deserialize, Deserializer};
12use std::{collections::HashMap, convert::Infallible, fmt::Display, path::PathBuf, str::FromStr};
13use thiserror::Error;
14
15use super::{DisplayLuaKV, DisplayLuaValue};
16
17#[derive(Debug, PartialEq, Deserialize, Default, Clone)]
18pub struct BuiltinBuildSpec {
19    /// Keys are module names in the format normally used by the `require()` function
20    pub modules: HashMap<LuaModule, ModuleSpec>,
21}
22
23#[derive(Debug, PartialEq, Eq, Deserialize, Default, Clone, Hash)]
24pub struct LuaModule(String);
25
26impl LuaModule {
27    pub fn to_lua_path(&self) -> PathBuf {
28        self.to_file_path(".lua")
29    }
30
31    pub fn to_lua_init_path(&self) -> PathBuf {
32        self.to_path_buf().join("init.lua")
33    }
34
35    pub fn to_lib_path(&self) -> PathBuf {
36        self.to_file_path(&format!(".{}", c_dylib_extension()))
37    }
38
39    fn to_path_buf(&self) -> PathBuf {
40        PathBuf::from(self.0.replace('.', std::path::MAIN_SEPARATOR_STR))
41    }
42
43    pub(crate) fn to_file_path(&self, extension: &str) -> PathBuf {
44        PathBuf::from(self.0.replace('.', std::path::MAIN_SEPARATOR_STR) + extension)
45    }
46
47    pub fn from_pathbuf(path: PathBuf) -> Result<Self, ParseLuaModuleError> {
48        let extension = path
49            .extension()
50            .map(|ext| ext.to_string_lossy().to_string())
51            .unwrap_or("".into());
52        let module = path
53            .to_string_lossy()
54            .trim_end_matches(format!("init.{extension}").as_str())
55            .trim_end_matches(format!(".{extension}").as_str())
56            .trim_end_matches(std::path::MAIN_SEPARATOR_STR)
57            .replace(std::path::MAIN_SEPARATOR_STR, ".");
58        if module.is_empty() {
59            Err(ParseLuaModuleError::EmptyModule(
60                path.to_slash_lossy().to_string(),
61            ))
62        } else {
63            Ok(LuaModule(module))
64        }
65    }
66
67    pub fn join(&self, other: &LuaModule) -> LuaModule {
68        LuaModule(format!("{}.{}", self.0, other.0))
69    }
70
71    pub fn as_str(&self) -> &str {
72        self.0.as_str()
73    }
74}
75
76#[derive(Error, Debug, Diagnostic)]
77#[non_exhaustive]
78pub enum ParseLuaModuleError {
79    #[error("path '{0}' resulted in an empty Lua module.")]
80    EmptyModule(String),
81    #[error("cannot parse a Lua module from an empty string")]
82    EmptyString,
83}
84
85impl FromStr for LuaModule {
86    type Err = ParseLuaModuleError;
87
88    // NOTE: We may want to add some additional validations
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        if s.is_empty() {
91            Err(ParseLuaModuleError::EmptyString)
92        } else {
93            Ok(LuaModule(s.into()))
94        }
95    }
96}
97
98impl Display for LuaModule {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        self.0.fmt(f)
101    }
102}
103
104impl DisplayAsLuaValue for LuaModule {
105    fn display_lua_value(&self) -> DisplayLuaValue {
106        DisplayLuaValue::String(self.to_string())
107    }
108}
109
110impl DisplayAsLuaValue for HashMap<LuaModule, PathBuf> {
111    fn display_lua_value(&self) -> DisplayLuaValue {
112        use path_slash::PathBufExt as _;
113        DisplayLuaValue::Table(
114            self.iter()
115                .map(|(k, v)| DisplayLuaKV {
116                    key: k.to_string(),
117                    value: DisplayLuaValue::String(v.to_slash_lossy().into_owned()),
118                })
119                .collect_vec(),
120        )
121    }
122}
123
124#[derive(Debug, PartialEq, Clone)]
125pub enum ModuleSpec {
126    /// Pathnames of Lua files or C sources, for modules based on a single source file.
127    SourcePath(PathBuf),
128    /// Pathnames of C sources of a simple module written in C composed of multiple files.
129    SourcePaths(Vec<PathBuf>),
130    ModulePaths(ModulePaths),
131}
132
133impl ModuleSpec {
134    pub fn from_internal(
135        internal: ModuleSpecInternal,
136    ) -> Result<ModuleSpec, ModulePathsMissingSources> {
137        match internal {
138            ModuleSpecInternal::SourcePath(path) => Ok(ModuleSpec::SourcePath(path)),
139            ModuleSpecInternal::SourcePaths(paths) => Ok(ModuleSpec::SourcePaths(paths)),
140            ModuleSpecInternal::ModulePaths(module_paths) => Ok(ModuleSpec::ModulePaths(
141                ModulePaths::from_internal(module_paths)?,
142            )),
143        }
144    }
145}
146
147impl<'de> Deserialize<'de> for ModuleSpec {
148    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
149    where
150        D: Deserializer<'de>,
151    {
152        Self::from_internal(ModuleSpecInternal::deserialize(deserializer)?)
153            .map_err(de::Error::custom)
154    }
155}
156
157impl TryFrom<ModuleSpecInternal> for ModuleSpec {
158    type Error = ModulePathsMissingSources;
159
160    fn try_from(internal: ModuleSpecInternal) -> Result<Self, Self::Error> {
161        Self::from_internal(internal)
162    }
163}
164
165#[derive(Debug, PartialEq, Clone)]
166pub enum ModuleSpecInternal {
167    SourcePath(PathBuf),
168    SourcePaths(Vec<PathBuf>),
169    ModulePaths(ModulePathsInternal),
170}
171
172impl<'de> Deserialize<'de> for ModuleSpecInternal {
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: Deserializer<'de>,
176    {
177        let value = normalize_lua_value(serde_value::Value::deserialize(deserializer)?);
178        match value {
179            serde_value::Value::String(s) => Ok(Self::SourcePath(PathBuf::from(s))),
180            serde_value::Value::Seq(_) => {
181                let src_paths: Vec<PathBuf> =
182                    value.deserialize_into().map_err(de::Error::custom)?;
183                Ok(Self::SourcePaths(src_paths))
184            }
185            serde_value::Value::Map(_) => {
186                let module_paths: ModulePathsInternal =
187                    value.deserialize_into().map_err(de::Error::custom)?;
188                Ok(Self::ModulePaths(module_paths))
189            }
190            _ => Err(de::Error::custom(format!(
191                "expected a string, list, or table for module spec, got: {value:?}"
192            ))),
193        }
194    }
195}
196
197impl DisplayAsLuaValue for ModuleSpecInternal {
198    fn display_lua_value(&self) -> DisplayLuaValue {
199        match self {
200            ModuleSpecInternal::SourcePath(path) => {
201                DisplayLuaValue::String(path.to_string_lossy().into())
202            }
203            ModuleSpecInternal::SourcePaths(paths) => DisplayLuaValue::List(
204                paths
205                    .iter()
206                    .map(|p| DisplayLuaValue::String(p.to_string_lossy().into()))
207                    .collect(),
208            ),
209            ModuleSpecInternal::ModulePaths(module_paths) => module_paths.display_lua_value(),
210        }
211    }
212}
213
214fn deserialize_definitions<'de, D>(
215    deserializer: D,
216) -> Result<Vec<(String, Option<String>)>, D::Error>
217where
218    D: Deserializer<'de>,
219{
220    let values: Vec<String> = deserialize_vec_from_lua_array_or_string(deserializer)?;
221    values
222        .iter()
223        .map(|val| {
224            if let Some((key, value)) = val.split_once('=') {
225                Ok((key.into(), Some(value.into())))
226            } else {
227                Ok((val.into(), None))
228            }
229        })
230        .try_collect()
231}
232
233#[derive(Error, Debug, Diagnostic)]
234#[non_exhaustive]
235#[error("cannot resolve ambiguous platform override for `build.modules`.")]
236pub struct ModuleSpecAmbiguousPlatformOverride;
237
238impl PartialOverride for ModuleSpecInternal {
239    type Err = ModuleSpecAmbiguousPlatformOverride;
240
241    fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
242        match (override_spec, self) {
243            (ModuleSpecInternal::SourcePath(_), b @ ModuleSpecInternal::SourcePath(_)) => {
244                Ok(b.to_owned())
245            }
246            (ModuleSpecInternal::SourcePaths(_), b @ ModuleSpecInternal::SourcePaths(_)) => {
247                Ok(b.to_owned())
248            }
249            (ModuleSpecInternal::ModulePaths(a), ModuleSpecInternal::ModulePaths(b)) => Ok(
250                ModuleSpecInternal::ModulePaths(a.apply_overrides(b).unwrap()),
251            ),
252            _ => Err(ModuleSpecAmbiguousPlatformOverride),
253        }
254    }
255}
256
257#[derive(Error, Debug, Diagnostic)]
258#[non_exhaustive]
259#[error("could not resolve platform override for `build.modules`. THIS IS A BUG!")]
260pub struct BuildModulesPlatformOverride;
261
262impl PlatformOverridable for ModuleSpecInternal {
263    type Err = BuildModulesPlatformOverride;
264
265    fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
266    where
267        T: PlatformOverridable,
268    {
269        Err(BuildModulesPlatformOverride)
270    }
271}
272
273#[derive(Error, Debug, Diagnostic)]
274#[non_exhaustive]
275#[error("missing or empty field `sources`")]
276pub struct ModulePathsMissingSources;
277
278/// Specification for building a Lua module from various sources
279#[derive(Debug, PartialEq, Clone)]
280pub struct ModulePaths {
281    /// Path names of C sources, mandatory field
282    pub sources: Vec<PathBuf>,
283    /// External libraries to be linked
284    pub libraries: Vec<PathBuf>,
285    /// C defines, e.g. { "FOO=bar", "USE_BLA" }
286    pub defines: Vec<(String, Option<String>)>,
287    /// Directories to be added to the compiler's headers lookup directory list.
288    pub incdirs: Vec<PathBuf>,
289    /// Directories to be added to the linker's library lookup directory list.
290    pub libdirs: Vec<PathBuf>,
291}
292
293impl ModulePaths {
294    fn from_internal(
295        internal: ModulePathsInternal,
296    ) -> Result<ModulePaths, ModulePathsMissingSources> {
297        if internal.sources.is_empty() {
298            Err(ModulePathsMissingSources)
299        } else {
300            Ok(ModulePaths {
301                sources: internal.sources,
302                libraries: internal.libraries,
303                defines: internal.defines,
304                incdirs: internal.incdirs,
305                libdirs: internal.libdirs,
306            })
307        }
308    }
309}
310
311impl TryFrom<ModulePathsInternal> for ModulePaths {
312    type Error = ModulePathsMissingSources;
313
314    fn try_from(internal: ModulePathsInternal) -> Result<Self, Self::Error> {
315        Self::from_internal(internal)
316    }
317}
318
319impl<'de> Deserialize<'de> for ModulePaths {
320    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
321    where
322        D: Deserializer<'de>,
323    {
324        Self::from_internal(ModulePathsInternal::deserialize(deserializer)?)
325            .map_err(de::Error::custom)
326    }
327}
328
329#[derive(Debug, PartialEq, Deserialize, Clone, Default)]
330pub struct ModulePathsInternal {
331    #[serde(default, deserialize_with = "deserialize_vec_from_lua_array_or_string")]
332    pub sources: Vec<PathBuf>,
333    #[serde(default, deserialize_with = "deserialize_vec_from_lua_array_or_string")]
334    pub libraries: Vec<PathBuf>,
335    #[serde(default, deserialize_with = "deserialize_definitions")]
336    pub defines: Vec<(String, Option<String>)>,
337    #[serde(default, deserialize_with = "deserialize_vec_from_lua_array_or_string")]
338    pub incdirs: Vec<PathBuf>,
339    #[serde(default, deserialize_with = "deserialize_vec_from_lua_array_or_string")]
340    pub libdirs: Vec<PathBuf>,
341}
342
343impl DisplayAsLuaValue for ModulePathsInternal {
344    fn display_lua_value(&self) -> DisplayLuaValue {
345        DisplayLuaValue::Table(vec![
346            DisplayLuaKV {
347                key: "sources".into(),
348                value: DisplayLuaValue::List(
349                    self.sources
350                        .iter()
351                        .map(|s| DisplayLuaValue::String(s.to_string_lossy().into()))
352                        .collect(),
353                ),
354            },
355            DisplayLuaKV {
356                key: "libraries".into(),
357                value: DisplayLuaValue::List(
358                    self.libraries
359                        .iter()
360                        .map(|s| DisplayLuaValue::String(s.to_string_lossy().into()))
361                        .collect(),
362                ),
363            },
364            DisplayLuaKV {
365                key: "defines".into(),
366                value: DisplayLuaValue::List(
367                    self.defines
368                        .iter()
369                        .map(|(k, v)| {
370                            if let Some(v) = v {
371                                DisplayLuaValue::String(format!("{k}={v}"))
372                            } else {
373                                DisplayLuaValue::String(k.clone())
374                            }
375                        })
376                        .collect(),
377                ),
378            },
379            DisplayLuaKV {
380                key: "incdirs".into(),
381                value: DisplayLuaValue::List(
382                    self.incdirs
383                        .iter()
384                        .map(|s| DisplayLuaValue::String(s.to_string_lossy().into()))
385                        .collect(),
386                ),
387            },
388            DisplayLuaKV {
389                key: "libdirs".into(),
390                value: DisplayLuaValue::List(
391                    self.libdirs
392                        .iter()
393                        .map(|s| DisplayLuaValue::String(s.to_string_lossy().into()))
394                        .collect(),
395                ),
396            },
397        ])
398    }
399}
400
401impl PartialOverride for ModulePathsInternal {
402    type Err = Infallible;
403
404    fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
405        Ok(Self {
406            sources: override_vec(override_spec.sources.as_ref(), self.sources.as_ref()),
407            libraries: override_vec(override_spec.libraries.as_ref(), self.libraries.as_ref()),
408            defines: override_vec(override_spec.defines.as_ref(), self.defines.as_ref()),
409            incdirs: override_vec(override_spec.incdirs.as_ref(), self.incdirs.as_ref()),
410            libdirs: override_vec(override_spec.libdirs.as_ref(), self.libdirs.as_ref()),
411        })
412    }
413}
414
415impl PlatformOverridable for ModulePathsInternal {
416    type Err = Infallible;
417
418    fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
419    where
420        T: PlatformOverridable,
421        T: Default,
422    {
423        Ok(PerPlatform::default())
424    }
425}
426
427fn override_vec<T: Clone>(override_vec: &[T], base: &[T]) -> Vec<T> {
428    if override_vec.is_empty() {
429        return base.to_owned();
430    }
431    override_vec.to_owned()
432}
433
434#[cfg(test)]
435mod tests {
436    use ottavino::{Closure, Executor, Fuel, Lua};
437    use ottavino_util::serde::from_value;
438
439    use super::*;
440
441    fn exec_lua<T: serde::de::DeserializeOwned>(
442        code: &str,
443        key: &'static str,
444    ) -> Result<T, ottavino::ExternError> {
445        Lua::core().try_enter(|ctx| {
446            let closure = Closure::load(ctx, None, code.as_bytes())?;
447            let executor = Executor::start(ctx, closure.into(), ());
448            executor.step(ctx, &mut Fuel::with(i32::MAX))?;
449            from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
450        })
451    }
452
453    #[tokio::test]
454    pub async fn parse_lua_module_from_path() {
455        let lua_module = LuaModule::from_pathbuf("foo/init.lua".into()).unwrap();
456        assert_eq!(&lua_module.0, "foo");
457        let lua_module = LuaModule::from_pathbuf("foo/bar.lua".into()).unwrap();
458        assert_eq!(&lua_module.0, "foo.bar");
459        let lua_module = LuaModule::from_pathbuf("foo/bar/init.lua".into()).unwrap();
460        assert_eq!(&lua_module.0, "foo.bar");
461        let lua_module = LuaModule::from_pathbuf("foo/bar/baz.lua".into()).unwrap();
462        assert_eq!(&lua_module.0, "foo.bar.baz");
463    }
464
465    #[tokio::test]
466    pub async fn modules_spec_from_lua() {
467        let lua_content = "
468        build = {\n
469            modules = {\n
470                foo = 'lua/foo/init.lua',\n
471                bar = {\n
472                  'lua/bar.lua',\n
473                  'lua/bar/internal.lua',\n
474                },\n
475                baz = {\n
476                    sources = {\n
477                        'lua/baz.lua',\n
478                    },\n
479                    defines = { 'USE_BAZ' },\n
480                },\n
481                foo = 'lua/foo/init.lua',
482            },\n
483        }\n
484        ";
485        let build_spec: BuiltinBuildSpec = exec_lua(lua_content, "build").unwrap();
486        let foo = build_spec
487            .modules
488            .get(&LuaModule::from_str("foo").unwrap())
489            .unwrap();
490        assert_eq!(*foo, ModuleSpec::SourcePath("lua/foo/init.lua".into()));
491        let bar = build_spec
492            .modules
493            .get(&LuaModule::from_str("bar").unwrap())
494            .unwrap();
495        assert_eq!(
496            *bar,
497            ModuleSpec::SourcePaths(vec!["lua/bar.lua".into(), "lua/bar/internal.lua".into()])
498        );
499        let baz = build_spec
500            .modules
501            .get(&LuaModule::from_str("baz").unwrap())
502            .unwrap();
503        assert!(matches!(baz, ModuleSpec::ModulePaths { .. }));
504        let lua_content_no_sources = "
505        build = {\n
506            modules = {\n
507                baz = {\n
508                    defines = { 'USE_BAZ' },\n
509                },\n
510            },\n
511        }\n
512        ";
513        let result: Result<BuiltinBuildSpec, _> = exec_lua(lua_content_no_sources, "build");
514        let _err = result.unwrap_err();
515        let lua_content_complex_defines = "
516        build = {\n
517            modules = {\n
518                baz = {\n
519                    sources = {\n
520                        'lua/baz.lua',\n
521                    },\n
522                    defines = { 'USE_BAZ=1', 'ENABLE_LOGGING=true', 'LINK_STATIC' },\n
523                },\n
524            },\n
525        }\n
526        ";
527        let build_spec: BuiltinBuildSpec = exec_lua(lua_content_complex_defines, "build").unwrap();
528        let baz = build_spec
529            .modules
530            .get(&LuaModule::from_str("baz").unwrap())
531            .unwrap();
532        match baz {
533            ModuleSpec::ModulePaths(paths) => assert_eq!(
534                paths.defines,
535                vec![
536                    ("USE_BAZ".into(), Some("1".into())),
537                    ("ENABLE_LOGGING".into(), Some("true".into())),
538                    ("LINK_STATIC".into(), None)
539                ]
540            ),
541            _ => panic!(),
542        }
543    }
544}