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