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