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 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("path '{0}' resulted in an empty Lua module.")]
79 EmptyModule(String),
80 #[error("cannot parse a Lua module from an empty string")]
81 EmptyString,
82}
83
84impl FromStr for LuaModule {
85 type Err = ParseLuaModuleError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 if s.is_empty() {
90 Err(ParseLuaModuleError::EmptyString)
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 SourcePath(PathBuf),
127 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#[derive(Debug, PartialEq, Clone)]
276pub struct ModulePaths {
277 pub sources: Vec<PathBuf>,
279 pub libraries: Vec<PathBuf>,
281 pub defines: Vec<(String, Option<String>)>,
283 pub incdirs: Vec<PathBuf>,
285 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}