1mod build;
2mod dependency;
3mod deploy;
4mod partial;
5mod platform;
6mod rock_source;
7mod serde_util;
8mod test_spec;
9
10use std::{
11 collections::HashMap, convert::Infallible, fmt::Display, io, path::PathBuf, str::FromStr,
12};
13
14use ottavino::{Closure, Executor, Fuel};
15use ottavino_util::serde::from_value;
16use serde::{de::DeserializeOwned, Deserialize, Serialize};
17
18pub use build::*;
19pub use dependency::*;
20pub use deploy::*;
21use miette::{Diagnostic, NamedSource, SourceSpan};
22pub use partial::*;
23pub use platform::*;
24pub use rock_source::*;
25use ssri::Integrity;
26pub use test_spec::*;
27use thiserror::Error;
28use url::Url;
29
30pub(crate) use lux_macros::DisplayAsLuaKV;
31pub(crate) use serde_util::*;
32
33impl DisplayAsLuaValue for Url {
34 fn display_lua_value(&self) -> DisplayLuaValue {
35 DisplayLuaValue::String(self.to_string())
36 }
37}
38
39use crate::{
40 hash::HasIntegrity,
41 lua_version::{LuaVersion, LuaVersionUnset},
42 package::{PackageName, PackageSpec, PackageVersion, PackageVersionReq},
43 project::{project_toml::ProjectTomlError, ProjectRoot},
44 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
45 ROCKSPEC_FUEL_LIMIT,
46};
47
48#[derive(Error, Debug, Diagnostic)]
49#[non_exhaustive]
50pub enum LuaRockspecError {
51 #[error("manifest exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
52 #[diagnostic(
53 code(lux_lib::lua_rockspec::fuel_limit_exceeded),
54 help(
55 r#"the rockspec may be too complex or contain too many dependencies.
56try reducing the number of dependencies or simplifying build instructions.
57"#
58 )
59 )]
60 FuelLimitExceeded,
61 #[error("could not parse rockspec")]
62 #[diagnostic(code(lux_lib::lua_rockspec::execution),
63 help("check the rockspec or lux.toml for valid syntax and make sure it matches the specification."),
64 url("https://lux.lumen-labs.org/reference/lux-toml")
65 )]
66 ExecutionError {
67 cause: Box<String>,
68 #[source_code]
69 content: NamedSource<String>,
70 #[label("this package")]
71 pkg_span: SourceSpan,
72 #[label("{cause}")]
73 err_span: Option<SourceSpan>,
74 },
75 #[error("{}copy_directories cannot contain the rockspec name", ._0.as_ref().map(|p| format!("{p}: ")).unwrap_or_default())]
76 CopyDirectoriesContainRockspecName(Option<String>),
77 #[error("cannot create Lua rockspec with off-spec dependency")]
78 #[diagnostic(
79 code(lux_lib::lua_rockspec::off_spec_dependency),
80 url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
81 )]
82 OffSpecDependency(PackageName),
83 #[error("cannot create Lua rockspec with off-spec build dependency")]
84 #[diagnostic(
85 code(lux_lib::lua_rockspec::off_spec_build_dependency),
86 url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
87 )]
88 OffSpecBuildDependency(PackageName),
89 #[error("cannot create Lua rockspec with off-spec test dependency")]
90 #[diagnostic(
91 code(lux_lib::lua_rockspec::off_spec_test_dependency),
92 url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
93 )]
94 OffSpecTestDependency(PackageName),
95 #[error(transparent)]
96 #[diagnostic(transparent)]
97 ProjectToml(#[from] ProjectTomlError),
98}
99
100impl LuaRockspecError {
101 fn from_ottavino(cause: ottavino::ExternError, rockspec_content: &str) -> Self {
102 let msg = match &cause {
103 ottavino::ExternError::Runtime(rt) => rt.to_string(),
104 _ => cause.to_string(),
105 };
106 let field = msg
107 .strip_prefix("invalid rockspec field '")
108 .and_then(|rest| rest.split('\'').next())
109 .map(|f| f.to_owned());
110 let err_span = field.as_deref().and_then(|field| {
111 let span = find_key_span(rockspec_content, field);
112 if span.is_empty() {
113 None
114 } else {
115 Some(span)
116 }
117 });
118 LuaRockspecError::ExecutionError {
119 cause: Box::new(msg),
120 content: NamedSource::new("rockspec", rockspec_content.to_string()),
121 pkg_span: find_value_span(rockspec_content, "package"),
122 err_span,
123 }
124 }
125}
126
127#[derive(Error, Debug, Diagnostic)]
131#[non_exhaustive]
132enum ParseLuaRockspecFieldError {
133 #[error("invalid rockspec field '{field}': {source}")]
134 Deserialize {
135 field: String,
136 source: Box<ottavino_util::serde::de::Error>,
137 },
138 #[error(
139 r#"invalid rockspec field '{field}'.
140Failed to parse a table field: {source}"#
141 )]
142 DeserializeTable {
143 field: String,
144 source: Box<ottavino_util::serde::de::Error>,
145 },
146 #[error("invalid rockspec field '{field}'. expected a table, but got: {invalid_type}")]
147 NotATable { field: String, invalid_type: String },
148}
149
150#[derive(Clone, Debug)]
152#[cfg_attr(test, derive(PartialEq))]
153pub struct LocalLuaRockspec {
154 rockspec_format: Option<RockspecFormat>,
156 package: PackageName,
158 version: PackageVersion,
160 description: RockDescription,
161 supported_platforms: PlatformSupport,
162 lua: PackageVersionReq,
164 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
165 build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
166 external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
167 test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
168 build: PerPlatform<BuildSpec>,
169 source: PerPlatform<RemoteRockSource>,
170 test: PerPlatform<TestSpec>,
171 deploy: PerPlatform<DeploySpec>,
172 raw_content: String,
174}
175
176trait HasRockspecKey<'gc> {
177 fn get_rockspec_key<V: Deserialize<'gc>>(
178 &self,
179 ctx: ottavino::Context<'gc>,
180 key: &str,
181 ) -> Result<V, ParseLuaRockspecFieldError>;
182}
183
184impl<'gc> HasRockspecKey<'gc> for ottavino::Table<'gc> {
185 fn get_rockspec_key<V: Deserialize<'gc>>(
186 &self,
187 ctx: ottavino::Context<'gc>,
188 key: &str,
189 ) -> Result<V, ParseLuaRockspecFieldError> {
190 let value = self.get_value(ctx, key.to_string());
191 let is_table = matches!(&value, ottavino::Value::Table(_));
192 from_value(value).map_err(|source| {
193 if is_table {
194 ParseLuaRockspecFieldError::DeserializeTable {
195 field: key.into(),
196 source: Box::new(source),
197 }
198 } else {
199 ParseLuaRockspecFieldError::Deserialize {
200 field: key.into(),
201 source: Box::new(source),
202 }
203 }
204 })
205 }
206}
207
208impl LocalLuaRockspec {
209 pub(crate) fn from_project_toml(
212 project: &crate::project::project_toml::LocalProjectToml,
213 ) -> Self {
214 use crate::rockspec::Rockspec;
215
216 Self {
217 rockspec_format: project.format().clone(),
218 package: project.package().clone(),
219 version: project.version().clone(),
220 description: project.description().clone(),
221 supported_platforms: project.supported_platforms().clone(),
222 lua: project.lua().clone(),
223 dependencies: project.dependencies().clone(),
224 build_dependencies: project.build_dependencies().clone(),
225 external_dependencies: project.external_dependencies().clone(),
226 test_dependencies: project.test_dependencies().clone(),
227 build: project.build().clone(),
228 source: project.source().clone(),
229 test: project.test().clone(),
230 deploy: project.deploy().clone(),
231 raw_content: String::new(),
232 }
233 }
234
235 pub fn into_remote_with_source(
238 self,
239 source: PerPlatform<RemoteRockSource>,
240 ) -> RemoteLuaRockspec {
241 RemoteLuaRockspec {
242 local: self,
243 source,
244 }
245 }
246
247 pub fn new(
248 rockspec_content: &str,
249 project_root: ProjectRoot,
250 ) -> Result<Self, LuaRockspecError> {
251 let mut lua = ottavino::Lua::core();
252
253 let rockspec = lua
254 .try_enter(|ctx| {
255 let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
256
257 let executor = Executor::start(ctx, closure.into(), ());
258
259 let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
260
261 if !output {
262 return Ok(Err(LuaRockspecError::FuelLimitExceeded));
263 }
264
265 let globals = ctx.globals();
266
267 let dependencies: PerPlatform<Vec<LuaDependencySpec>> =
268 globals.get_rockspec_key(ctx, "dependencies")?;
269
270 let lua_version_req = dependencies
271 .current_platform()
272 .iter()
273 .find(|dep| dep.name().to_string() == "lua")
274 .cloned()
275 .map(|dep| dep.version_req().clone())
276 .unwrap_or(PackageVersionReq::Any);
277
278 fn strip_lua(
279 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
280 ) -> PerPlatform<Vec<LuaDependencySpec>> {
281 dependencies.map(|deps| {
282 deps.iter()
283 .filter(|dep| dep.name().to_string() != "lua")
284 .cloned()
285 .collect()
286 })
287 }
288
289 let build_dependencies: PerPlatform<Vec<LuaDependencySpec>> =
290 globals.get_rockspec_key(ctx, "build_dependencies")?;
291
292 let test_dependencies: PerPlatform<Vec<LuaDependencySpec>> =
293 globals.get_rockspec_key(ctx, "test_dependencies")?;
294
295 let source: PerPlatform<RemoteRockSource> = match globals.get_value(ctx, "source") {
296 ottavino::Value::Nil => {
297 PerPlatform::new(RockSourceSpec::File(project_root.to_path_buf()).into())
298 }
299 value => from_value(value).map_err(|source| {
300 ParseLuaRockspecFieldError::Deserialize {
301 field: "source".into(),
302 source: Box::new(source),
303 }
304 })?,
305 };
306
307 let rockspec = LocalLuaRockspec {
308 rockspec_format: globals.get_rockspec_key(ctx, "rockspec_format")?,
309 package: globals.get_rockspec_key(ctx, "package")?,
310 version: globals.get_rockspec_key(ctx, "version")?,
311 description: parse_lua_tbl_or_default(ctx, "description")?,
312 supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")?,
313 lua: lua_version_req,
314 dependencies: strip_lua(dependencies),
315 build_dependencies: strip_lua(build_dependencies),
316 test_dependencies: strip_lua(test_dependencies),
317 external_dependencies: globals
318 .get_rockspec_key(ctx, "external_dependencies")?,
319 build: globals.get_rockspec_key(ctx, "build")?,
320 test: globals.get_rockspec_key(ctx, "test")?,
321 deploy: globals.get_rockspec_key(ctx, "deploy")?,
322 raw_content: rockspec_content.into(),
323
324 source,
325 };
326
327 Ok(Ok(rockspec))
328 })
329 .map_err(|cause| LuaRockspecError::from_ottavino(cause, rockspec_content))??;
330
331 let rockspec_file_name = format!("{}-{}.rockspec", rockspec.package(), rockspec.version());
332
333 if rockspec
334 .build()
335 .default
336 .copy_directories
337 .contains(&PathBuf::from(&rockspec_file_name))
338 {
339 return Err(LuaRockspecError::CopyDirectoriesContainRockspecName(None));
340 }
341
342 for (platform, build_override) in &rockspec.build().per_platform {
343 if build_override
344 .copy_directories
345 .contains(&PathBuf::from(&rockspec_file_name))
346 {
347 return Err(LuaRockspecError::CopyDirectoriesContainRockspecName(Some(
348 platform.to_string(),
349 )));
350 }
351 }
352 Ok(rockspec)
353 }
354}
355
356impl Rockspec for LocalLuaRockspec {
357 type Error = Infallible;
358
359 fn package(&self) -> &PackageName {
360 &self.package
361 }
362
363 fn version(&self) -> &PackageVersion {
364 &self.version
365 }
366
367 fn description(&self) -> &RockDescription {
368 &self.description
369 }
370
371 fn supported_platforms(&self) -> &PlatformSupport {
372 &self.supported_platforms
373 }
374
375 fn lua(&self) -> &PackageVersionReq {
376 &self.lua
377 }
378
379 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
380 &self.dependencies
381 }
382
383 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
384 &self.build_dependencies
385 }
386
387 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
388 &self.external_dependencies
389 }
390
391 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
392 &self.test_dependencies
393 }
394
395 fn build(&self) -> &PerPlatform<BuildSpec> {
396 &self.build
397 }
398
399 fn test(&self) -> &PerPlatform<TestSpec> {
400 &self.test
401 }
402
403 fn source(&self) -> &PerPlatform<RemoteRockSource> {
404 &self.source
405 }
406
407 fn deploy(&self) -> &PerPlatform<DeploySpec> {
408 &self.deploy
409 }
410
411 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
412 &mut self.build
413 }
414
415 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
416 &mut self.test
417 }
418
419 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
420 &mut self.source
421 }
422
423 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
424 &mut self.deploy
425 }
426
427 fn format(&self) -> &Option<RockspecFormat> {
428 &self.rockspec_format
429 }
430
431 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
432 Ok(self.raw_content.clone())
433 }
434}
435
436impl HasIntegrity for LocalLuaRockspec {
437 async fn hash(&self) -> io::Result<Integrity> {
438 Ok(Integrity::from(&self.raw_content))
439 }
440}
441
442#[derive(Clone, Debug)]
444#[cfg_attr(test, derive(PartialEq))]
445pub struct RemoteLuaRockspec {
446 local: LocalLuaRockspec,
447 source: PerPlatform<RemoteRockSource>,
448}
449
450impl RemoteLuaRockspec {
451 pub fn new(rockspec_content: &str) -> Result<Self, LuaRockspecError> {
452 let mut lua = ottavino::Lua::core();
453
454 let source = lua
455 .try_enter(|ctx| {
456 let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
457
458 let executor = Executor::start(ctx, closure.into(), ());
459
460 let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
461
462 if !output {
463 return Ok(Err(LuaRockspecError::FuelLimitExceeded));
464 }
465
466 let globals = ctx.globals();
467
468 let source: PerPlatform<RemoteRockSource> =
469 globals.get_rockspec_key(ctx, "source")?;
470
471 Ok(Ok(source))
472 })
473 .map_err(|cause| LuaRockspecError::from_ottavino(cause, rockspec_content))??;
474
475 Ok(RemoteLuaRockspec {
476 local: LocalLuaRockspec::new(rockspec_content, ProjectRoot::new())?,
478 source,
479 })
480 }
481
482 pub fn from_package_and_source_spec(
483 package_spec: PackageSpec,
484 source_spec: RockSourceSpec,
485 ) -> Self {
486 let version = package_spec.version().clone();
487 let rockspec_format = RockspecFormat::default();
488 let raw_content = format!(
489 r#"
490rockspec_format = "{}"
491package = "{}"
492version = "{}"
493{}
494build = {{
495 type = "source"
496}}"#,
497 rockspec_format,
498 package_spec.name(),
499 version,
500 source_spec.display_lua(),
501 );
502
503 let source: RemoteRockSource = source_spec.into();
504
505 let local = LocalLuaRockspec {
506 rockspec_format: Some(rockspec_format),
507 package: package_spec.name().clone(),
508 version,
509 description: RockDescription::default(),
510 supported_platforms: PlatformSupport::default(),
511 lua: PackageVersionReq::Any,
512 dependencies: PerPlatform::default(),
513 build_dependencies: PerPlatform::default(),
514 external_dependencies: PerPlatform::default(),
515 test_dependencies: PerPlatform::default(),
516 build: PerPlatform::new(BuildSpec {
517 build_backend: Some(BuildBackendSpec::Source),
518 install: InstallSpec::default(),
519 copy_directories: Vec::new(),
520 patches: HashMap::new(),
521 }),
522 source: PerPlatform::new(source.clone()),
523 test: PerPlatform::default(),
524 deploy: PerPlatform::default(),
525 raw_content,
526 };
527 Self {
528 local,
529 source: PerPlatform::new(source),
530 }
531 }
532}
533
534impl Rockspec for RemoteLuaRockspec {
535 type Error = Infallible;
536
537 fn package(&self) -> &PackageName {
538 self.local.package()
539 }
540
541 fn version(&self) -> &PackageVersion {
542 self.local.version()
543 }
544
545 fn description(&self) -> &RockDescription {
546 self.local.description()
547 }
548
549 fn supported_platforms(&self) -> &PlatformSupport {
550 self.local.supported_platforms()
551 }
552
553 fn lua(&self) -> &PackageVersionReq {
554 self.local.lua()
555 }
556
557 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
558 self.local.dependencies()
559 }
560
561 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
562 match self.format() {
563 Some(RockspecFormat::_1_0 | RockspecFormat::_2_0)
566 if self
567 .build()
568 .current_platform()
569 .build_backend
570 .as_ref()
571 .is_some_and(|build_backend| build_backend.can_use_build_dependencies()) =>
572 {
573 self.local.dependencies()
574 }
575 _ => self.local.build_dependencies(),
576 }
577 }
578
579 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
580 self.local.external_dependencies()
581 }
582
583 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
584 self.local.test_dependencies()
585 }
586
587 fn build(&self) -> &PerPlatform<BuildSpec> {
588 self.local.build()
589 }
590
591 fn test(&self) -> &PerPlatform<TestSpec> {
592 self.local.test()
593 }
594
595 fn source(&self) -> &PerPlatform<RemoteRockSource> {
596 &self.source
597 }
598
599 fn deploy(&self) -> &PerPlatform<DeploySpec> {
600 self.local.deploy()
601 }
602
603 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
604 self.local.build_mut()
605 }
606
607 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
608 self.local.test_mut()
609 }
610
611 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
612 &mut self.source
613 }
614
615 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
616 self.local.deploy_mut()
617 }
618
619 fn format(&self) -> &Option<RockspecFormat> {
620 self.local.format()
621 }
622
623 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
624 Ok(self.local.raw_content.clone())
625 }
626}
627
628#[derive(Error, Debug, Diagnostic)]
629#[non_exhaustive]
630pub enum LuaVersionError {
631 #[error(
632 r#"
633The lua version {0} is not supported by {1} version {2}.
634
635HINT: If Lux has auto-detected an incompatible Lua installation,
636 use `--lua-version` to specify the Lua version to use.
637 Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit' and 'jit52'.
638"#
639 )]
640 LuaVersionUnsupported(LuaVersion, PackageName, PackageVersion),
641 #[error(transparent)]
642 #[diagnostic(transparent)]
643 LuaVersionUnset(#[from] LuaVersionUnset),
644}
645
646impl HasIntegrity for RemoteLuaRockspec {
647 async fn hash(&self) -> io::Result<Integrity> {
648 Ok(Integrity::from(&self.local.raw_content))
649 }
650}
651
652#[derive(Clone, Deserialize, Debug, PartialEq, Default, lux_macros::DisplayAsLuaKV)]
654#[display_lua(key = "description")]
655pub struct RockDescription {
656 pub summary: Option<String>,
658 pub detailed: Option<String>,
660 pub license: Option<String>,
662 #[serde(default, deserialize_with = "deserialize_url")]
664 pub homepage: Option<Url>,
665 #[serde(default, deserialize_with = "deserialize_url")]
667 pub issues_url: Option<Url>,
668 pub maintainer: Option<String>,
670 #[serde(default)]
672 pub labels: Vec<String>,
673}
674
675fn find_key_span(rockspec_content: &str, key: &str) -> SourceSpan {
676 rockspec_content
677 .match_indices(key)
678 .find(|&(start, _)| {
679 let end = start + key.len();
680 let prefix_ok =
681 start == 0 || !rockspec_content.as_bytes()[start - 1].is_ascii_alphanumeric();
682 let suffix_ok = rockspec_content
683 .get(end..)
684 .is_some_and(|s| s.starts_with('=') || s.trim_start().starts_with('='));
685 prefix_ok && suffix_ok
686 })
687 .map(|(start, _)| (start..start + key.len()).into())
688 .unwrap_or((0..0).into())
689}
690
691fn find_value_span(rockspec_content: &str, key: &str) -> SourceSpan {
692 let key = find_key_span(rockspec_content, key);
693 if key.is_empty() {
694 return key;
695 }
696 let key_offset = key.offset();
697 let key_end = key_offset + key.len();
698 let eq_start = key_end + rockspec_content[key_end..].find('=').unwrap_or(0);
699 let value_offset = eq_start + 1;
700 let line_end = rockspec_content[value_offset..]
701 .find('\n')
702 .map(|i| value_offset + i)
703 .unwrap_or(rockspec_content.len());
704 let trimmed = &rockspec_content[value_offset..line_end];
705 let start = value_offset + (trimmed.len() - trimmed.trim_start().len());
706 let end = line_end - (trimmed.len() - trimmed.trim_end().len());
707 (start..end).into()
708}
709
710fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
711where
712 D: serde::Deserializer<'de>,
713{
714 let s = Option::<String>::deserialize(deserializer)?;
715 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
716 .transpose()
717}
718
719#[derive(Error, Debug, Diagnostic)]
720#[non_exhaustive]
721#[error("invalid rockspec format")]
722pub struct InvalidRockspecFormat(String);
723
724#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
725pub enum RockspecFormat {
726 #[serde(rename = "1.0")]
727 _1_0,
728 #[serde(rename = "2.0")]
729 _2_0,
730 #[serde(rename = "3.0")]
731 #[default]
732 _3_0,
733}
734
735impl FromStr for RockspecFormat {
736 type Err = InvalidRockspecFormat;
737
738 fn from_str(s: &str) -> Result<Self, Self::Err> {
739 match s {
740 "1.0" => Ok(Self::_1_0),
741 "2.0" => Ok(Self::_2_0),
742 "3.0" => Ok(Self::_3_0),
743 txt => Err(InvalidRockspecFormat(txt.to_string())),
744 }
745 }
746}
747
748impl Display for RockspecFormat {
749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 match self {
751 Self::_1_0 => write!(f, "1.0"),
752 Self::_2_0 => write!(f, "2.0"),
753 Self::_3_0 => write!(f, "3.0"),
754 }
755 }
756}
757
758fn parse_lua_tbl_or_default<T>(
759 ctx: ottavino::Context<'_>,
760 field: &str,
761) -> Result<T, ParseLuaRockspecFieldError>
762where
763 T: Default,
764 T: DeserializeOwned,
765{
766 let ret = match ctx.globals().get_value(ctx, field.to_string()) {
767 ottavino::Value::Nil => T::default(),
768 value @ ottavino::Value::Table(_) => {
769 from_value(value).map_err(|source| ParseLuaRockspecFieldError::DeserializeTable {
770 field: field.to_string(),
771 source: Box::new(source),
772 })?
773 }
774 value => Err(ParseLuaRockspecFieldError::NotATable {
775 field: field.to_string(),
776 invalid_type: value.type_name().to_string(),
777 })?,
778 };
779 Ok(ret)
780}
781
782#[cfg(test)]
783mod tests {
784 use std::path::PathBuf;
785
786 use crate::git::GitSource;
787 use crate::lua_rockspec::PlatformIdentifier;
788 use crate::package::PackageSpec;
789
790 use super::*;
791
792 #[test]
793 pub fn parse_rockspec() {
794 let rockspec_content = "
795 rockspec_format = '1.0'\n
796 package = 'foo'\n
797 version = '1.0.0-1'\n
798 source = {\n
799 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
800 }\n
801 "
802 .to_string();
803 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
804 assert_eq!(rockspec.local.rockspec_format, Some(RockspecFormat::_1_0));
805 assert_eq!(rockspec.local.package, "foo".into());
806 assert_eq!(rockspec.local.version, "1.0.0-1".parse().unwrap());
807 assert_eq!(rockspec.local.description, RockDescription::default());
808
809 let rockspec_content = "
810 package = 'bar'\n
811 version = '2.0.0-1'\n
812 description = {}\n
813 source = {\n
814 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
815 }\n
816 "
817 .to_string();
818 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
819 assert_eq!(rockspec.local.rockspec_format, None);
820 assert_eq!(rockspec.local.package, "bar".into());
821 assert_eq!(rockspec.local.version, "2.0.0-1".parse().unwrap());
822 assert_eq!(rockspec.local.description, RockDescription::default());
823
824 let rockspec_content = "
825 package = 'rocks.nvim'\n
826 version = '3.0.0-1'\n
827 description = {\n
828 summary = 'some summary',
829 detailed = 'some detailed description',
830 license = 'MIT',
831 homepage = 'https://github.com/lumen-oss/rocks.nvim',
832 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
833 maintainer = 'Lumen Labs',
834 }\n
835 source = {\n
836 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
837 }\n
838 "
839 .to_string();
840 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
841 assert_eq!(rockspec.local.rockspec_format, None);
842 assert_eq!(rockspec.local.package, "rocks.nvim".into());
843 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
844 let expected_description = RockDescription {
845 summary: Some("some summary".into()),
846 detailed: Some("some detailed description".into()),
847 license: Some("MIT".into()),
848 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
849 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
850 maintainer: Some("Lumen Labs".into()),
851 labels: Vec::new(),
852 };
853 assert_eq!(rockspec.local.description, expected_description);
854
855 let rockspec_content = "
856 package = 'rocks.nvim'\n
857 version = '3.0.0-1'\n
858 description = {\n
859 summary = 'some summary',
860 detailed = 'some detailed description',
861 license = 'MIT',
862 homepage = 'https://github.com/lumen-oss/rocks.nvim',
863 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
864 maintainer = 'Lumen Labs',
865 labels = {},
866 }\n
867 external_dependencies = { FOO = { library = 'foo' } }\n
868 source = {\n
869 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
870 }\n
871 "
872 .to_string();
873 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
874 assert_eq!(rockspec.local.rockspec_format, None);
875 assert_eq!(rockspec.local.package, "rocks.nvim".into());
876 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
877 let expected_description = RockDescription {
878 summary: Some("some summary".into()),
879 detailed: Some("some detailed description".into()),
880 license: Some("MIT".into()),
881 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
882 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
883 maintainer: Some("Lumen Labs".into()),
884 labels: Vec::new(),
885 };
886 assert_eq!(rockspec.local.description, expected_description);
887 assert_eq!(
888 *rockspec
889 .local
890 .external_dependencies
891 .default
892 .get("FOO")
893 .unwrap(),
894 ExternalDependencySpec {
895 library: Some("foo".into()),
896 header: None
897 }
898 );
899
900 let rockspec_content = "
901 package = 'rocks.nvim'\n
902 version = '3.0.0-1'\n
903 description = {\n
904 summary = 'some summary',
905 detailed = 'some detailed description',
906 license = 'MIT',
907 homepage = 'https://github.com/lumen-oss/rocks.nvim',
908 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
909 maintainer = 'Lumen Labs',
910 labels = { 'package management', },
911 }\n
912 supported_platforms = { 'unix', '!windows' }\n
913 dependencies = { 'neorg ~> 6' }\n
914 build_dependencies = { 'foo' }\n
915 external_dependencies = { FOO = { header = 'foo.h' } }\n
916 test_dependencies = { 'busted >= 2.0.0' }\n
917 source = {\n
918 url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
919 hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
920 }\n
921 "
922 .to_string();
923 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
924 assert_eq!(rockspec.local.rockspec_format, None);
925 assert_eq!(rockspec.local.package, "rocks.nvim".into());
926 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
927 let expected_description = RockDescription {
928 summary: Some("some summary".into()),
929 detailed: Some("some detailed description".into()),
930 license: Some("MIT".into()),
931 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
932 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
933 maintainer: Some("Lumen Labs".into()),
934 labels: vec!["package management".into()],
935 };
936 assert_eq!(rockspec.local.description, expected_description);
937 assert!(rockspec
938 .local
939 .supported_platforms
940 .is_supported(&PlatformIdentifier::Unix));
941 assert!(!rockspec
942 .local
943 .supported_platforms
944 .is_supported(&PlatformIdentifier::Windows));
945 let neorg = PackageSpec::parse("neorg".into(), "6.0.0".into()).unwrap();
946 assert!(rockspec
947 .local
948 .dependencies
949 .default
950 .into_iter()
951 .any(|dep| dep.matches(&neorg)));
952 let foo = PackageSpec::parse("foo".into(), "1.0.0".into()).unwrap();
953 assert!(rockspec
954 .local
955 .build_dependencies
956 .default
957 .into_iter()
958 .any(|dep| dep.matches(&foo)));
959 let busted = PackageSpec::parse("busted".into(), "2.2.0".into()).unwrap();
960 assert_eq!(
961 *rockspec
962 .local
963 .external_dependencies
964 .default
965 .get("FOO")
966 .unwrap(),
967 ExternalDependencySpec {
968 header: Some("foo.h".into()),
969 library: None
970 }
971 );
972 assert!(rockspec
973 .local
974 .test_dependencies
975 .default
976 .into_iter()
977 .any(|dep| dep.matches(&busted)));
978
979 let rockspec_content = "
980 rockspec_format = '1.0'\n
981 package = 'foo'\n
982 version = '1.0.0-1'\n
983 source = {\n
984 url = 'git+https://hub.com/owner/example-project/',\n
985 branch = 'bar',\n
986 }\n
987 "
988 .to_string();
989 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
990 assert_eq!(
991 rockspec.local.source.default.source_spec,
992 RockSourceSpec::Git(GitSource {
993 url: "https://hub.com/owner/example-project/".parse().unwrap(),
994 git_ref: Some(GitRef::Branch("bar".into()))
995 })
996 );
997 assert_eq!(rockspec.local.test, PerPlatform::default());
998 let rockspec_content = "
999 rockspec_format = '1.0'\n
1000 package = 'foo'\n
1001 version = '1.0.0-1'\n
1002 source = {\n
1003 url = 'git+https://hub.com/owner/example-project/',\n
1004 tag = 'bar',\n
1005 }\n
1006 "
1007 .to_string();
1008 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1009 assert_eq!(
1010 rockspec.local.source.default.source_spec,
1011 RockSourceSpec::Git(GitSource {
1012 url: "https://hub.com/owner/example-project/".parse().unwrap(),
1013 git_ref: Some(GitRef::Tag("bar".into()))
1014 })
1015 );
1016 let rockspec_content = "
1017 rockspec_format = '1.0'\n
1018 package = 'foo'\n
1019 version = '1.0.0-1'\n
1020 source = {\n
1021 url = 'git+https://hub.com/owner/example-project/',\n
1022 branch = 'bar',\n
1023 tag = 'baz',\n
1024 }\n
1025 "
1026 .to_string();
1027 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1028 let rockspec_content = "
1029 rockspec_format = '1.0'\n
1030 package = 'foo'\n
1031 version = '1.0.0-1'\n
1032 source = {\n
1033 url = 'git+https://hub.com/owner/example-project/',\n
1034 tag = 'bar',\n
1035 file = 'foo.tar.gz',\n
1036 }\n
1037 build = {\n
1038 install = {\n
1039 conf = {['foo.bar'] = 'config/bar.toml'},\n
1040 },\n
1041 }\n
1042 "
1043 .to_string();
1044 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1045 assert_eq!(
1046 rockspec.local.source.default.archive_name,
1047 Some("foo.tar.gz".into())
1048 );
1049 let foo_bar_path = rockspec
1050 .local
1051 .build
1052 .default
1053 .install
1054 .conf
1055 .get("foo.bar")
1056 .unwrap();
1057 assert_eq!(*foo_bar_path, PathBuf::from("config/bar.toml"));
1058 let rockspec_content = "
1059 rockspec_format = '1.0'\n
1060 package = 'foo'\n
1061 version = '1.0.0-1'\n
1062 source = {\n
1063 url = 'git+https://hub.com/example-project/foo.zip',\n
1064 }\n
1065 build = {\n
1066 install = {\n
1067 lua = {\n
1068 'foo.lua',\n
1069 ['foo.bar'] = 'src/bar.lua',\n
1070 },\n
1071 bin = {['foo.bar'] = 'bin/bar'},\n
1072 },\n
1073 }\n
1074 "
1075 .to_string();
1076 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1077 assert!(matches!(
1078 rockspec.local.build.default.build_backend,
1079 Some(BuildBackendSpec::Builtin { .. })
1080 ));
1081 let install_lua_spec = rockspec.local.build.default.install.lua;
1082 let foo_bar_path = install_lua_spec
1083 .get(&LuaModule::from_str("foo.bar").unwrap())
1084 .unwrap();
1085 assert_eq!(*foo_bar_path, PathBuf::from("src/bar.lua"));
1086 let foo_path = install_lua_spec
1087 .get(&LuaModule::from_str("foo").unwrap())
1088 .unwrap();
1089 assert_eq!(*foo_path, PathBuf::from("foo.lua"));
1090 let foo_bar_path = rockspec
1091 .local
1092 .build
1093 .default
1094 .install
1095 .bin
1096 .get("foo.bar")
1097 .unwrap();
1098 assert_eq!(*foo_bar_path, PathBuf::from("bin/bar"));
1099 let rockspec_content = "
1100 rockspec_format = '1.0'\n
1101 package = 'foo'\n
1102 version = '1.0.0-1'\n
1103 source = {\n
1104 url = 'git+https://hub.com/example-project/',\n
1105 }\n
1106 build = {\n
1107 copy_directories = { 'lua' },\n
1108 }\n
1109 "
1110 .to_string();
1111 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1112 let rockspec_content = "
1113 rockspec_format = '1.0'\n
1114 package = 'foo'\n
1115 version = '1.0.0-1'\n
1116 source = {\n
1117 url = 'git+https://hub.com/example-project/',\n
1118 }\n
1119 build = {\n
1120 copy_directories = { 'lib' },\n
1121 }\n
1122 "
1123 .to_string();
1124 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1125 let rockspec_content = "
1126 rockspec_format = '1.0'\n
1127 package = 'foo'\n
1128 version = '1.0.0-1'\n
1129 source = {\n
1130 url = 'git+https://hub.com/example-project/',\n
1131 }\n
1132 build = {\n
1133 copy_directories = { 'rock_manifest' },\n
1134 }\n
1135 "
1136 .to_string();
1137 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1138 let rockspec_content = "
1139 rockspec_format = '1.0'\n
1140 package = 'foo'\n
1141 version = '1.0.0-1'\n
1142 source = {\n
1143 url = 'git+https://hub.com/example-project/foo.zip',\n
1144 dir = 'baz',\n
1145 }\n
1146 build = {\n
1147 type = 'make',\n
1148 install = {\n
1149 lib = {['foo.so'] = 'lib/bar.so'},\n
1150 },\n
1151 copy_directories = {\n
1152 'plugin',\n
1153 'ftplugin',\n
1154 },\n
1155 patches = {\n
1156 ['lua51-support.diff'] = [[\n
1157 --- before.c\n
1158 +++ path/to/after.c\n
1159 ]],\n
1160 },\n
1161 }\n
1162 "
1163 .to_string();
1164 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1165 assert_eq!(rockspec.local.source.default.unpack_dir, Some("baz".into()));
1166 assert_eq!(
1167 rockspec.local.build.default.build_backend,
1168 Some(BuildBackendSpec::Make(MakeBuildSpec::default()))
1169 );
1170 let foo_bar_path = rockspec
1171 .local
1172 .build
1173 .default
1174 .install
1175 .lib
1176 .get("foo.so")
1177 .unwrap();
1178 assert_eq!(*foo_bar_path, PathBuf::from("lib/bar.so"));
1179 let copy_directories = rockspec.local.build.default.copy_directories;
1180 assert_eq!(
1181 copy_directories,
1182 vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1183 );
1184 let patches = rockspec.local.build.default.patches;
1185 let _patch = patches.get(&PathBuf::from("lua51-support.diff")).unwrap();
1186 let rockspec_content = "
1187 rockspec_format = '1.0'\n
1188 package = 'foo'\n
1189 version = '1.0.0-1'\n
1190 source = {\n
1191 url = 'git+https://hub.com/example-project/foo.zip',\n
1192 }\n
1193 build = {\n
1194 type = 'cmake',\n
1195 }\n
1196 "
1197 .to_string();
1198 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1199 assert_eq!(
1200 rockspec.local.build.default.build_backend,
1201 Some(BuildBackendSpec::CMake(CMakeBuildSpec::default()))
1202 );
1203 let rockspec_content = "
1204 rockspec_format = '1.0'\n
1205 package = 'foo'\n
1206 version = '1.0.0-1'\n
1207 source = {\n
1208 url = 'git+https://hub.com/example-project/foo.zip',\n
1209 }\n
1210 build = {\n
1211 type = 'command',\n
1212 build_command = 'foo',\n
1213 install_command = 'bar',\n
1214 }\n
1215 "
1216 .to_string();
1217 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1218 assert!(matches!(
1219 rockspec.local.build.default.build_backend,
1220 Some(BuildBackendSpec::Command(CommandBuildSpec { .. }))
1221 ));
1222 let rockspec_content = "
1223 rockspec_format = '1.0'\n
1224 package = 'foo'\n
1225 version = '1.0.0-1'\n
1226 source = {\n
1227 url = 'git+https://hub.com/example-project/foo.zip',\n
1228 }\n
1229 build = {\n
1230 type = 'command',\n
1231 install_command = 'foo',\n
1232 }\n
1233 "
1234 .to_string();
1235 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1236 let rockspec_content = "
1237 rockspec_format = '1.0'\n
1238 package = 'foo'\n
1239 version = '1.0.0-1'\n
1240 source = {\n
1241 url = 'git+https://hub.com/example-project/foo.zip',\n
1242 }\n
1243 build = {\n
1244 type = 'command',\n
1245 build_command = 'foo',\n
1246 }\n
1247 "
1248 .to_string();
1249 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1250 let rockspec_content = "
1252 package = 'rocks'\n
1253 version = '3.0.0-1'\n
1254 dependencies = {\n
1255 'neorg ~> 6',\n
1256 'toml-edit ~> 1',\n
1257 platforms = {\n
1258 windows = {\n
1259 'neorg = 5.0.0',\n
1260 'toml = 1.0.0',\n
1261 },\n
1262 unix = {\n
1263 'neorg = 5.0.0',\n
1264 },\n
1265 linux = {\n
1266 'toml = 1.0.0',\n
1267 },\n
1268 },\n
1269 }\n
1270 source = {\n
1271 url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
1272 hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
1273 }\n
1274 "
1275 .to_string();
1276 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1277 let neorg_override = PackageSpec::parse("neorg".into(), "5.0.0".into()).unwrap();
1278 let toml_edit = PackageSpec::parse("toml-edit".into(), "1.0.0".into()).unwrap();
1279 let toml = PackageSpec::parse("toml".into(), "1.0.0".into()).unwrap();
1280 assert_eq!(rockspec.local.dependencies.default.len(), 2);
1281 let per_platform = &rockspec.local.dependencies.per_platform;
1282 assert_eq!(
1283 per_platform
1284 .get(&PlatformIdentifier::Windows)
1285 .unwrap()
1286 .iter()
1287 .filter(|dep| dep.matches(&neorg_override)
1288 || dep.matches(&toml_edit)
1289 || dep.matches(&toml))
1290 .count(),
1291 3
1292 );
1293 assert_eq!(
1294 per_platform
1295 .get(&PlatformIdentifier::Unix)
1296 .unwrap()
1297 .iter()
1298 .filter(|dep| dep.matches(&neorg_override)
1299 || dep.matches(&toml_edit)
1300 || dep.matches(&toml))
1301 .count(),
1302 2
1303 );
1304 assert_eq!(
1305 per_platform
1306 .get(&PlatformIdentifier::Linux)
1307 .unwrap()
1308 .iter()
1309 .filter(|dep| dep.matches(&neorg_override)
1310 || dep.matches(&toml_edit)
1311 || dep.matches(&toml))
1312 .count(),
1313 3
1314 );
1315 let rockspec_content = "
1316 package = 'rocks'\n
1317 version = '3.0.0-1'\n
1318 external_dependencies = {\n
1319 FOO = { library = 'foo' },\n
1320 platforms = {\n
1321 windows = {\n
1322 FOO = { library = 'foo.dll' },\n
1323 },\n
1324 unix = {\n
1325 BAR = { header = 'bar.h' },\n
1326 },\n
1327 linux = {\n
1328 FOO = { library = 'foo.so' },\n
1329 },\n
1330 },\n
1331 }\n
1332 source = {\n
1333 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1334 }\n
1335 "
1336 .to_string();
1337 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1338 assert_eq!(
1339 *rockspec
1340 .local
1341 .external_dependencies
1342 .default
1343 .get("FOO")
1344 .unwrap(),
1345 ExternalDependencySpec {
1346 library: Some("foo".into()),
1347 header: None
1348 }
1349 );
1350 let per_platform = rockspec.local.external_dependencies.per_platform;
1351 assert_eq!(
1352 *per_platform
1353 .get(&PlatformIdentifier::Windows)
1354 .and_then(|it| it.get("FOO"))
1355 .unwrap(),
1356 ExternalDependencySpec {
1357 library: Some("foo.dll".into()),
1358 header: None
1359 }
1360 );
1361 assert_eq!(
1362 *per_platform
1363 .get(&PlatformIdentifier::Unix)
1364 .and_then(|it| it.get("FOO"))
1365 .unwrap(),
1366 ExternalDependencySpec {
1367 library: Some("foo".into()),
1368 header: None
1369 }
1370 );
1371 assert_eq!(
1372 *per_platform
1373 .get(&PlatformIdentifier::Unix)
1374 .and_then(|it| it.get("BAR"))
1375 .unwrap(),
1376 ExternalDependencySpec {
1377 header: Some("bar.h".into()),
1378 library: None
1379 }
1380 );
1381 assert_eq!(
1382 *per_platform
1383 .get(&PlatformIdentifier::Linux)
1384 .and_then(|it| it.get("BAR"))
1385 .unwrap(),
1386 ExternalDependencySpec {
1387 header: Some("bar.h".into()),
1388 library: None
1389 }
1390 );
1391 assert_eq!(
1392 *per_platform
1393 .get(&PlatformIdentifier::Linux)
1394 .and_then(|it| it.get("FOO"))
1395 .unwrap(),
1396 ExternalDependencySpec {
1397 library: Some("foo.so".into()),
1398 header: None
1399 }
1400 );
1401 let rockspec_content = "
1402 rockspec_format = '1.0'\n
1403 package = 'foo'\n
1404 version = '1.0.0-1'\n
1405 source = {\n
1406 url = 'git+https://hub.com/example-project/.git',\n
1407 branch = 'bar',\n
1408 platforms = {\n
1409 macosx = {\n
1410 branch = 'mac',\n
1411 },\n
1412 windows = {\n
1413 url = 'git+https://winhub.com/example-project/.git',\n
1414 branch = 'win',\n
1415 },\n
1416 },\n
1417 }\n
1418 "
1419 .to_string();
1420 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1421 assert_eq!(
1422 rockspec.local.source.default.source_spec,
1423 RockSourceSpec::Git(GitSource {
1424 url: "https://hub.com/example-project/.git".parse().unwrap(),
1425 git_ref: Some(GitRef::Branch("bar".into()))
1426 })
1427 );
1428 assert_eq!(
1429 rockspec
1430 .source
1431 .per_platform
1432 .get(&PlatformIdentifier::MacOSX)
1433 .map(|it| it.source_spec.clone())
1434 .unwrap(),
1435 RockSourceSpec::Git(GitSource {
1436 url: "https://hub.com/example-project/.git".parse().unwrap(),
1437 git_ref: Some(GitRef::Branch("mac".into()))
1438 })
1439 );
1440 assert_eq!(
1441 rockspec
1442 .source
1443 .per_platform
1444 .get(&PlatformIdentifier::Windows)
1445 .map(|it| it.source_spec.clone())
1446 .unwrap(),
1447 RockSourceSpec::Git(GitSource {
1448 url: "https://winhub.com/example-project/.git".parse().unwrap(),
1449 git_ref: Some(GitRef::Branch("win".into()))
1450 })
1451 );
1452 let rockspec_content = "
1453 rockspec_format = '1.0'\n
1454 package = 'foo'\n
1455 version = '1.0.0-1'\n
1456 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1457 build = {\n
1458 type = 'make',\n
1459 install = {\n
1460 lib = {['foo.bar'] = 'lib/bar.so'},\n
1461 },\n
1462 copy_directories = { 'plugin' },\n
1463 platforms = {\n
1464 unix = {\n
1465 copy_directories = { 'ftplugin' },\n
1466 },\n
1467 linux = {\n
1468 copy_directories = { 'foo' },\n
1469 },\n
1470 },\n
1471 }\n
1472 "
1473 .to_string();
1474 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1475 let per_platform = rockspec.local.build.per_platform;
1476 let unix = per_platform.get(&PlatformIdentifier::Unix).unwrap();
1477 assert_eq!(
1478 unix.copy_directories,
1479 vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1480 );
1481 let linux = per_platform.get(&PlatformIdentifier::Linux).unwrap();
1482 assert_eq!(
1483 linux.copy_directories,
1484 vec![
1485 PathBuf::from("plugin"),
1486 PathBuf::from("foo"),
1487 PathBuf::from("ftplugin")
1488 ]
1489 );
1490 let rockspec_content = "
1491 package = 'foo'\n
1492 version = '1.0.0-1'\n
1493 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1494 build = {\n
1495 type = 'builtin',\n
1496 modules = {\n
1497 cjson = {\n
1498 sources = { 'lua_cjson.c', 'strbuf.c', 'fpconv.c' },\n
1499 }\n
1500 },\n
1501 platforms = {\n
1502 win32 = { modules = { cjson = { defines = {\n
1503 'DISABLE_INVALID_NUMBERS', 'USE_INTERNAL_ISINF'\n
1504 } } } }\n
1505 },\n
1506 }\n
1507 "
1508 .to_string();
1509 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1510 let win32 = rockspec.local.build.get(&PlatformIdentifier::Windows);
1511 assert_eq!(
1512 win32.build_backend,
1513 Some(BuildBackendSpec::Builtin(BuiltinBuildSpec {
1514 modules: vec![(
1515 LuaModule::from_str("cjson").unwrap(),
1516 ModuleSpec::ModulePaths(ModulePaths {
1517 sources: vec!["lua_cjson.c".into(), "strbuf.c".into(), "fpconv.c".into()],
1518 libraries: Vec::default(),
1519 defines: vec![
1520 ("DISABLE_INVALID_NUMBERS".into(), None),
1521 ("USE_INTERNAL_ISINF".into(), None)
1522 ],
1523 incdirs: Vec::default(),
1524 libdirs: Vec::default(),
1525 })
1526 )]
1527 .into_iter()
1528 .collect()
1529 }))
1530 );
1531 let rockspec_content = "
1532 rockspec_format = '1.0'\n
1533 package = 'foo'\n
1534 version = '1.0.0-1'\n
1535 deploy = {\n
1536 wrap_bin_scripts = false,\n
1537 }\n
1538 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1539 ";
1540 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1541 let deploy_spec = &rockspec.deploy().current_platform();
1542 assert!(!deploy_spec.wrap_bin_scripts);
1543 }
1544
1545 #[test]
1546 pub fn parse_scm_rockspec() {
1547 let rockspec_content = "
1548 package = 'foo'\n
1549 version = 'scm-1'\n
1550 source = {\n
1551 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1552 }\n
1553 "
1554 .to_string();
1555 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1556 assert_eq!(rockspec.local.package, "foo".into());
1557 assert_eq!(rockspec.local.version, "scm-1".parse().unwrap());
1558 }
1559
1560 #[test]
1561 pub fn regression_luasystem() {
1562 let rockspec_content = String::from_utf8(
1563 std::fs::read(
1564 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1565 .join("resources/test/luasystem-0.4.4-1.rockspec"),
1566 )
1567 .unwrap(),
1568 )
1569 .unwrap();
1570 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1571 let build_spec = rockspec.local.build.current_platform();
1572 assert!(matches!(
1573 build_spec.build_backend,
1574 Some(BuildBackendSpec::Builtin { .. })
1575 ));
1576 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) =
1577 &build_spec.build_backend
1578 {
1579 assert_eq!(
1580 modules.get(&LuaModule::from_str("system.init").unwrap()),
1581 Some(&ModuleSpec::SourcePath("system/init.lua".into()))
1582 );
1583 assert_eq!(
1584 modules.get(&LuaModule::from_str("system.core").unwrap()),
1585 Some(&ModuleSpec::ModulePaths(ModulePaths {
1586 sources: vec![
1587 "src/core.c".into(),
1588 "src/compat.c".into(),
1589 "src/time.c".into(),
1590 "src/environment.c".into(),
1591 "src/random.c".into(),
1592 "src/term.c".into(),
1593 "src/bitflags.c".into(),
1594 "src/wcwidth.c".into(),
1595 ],
1596 defines: luasystem_expected_defines(),
1597 libraries: luasystem_expected_libraries(),
1598 incdirs: luasystem_expected_incdirs(),
1599 libdirs: luasystem_expected_libdirs(),
1600 }))
1601 );
1602 }
1603 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1604 .local
1605 .build
1606 .get(&PlatformIdentifier::Windows)
1607 .build_backend
1608 {
1609 if let ModuleSpec::ModulePaths(paths) = modules
1610 .get(&LuaModule::from_str("system.core").unwrap())
1611 .unwrap()
1612 {
1613 assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1614 };
1615 }
1616 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1617 .local
1618 .build
1619 .get(&PlatformIdentifier::Win32)
1620 .build_backend
1621 {
1622 if let ModuleSpec::ModulePaths(paths) = modules
1623 .get(&LuaModule::from_str("system.core").unwrap())
1624 .unwrap()
1625 {
1626 assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1627 };
1628 }
1629 }
1630
1631 fn luasystem_expected_defines() -> Vec<(String, Option<String>)> {
1632 if cfg!(target_os = "windows") {
1633 vec![
1634 ("WINVER".into(), Some("0x0600".into())),
1635 ("_WIN32_WINNT".into(), Some("0x0600".into())),
1636 ]
1637 } else {
1638 Vec::default()
1639 }
1640 }
1641
1642 fn luasystem_expected_windows_libraries() -> Vec<PathBuf> {
1643 vec!["advapi32".into(), "winmm".into()]
1644 }
1645 fn luasystem_expected_libraries() -> Vec<PathBuf> {
1646 if cfg!(any(target_os = "linux", target_os = "android")) {
1647 vec!["rt".into()]
1648 } else if cfg!(target_os = "windows") {
1649 luasystem_expected_windows_libraries()
1650 } else {
1651 Vec::default()
1652 }
1653 }
1654
1655 fn luasystem_expected_incdirs() -> Vec<PathBuf> {
1656 Vec::default()
1657 }
1658
1659 fn luasystem_expected_libdirs() -> Vec<PathBuf> {
1660 Vec::default()
1661 }
1662
1663 #[test]
1664 pub fn rust_mlua_rockspec() {
1665 let rockspec_content = "
1666 package = 'foo'\n
1667 version = 'scm-1'\n
1668 source = {\n
1669 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1670 }\n
1671 build = {
1672 type = 'rust-mlua',
1673 modules = {
1674 'foo',
1675 bar = 'baz',
1676 },
1677 target_path = 'path/to/cargo/target/directory',
1678 default_features = false,
1679 include = {
1680 'file.lua',
1681 ['path/to/another/file.lua'] = 'another-file.lua',
1682 },
1683 features = {'extra', 'features'},
1684 }
1685 ";
1686 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1687 let build_spec = rockspec.local.build.current_platform();
1688 if let Some(BuildBackendSpec::RustMlua(build_spec)) = build_spec.build_backend.to_owned() {
1689 assert_eq!(
1690 build_spec.modules.get("foo").unwrap(),
1691 &PathBuf::from(format!("libfoo.{}", std::env::consts::DLL_EXTENSION))
1692 );
1693 assert_eq!(
1694 build_spec.modules.get("bar").unwrap(),
1695 &PathBuf::from(format!("libbaz.{}", std::env::consts::DLL_EXTENSION))
1696 );
1697 assert_eq!(
1698 build_spec.include.get(&PathBuf::from("file.lua")).unwrap(),
1699 &PathBuf::from("file.lua")
1700 );
1701 assert_eq!(
1702 build_spec
1703 .include
1704 .get(&PathBuf::from("path/to/another/file.lua"))
1705 .unwrap(),
1706 &PathBuf::from("another-file.lua")
1707 );
1708 } else {
1709 panic!("Expected RustMlua build backend");
1710 }
1711 }
1712
1713 #[test]
1714 pub fn rust_binary_rockspec() {
1715 let rockspec_content = "
1716 package = 'foo'\n
1717 version = 'scm-1'\n
1718 source = {\n
1719 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1720 }\n
1721 build = {\n
1722 type = 'rust-binary',\n
1723 package = 'foo',\n
1724 features = {'extra', 'features'},\n
1725 }\n
1726 ";
1727 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1728 let build_spec = rockspec.local.build.current_platform();
1729 if let Some(BuildBackendSpec::RustBinary(build_spec)) = build_spec.build_backend.to_owned()
1730 {
1731 assert_eq!(build_spec.package, Some("foo".to_string()));
1732 assert_eq!(build_spec.features, vec!["extra", "features"]);
1733 } else {
1734 panic!("Expected RustBinary build backend");
1735 }
1736 }
1737
1738 #[tokio::test]
1739 pub async fn regression_ltui() {
1740 let content = String::from_utf8(
1741 std::fs::read(
1742 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1743 .join("resources/test/ltui-2.8-2.rockspec"),
1744 )
1745 .unwrap(),
1746 )
1747 .unwrap();
1748 RemoteLuaRockspec::new(&content).unwrap();
1749 }
1750
1751 #[test]
1754 pub fn regression_off_spec_install_binaries() {
1755 let rockspec_content = r#"
1756 package = "WSAPI"
1757 version = "1.7-1"
1758
1759 source = {
1760 url = "git://github.com/keplerproject/wsapi",
1761 tag = "v1.7",
1762 }
1763
1764 build = {
1765 type = "builtin",
1766 modules = {
1767 ["wsapi"] = "src/wsapi.lua",
1768 },
1769 -- Offending Line
1770 install = { bin = { "src/launcher/wsapi.cgi" } }
1771 }
1772 "#;
1773
1774 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1775
1776 assert_eq!(
1777 rockspec.build().current_platform().install.bin,
1778 HashMap::from([("wsapi.cgi".into(), PathBuf::from("src/launcher/wsapi.cgi"))])
1779 );
1780 }
1781
1782 #[test]
1783 pub fn regression_external_dependencies() {
1784 let content = String::from_utf8(
1785 std::fs::read(
1786 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1787 .join("resources/test/luaossl-20220711-0.rockspec"),
1788 )
1789 .unwrap(),
1790 )
1791 .unwrap();
1792 let rockspec = RemoteLuaRockspec::new(&content).unwrap();
1793 if cfg!(target_family = "unix") {
1794 assert_eq!(
1795 rockspec
1796 .local
1797 .external_dependencies
1798 .current_platform()
1799 .get("OPENSSL")
1800 .unwrap(),
1801 &ExternalDependencySpec {
1802 library: Some("ssl".into()),
1803 header: Some("openssl/ssl.h".into()),
1804 }
1805 );
1806 }
1807 let per_platform = rockspec.local.external_dependencies.per_platform;
1808 assert_eq!(
1809 *per_platform
1810 .get(&PlatformIdentifier::Windows)
1811 .and_then(|it| it.get("OPENSSL"))
1812 .unwrap(),
1813 ExternalDependencySpec {
1814 library: Some("libeay32".into()),
1815 header: Some("openssl/ssl.h".into()),
1816 }
1817 );
1818 }
1819
1820 #[test]
1821 pub fn remote_lua_rockspec_from_package_and_source_spec() {
1822 let package_req = "foo@1.0.5".parse().unwrap();
1823 let source = GitSource {
1824 url: "https://hub.com/owner/example-project.git".parse().unwrap(),
1825 git_ref: Some(GitRef::Tag("1.0.5".into())),
1826 };
1827 let source_spec = RockSourceSpec::Git(source);
1828 let rockspec =
1829 RemoteLuaRockspec::from_package_and_source_spec(package_req, source_spec.clone());
1830 let generated_rockspec_str = rockspec.local.raw_content;
1831 let rockspec2 = RemoteLuaRockspec::new(&generated_rockspec_str).unwrap();
1832 assert_eq!(rockspec2.local.package, "foo".into());
1833 assert_eq!(rockspec2.local.version, "1.0.5".parse().unwrap());
1834 assert_eq!(rockspec2.local.source, PerPlatform::new(source_spec.into()));
1835 }
1836
1837 #[test]
1838 pub fn regression_complex_source_field() {
1839 let rockspec_content = r#"
1840 package = "say"
1841 local rock_version = "1.4.1"
1842 local rock_release = "3"
1843 local namespace = "lunarmodules"
1844 local repository = package
1845
1846 version = ("%s-%s"):format(rock_version, rock_release)
1847
1848 source = {
1849 url = ("git+https://github.com/%s/%s.git"):format(namespace, repository),
1850 branch = rock_version == "scm" and "master" or nil,
1851 tag = rock_version ~= "scm" and "v"..rock_version or nil,
1852 }
1853
1854 description = {
1855 summary = "Lua string hashing/indexing library",
1856 }
1857
1858 dependencies = {
1859 "lua >= 5.1",
1860 }
1861
1862 build = {
1863 type = "builtin",
1864 }
1865 "#
1866 .to_string();
1867 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1868 }
1869
1870 fn eval_lua_global<T: serde::de::DeserializeOwned>(code: &str, key: &'static str) -> T {
1871 use ottavino::{Closure, Executor, Fuel, Lua};
1872 use ottavino_util::serde::from_value;
1873 Lua::core()
1874 .try_enter(|ctx| {
1875 let closure = Closure::load(ctx, None, code.as_bytes())?;
1876 let executor = Executor::start(ctx, closure.into(), ());
1877 executor.step(ctx, &mut Fuel::with(i32::MAX))?;
1878 from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
1879 })
1880 .unwrap()
1881 }
1882
1883 #[test]
1884 pub fn rock_description_roundtrip() {
1885 let desc = RockDescription {
1886 summary: Some("A great package".into()),
1887 detailed: Some("Detailed description here".into()),
1888 license: Some("MIT".into()),
1889 homepage: Some("https://example.com".parse().unwrap()),
1890 issues_url: Some("https://example.com/issues".parse().unwrap()),
1891 maintainer: Some("Some Maintainer <maintainer@example.com>".into()),
1892 labels: vec!["neovim".into(), "lua".into()],
1893 };
1894 let lua = desc.display_lua().to_string();
1895 let restored: RockDescription = eval_lua_global(&lua, "description");
1896 assert_eq!(desc, restored);
1897 }
1898
1899 #[test]
1900 pub fn rock_description_empty_roundtrip() {
1901 let desc = RockDescription::default();
1902 let lua = desc.display_lua().to_string();
1903 let lua = if lua.trim().is_empty() {
1905 "description = {}".to_string()
1906 } else {
1907 lua
1908 };
1909 let restored: RockDescription = eval_lua_global(&lua, "description");
1910 assert_eq!(desc, restored);
1911 }
1912
1913 #[test]
1914 pub fn regression_luasec() {
1915 let rockspec_content = r#"
1916package = "LuaSec"
1917version = "1.3.2-1"
1918source = {
1919 url = "git+https://github.com/brunoos/luasec",
1920 tag = "v1.3.2",
1921}
1922external_dependencies = {
1923 platforms = {
1924 unix = {
1925 OPENSSL = {
1926 header = "openssl/ssl.h",
1927 library = "ssl"
1928 }
1929 },
1930 windows = {
1931 OPENSSL = {
1932 header = "openssl/ssl.h",
1933 }
1934 },
1935 }
1936}
1937"#;
1938 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1939 let linux_external_deps = rockspec
1940 .external_dependencies()
1941 .get(&PlatformIdentifier::Linux);
1942 assert!(linux_external_deps.get("OPENSSL").is_some());
1943 let windows_external_deps = rockspec
1944 .external_dependencies()
1945 .get(&PlatformIdentifier::Windows);
1946 assert!(windows_external_deps.get("OPENSSL").is_some());
1947 }
1948
1949 #[test]
1950 pub fn find_value_span_spaces() {
1951 let source = "package = 'foo'\nversion = '1.0'";
1952 let span = find_value_span(source, "package");
1953 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1954 }
1955
1956 #[test]
1957 pub fn find_value_span_no_spaces() {
1958 let source = "package='foo'";
1959 let span = find_value_span(source, "package");
1960 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1961 }
1962
1963 #[test]
1964 pub fn find_value_span_returns_empty_when_not_found() {
1965 let source = "version = '1.0'";
1966 let span = find_value_span(source, "package");
1967 assert_eq!(span.len(), 0);
1968 }
1969
1970 #[test]
1971 pub fn find_value_span_skips_url_containing_field() {
1972 let source = "homepage = 'https://luapackager.com'\npackage = 'foo'";
1973 let span = find_value_span(source, "package");
1974 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1975 }
1976
1977 #[test]
1978 pub fn find_value_span_double_quoted() {
1979 let source = r#"package = "foo""#;
1980 let span = find_value_span(source, "package");
1981 assert_eq!(
1982 &source[span.offset()..span.offset() + span.len()],
1983 r#""foo""#
1984 );
1985 }
1986
1987 #[test]
1988 pub fn find_value_span_table() {
1989 let source = "description = {\n summary = 'bar'\n}";
1990 let span = find_value_span(source, "description");
1991 assert_eq!(&source[span.offset()..span.offset() + span.len()], "{");
1992 }
1993
1994 #[test]
1995 pub fn parse_lua_rockspec_field_error() {
1996 let rockspec_content = "package = 'my-lua-lib'\nfoo = 123";
1997 let extern_err = ottavino::Lua::core()
1998 .try_enter(|_ctx| {
1999 Err(ParseLuaRockspecFieldError::Deserialize {
2000 field: "foo".into(),
2001 source: Box::new(ottavino_util::serde::de::Error::Message(
2002 "expected bar".into(),
2003 )),
2004 })?;
2005 Ok(())
2006 })
2007 .unwrap_err();
2008 let err = LuaRockspecError::from_ottavino(extern_err, rockspec_content);
2009 match err {
2010 LuaRockspecError::ExecutionError {
2011 cause,
2012 err_span: Some(span),
2013 ..
2014 } => {
2015 assert!(cause.contains("invalid rockspec field 'foo'"));
2016 assert_eq!(
2017 &rockspec_content[span.offset()..span.offset() + span.len()],
2018 "foo"
2019 );
2020 }
2021 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2022 }
2023 let extern_err = ottavino::Lua::core()
2024 .try_enter(|_ctx| {
2025 Err(ParseLuaRockspecFieldError::DeserializeTable {
2026 field: "foo".into(),
2027 source: Box::new(ottavino_util::serde::de::Error::Message(
2028 "expected bar".into(),
2029 )),
2030 })?;
2031 Ok(())
2032 })
2033 .unwrap_err();
2034 let err = LuaRockspecError::from_ottavino(extern_err, rockspec_content);
2035 match err {
2036 LuaRockspecError::ExecutionError {
2037 cause,
2038 err_span: Some(span),
2039 ..
2040 } => {
2041 assert!(cause.contains("invalid rockspec field 'foo'"));
2042 assert_eq!(
2043 &rockspec_content[span.offset()..span.offset() + span.len()],
2044 "foo"
2045 );
2046 }
2047 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2048 }
2049 let extern_err = ottavino::Lua::core()
2050 .try_enter(|_ctx| {
2051 Err(ParseLuaRockspecFieldError::NotATable {
2052 field: "foo".into(),
2053 invalid_type: "integer".into(),
2054 })?;
2055 Ok(())
2056 })
2057 .unwrap_err();
2058 let err = LuaRockspecError::from_ottavino(extern_err, rockspec_content);
2059 match err {
2060 LuaRockspecError::ExecutionError {
2061 cause,
2062 err_span: Some(span),
2063 ..
2064 } => {
2065 assert!(cause.contains("invalid rockspec field 'foo'"));
2066 assert_eq!(
2067 &rockspec_content[span.offset()..span.offset() + span.len()],
2068 "foo"
2069 );
2070 }
2071 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2072 }
2073 }
2074}