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