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