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)]
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 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 SourcePath(PathBuf),
128 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#[derive(Debug, PartialEq, Clone)]
280pub struct ModulePaths {
281 pub sources: Vec<PathBuf>,
283 pub libraries: Vec<PathBuf>,
285 pub defines: Vec<(String, Option<String>)>,
287 pub incdirs: Vec<PathBuf>,
289 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}