1mod build;
2mod dependency;
3mod deploy;
4mod partial;
5mod platform;
6mod rock_source;
7mod serde_util;
8mod test_spec;
9
10use crate::package;
11use std::{
12 collections::HashMap, convert::Infallible, fmt::Display, io, path::PathBuf, str::FromStr,
13};
14
15use ottavino::{Closure, Executor, Fuel};
16use ottavino_util::serde::from_value;
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18
19pub use build::*;
20pub use dependency::*;
21pub use deploy::*;
22use miette::{Diagnostic, NamedSource, SourceSpan};
23pub use partial::*;
24pub use platform::*;
25pub use rock_source::*;
26use ssri::Integrity;
27pub use test_spec::*;
28use thiserror::Error;
29use url::Url;
30
31pub(crate) use lux_macros::DisplayAsLuaKV;
32pub(crate) use serde_util::*;
33
34impl DisplayAsLuaValue for Url {
35 fn display_lua_value(&self) -> DisplayLuaValue {
36 DisplayLuaValue::String(self.to_string())
37 }
38}
39
40use crate::{
41 hash::HasIntegrity,
42 lua_version::{LuaVersion, LuaVersionUnset},
43 project::{project_toml, 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(package::Name),
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(package::Name),
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(package::Name),
95 #[error(transparent)]
96 #[diagnostic(transparent)]
97 ProjectToml(#[from] project_toml::Error),
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: package::Name,
158 version: package::Version,
160 description: RockDescription,
161 supported_platforms: PlatformSupport,
162 lua: package::VersionReq,
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(package::VersionReq::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) -> &package::Name {
357 &self.package
358 }
359
360 fn version(&self) -> &package::Version {
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) -> &package::VersionReq {
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: package::Spec,
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: package::VersionReq::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: &package::Spec,
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) -> &package::Name {
572 self.local.package()
573 }
574
575 fn version(&self) -> &package::Version {
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) -> &package::VersionReq {
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, package::Name, package::Version),
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
827 use super::*;
828
829 #[test]
830 pub fn parse_rockspec() {
831 let rockspec_content = "
832 rockspec_format = '1.0'\n
833 package = 'foo'\n
834 version = '1.0.0-1'\n
835 source = {\n
836 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
837 }\n
838 "
839 .to_string();
840 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
841 assert_eq!(rockspec.local.rockspec_format, Some(RockspecFormat::_1_0));
842 assert_eq!(rockspec.local.package, "foo".into());
843 assert_eq!(rockspec.local.version, "1.0.0-1".parse().unwrap());
844 assert_eq!(rockspec.local.description, RockDescription::default());
845
846 let rockspec_content = "
847 package = 'bar'\n
848 version = '2.0.0-1'\n
849 description = {}\n
850 source = {\n
851 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
852 }\n
853 "
854 .to_string();
855 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
856 assert_eq!(rockspec.local.rockspec_format, None);
857 assert_eq!(rockspec.local.package, "bar".into());
858 assert_eq!(rockspec.local.version, "2.0.0-1".parse().unwrap());
859 assert_eq!(rockspec.local.description, RockDescription::default());
860
861 let rockspec_content = "
862 package = 'rocks.nvim'\n
863 version = '3.0.0-1'\n
864 description = {\n
865 summary = 'some summary',
866 detailed = 'some detailed description',
867 license = 'MIT',
868 homepage = 'https://github.com/lumen-oss/rocks.nvim',
869 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
870 maintainer = 'Lumen Labs',
871 }\n
872 source = {\n
873 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
874 }\n
875 "
876 .to_string();
877 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
878 assert_eq!(rockspec.local.rockspec_format, None);
879 assert_eq!(rockspec.local.package, "rocks.nvim".into());
880 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
881 let expected_description = RockDescription {
882 summary: Some("some summary".into()),
883 detailed: Some("some detailed description".into()),
884 license: Some("MIT".into()),
885 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
886 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
887 maintainer: Some("Lumen Labs".into()),
888 labels: Vec::new(),
889 };
890 assert_eq!(rockspec.local.description, expected_description);
891
892 let rockspec_content = "
893 package = 'rocks.nvim'\n
894 version = '3.0.0-1'\n
895 description = {\n
896 summary = 'some summary',
897 detailed = 'some detailed description',
898 license = 'MIT',
899 homepage = 'https://github.com/lumen-oss/rocks.nvim',
900 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
901 maintainer = 'Lumen Labs',
902 labels = {},
903 }\n
904 external_dependencies = { FOO = { library = 'foo' } }\n
905 source = {\n
906 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
907 }\n
908 "
909 .to_string();
910 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
911 assert_eq!(rockspec.local.rockspec_format, None);
912 assert_eq!(rockspec.local.package, "rocks.nvim".into());
913 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
914 let expected_description = RockDescription {
915 summary: Some("some summary".into()),
916 detailed: Some("some detailed description".into()),
917 license: Some("MIT".into()),
918 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
919 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
920 maintainer: Some("Lumen Labs".into()),
921 labels: Vec::new(),
922 };
923 assert_eq!(rockspec.local.description, expected_description);
924 assert_eq!(
925 *rockspec
926 .local
927 .external_dependencies
928 .default
929 .get("FOO")
930 .unwrap(),
931 ExternalDependencySpec {
932 library: Some("foo".into()),
933 header: None
934 }
935 );
936
937 let rockspec_content = "
938 package = 'rocks.nvim'\n
939 version = '3.0.0-1'\n
940 description = {\n
941 summary = 'some summary',
942 detailed = 'some detailed description',
943 license = 'MIT',
944 homepage = 'https://github.com/lumen-oss/rocks.nvim',
945 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
946 maintainer = 'Lumen Labs',
947 labels = { 'package management', },
948 }\n
949 supported_platforms = { 'unix', '!windows' }\n
950 dependencies = { 'neorg ~> 6' }\n
951 build_dependencies = { 'foo' }\n
952 external_dependencies = { FOO = { header = 'foo.h' } }\n
953 test_dependencies = { 'busted >= 2.0.0' }\n
954 source = {\n
955 url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
956 hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
957 }\n
958 "
959 .to_string();
960 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
961 assert_eq!(rockspec.local.rockspec_format, None);
962 assert_eq!(rockspec.local.package, "rocks.nvim".into());
963 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
964 let expected_description = RockDescription {
965 summary: Some("some summary".into()),
966 detailed: Some("some detailed description".into()),
967 license: Some("MIT".into()),
968 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
969 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
970 maintainer: Some("Lumen Labs".into()),
971 labels: vec!["package management".into()],
972 };
973 assert_eq!(rockspec.local.description, expected_description);
974 assert!(rockspec
975 .local
976 .supported_platforms
977 .is_supported(&PlatformIdentifier::Unix));
978 assert!(!rockspec
979 .local
980 .supported_platforms
981 .is_supported(&PlatformIdentifier::Windows));
982 let neorg = package::Spec::parse("neorg".into(), "6.0.0".into()).unwrap();
983 assert!(rockspec
984 .local
985 .dependencies
986 .default
987 .into_iter()
988 .any(|dep| dep.matches(&neorg)));
989 let foo = package::Spec::parse("foo".into(), "1.0.0".into()).unwrap();
990 assert!(rockspec
991 .local
992 .build_dependencies
993 .default
994 .into_iter()
995 .any(|dep| dep.matches(&foo)));
996 let busted = package::Spec::parse("busted".into(), "2.2.0".into()).unwrap();
997 assert_eq!(
998 *rockspec
999 .local
1000 .external_dependencies
1001 .default
1002 .get("FOO")
1003 .unwrap(),
1004 ExternalDependencySpec {
1005 header: Some("foo.h".into()),
1006 library: None
1007 }
1008 );
1009 assert!(rockspec
1010 .local
1011 .test_dependencies
1012 .default
1013 .into_iter()
1014 .any(|dep| dep.matches(&busted)));
1015
1016 let rockspec_content = "
1017 rockspec_format = '1.0'\n
1018 package = 'foo'\n
1019 version = '1.0.0-1'\n
1020 source = {\n
1021 url = 'git+https://hub.com/owner/example-project/',\n
1022 branch = 'bar',\n
1023 }\n
1024 "
1025 .to_string();
1026 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1027 assert_eq!(
1028 rockspec.local.source.default.source_spec,
1029 RockSourceSpec::Git(GitSource {
1030 url: "https://hub.com/owner/example-project/".parse().unwrap(),
1031 git_ref: Some(GitRef::Branch("bar".into()))
1032 })
1033 );
1034 assert_eq!(rockspec.local.test, PerPlatform::default());
1035 let rockspec_content = "
1036 rockspec_format = '1.0'\n
1037 package = 'foo'\n
1038 version = '1.0.0-1'\n
1039 source = {\n
1040 url = 'git+https://hub.com/owner/example-project/',\n
1041 tag = 'bar',\n
1042 }\n
1043 "
1044 .to_string();
1045 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1046 assert_eq!(
1047 rockspec.local.source.default.source_spec,
1048 RockSourceSpec::Git(GitSource {
1049 url: "https://hub.com/owner/example-project/".parse().unwrap(),
1050 git_ref: Some(GitRef::Tag("bar".into()))
1051 })
1052 );
1053 let rockspec_content = "
1054 rockspec_format = '1.0'\n
1055 package = 'foo'\n
1056 version = '1.0.0-1'\n
1057 source = {\n
1058 url = 'git+https://hub.com/owner/example-project/',\n
1059 branch = 'bar',\n
1060 tag = 'baz',\n
1061 }\n
1062 "
1063 .to_string();
1064 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1065 let rockspec_content = "
1066 rockspec_format = '1.0'\n
1067 package = 'foo'\n
1068 version = '1.0.0-1'\n
1069 source = {\n
1070 url = 'git+https://hub.com/owner/example-project/',\n
1071 tag = 'bar',\n
1072 file = 'foo.tar.gz',\n
1073 }\n
1074 build = {\n
1075 install = {\n
1076 conf = {['foo.bar'] = 'config/bar.toml'},\n
1077 },\n
1078 }\n
1079 "
1080 .to_string();
1081 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1082 assert_eq!(
1083 rockspec.local.source.default.archive_name,
1084 Some("foo.tar.gz".into())
1085 );
1086 let foo_bar_path = rockspec
1087 .local
1088 .build
1089 .default
1090 .install
1091 .conf
1092 .get("foo.bar")
1093 .unwrap();
1094 assert_eq!(*foo_bar_path, PathBuf::from("config/bar.toml"));
1095 let rockspec_content = "
1096 rockspec_format = '1.0'\n
1097 package = 'foo'\n
1098 version = '1.0.0-1'\n
1099 source = {\n
1100 url = 'git+https://hub.com/example-project/foo.zip',\n
1101 }\n
1102 build = {\n
1103 install = {\n
1104 lua = {\n
1105 'foo.lua',\n
1106 ['foo.bar'] = 'src/bar.lua',\n
1107 },\n
1108 bin = {['foo.bar'] = 'bin/bar'},\n
1109 },\n
1110 }\n
1111 "
1112 .to_string();
1113 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1114 assert!(matches!(
1115 rockspec.local.build.default.build_backend,
1116 Some(BuildBackendSpec::Builtin { .. })
1117 ));
1118 let install_lua_spec = rockspec.local.build.default.install.lua;
1119 let foo_bar_path = install_lua_spec
1120 .get(&LuaModule::from_str("foo.bar").unwrap())
1121 .unwrap();
1122 assert_eq!(*foo_bar_path, PathBuf::from("src/bar.lua"));
1123 let foo_path = install_lua_spec
1124 .get(&LuaModule::from_str("foo").unwrap())
1125 .unwrap();
1126 assert_eq!(*foo_path, PathBuf::from("foo.lua"));
1127 let foo_bar_path = rockspec
1128 .local
1129 .build
1130 .default
1131 .install
1132 .bin
1133 .get("foo.bar")
1134 .unwrap();
1135 assert_eq!(*foo_bar_path, PathBuf::from("bin/bar"));
1136 let rockspec_content = "
1137 rockspec_format = '1.0'\n
1138 package = 'foo'\n
1139 version = '1.0.0-1'\n
1140 source = {\n
1141 url = 'git+https://hub.com/example-project/',\n
1142 }\n
1143 build = {\n
1144 copy_directories = { 'lua' },\n
1145 }\n
1146 "
1147 .to_string();
1148 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1149 let rockspec_content = "
1150 rockspec_format = '1.0'\n
1151 package = 'foo'\n
1152 version = '1.0.0-1'\n
1153 source = {\n
1154 url = 'git+https://hub.com/example-project/',\n
1155 }\n
1156 build = {\n
1157 copy_directories = { 'lib' },\n
1158 }\n
1159 "
1160 .to_string();
1161 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1162 let rockspec_content = "
1163 rockspec_format = '1.0'\n
1164 package = 'foo'\n
1165 version = '1.0.0-1'\n
1166 source = {\n
1167 url = 'git+https://hub.com/example-project/',\n
1168 }\n
1169 build = {\n
1170 copy_directories = { 'rock_manifest' },\n
1171 }\n
1172 "
1173 .to_string();
1174 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1175 let rockspec_content = "
1176 rockspec_format = '1.0'\n
1177 package = 'foo'\n
1178 version = '1.0.0-1'\n
1179 source = {\n
1180 url = 'git+https://hub.com/example-project/foo.zip',\n
1181 dir = 'baz',\n
1182 }\n
1183 build = {\n
1184 type = 'make',\n
1185 install = {\n
1186 lib = {['foo.so'] = 'lib/bar.so'},\n
1187 },\n
1188 copy_directories = {\n
1189 'plugin',\n
1190 'ftplugin',\n
1191 },\n
1192 patches = {\n
1193 ['lua51-support.diff'] = [[\n
1194 --- before.c\n
1195 +++ path/to/after.c\n
1196 ]],\n
1197 },\n
1198 }\n
1199 "
1200 .to_string();
1201 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1202 assert_eq!(rockspec.local.source.default.unpack_dir, Some("baz".into()));
1203 assert_eq!(
1204 rockspec.local.build.default.build_backend,
1205 Some(BuildBackendSpec::Make(MakeBuildSpec::default()))
1206 );
1207 let foo_bar_path = rockspec
1208 .local
1209 .build
1210 .default
1211 .install
1212 .lib
1213 .get("foo.so")
1214 .unwrap();
1215 assert_eq!(*foo_bar_path, PathBuf::from("lib/bar.so"));
1216 let copy_directories = rockspec.local.build.default.copy_directories;
1217 assert_eq!(
1218 copy_directories,
1219 vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1220 );
1221 let patches = rockspec.local.build.default.patches;
1222 let _patch = patches.get(&PathBuf::from("lua51-support.diff")).unwrap();
1223 let rockspec_content = "
1224 rockspec_format = '1.0'\n
1225 package = 'foo'\n
1226 version = '1.0.0-1'\n
1227 source = {\n
1228 url = 'git+https://hub.com/example-project/foo.zip',\n
1229 }\n
1230 build = {\n
1231 type = 'cmake',\n
1232 }\n
1233 "
1234 .to_string();
1235 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1236 assert_eq!(
1237 rockspec.local.build.default.build_backend,
1238 Some(BuildBackendSpec::CMake(CMakeBuildSpec::default()))
1239 );
1240 let rockspec_content = "
1241 rockspec_format = '1.0'\n
1242 package = 'foo'\n
1243 version = '1.0.0-1'\n
1244 source = {\n
1245 url = 'git+https://hub.com/example-project/foo.zip',\n
1246 }\n
1247 build = {\n
1248 type = 'command',\n
1249 build_command = 'foo',\n
1250 install_command = 'bar',\n
1251 }\n
1252 "
1253 .to_string();
1254 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1255 assert!(matches!(
1256 rockspec.local.build.default.build_backend,
1257 Some(BuildBackendSpec::Command(CommandBuildSpec { .. }))
1258 ));
1259 let rockspec_content = "
1260 rockspec_format = '1.0'\n
1261 package = 'foo'\n
1262 version = '1.0.0-1'\n
1263 source = {\n
1264 url = 'git+https://hub.com/example-project/foo.zip',\n
1265 }\n
1266 build = {\n
1267 type = 'command',\n
1268 install_command = 'foo',\n
1269 }\n
1270 "
1271 .to_string();
1272 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1273 let rockspec_content = "
1274 rockspec_format = '1.0'\n
1275 package = 'foo'\n
1276 version = '1.0.0-1'\n
1277 source = {\n
1278 url = 'git+https://hub.com/example-project/foo.zip',\n
1279 }\n
1280 build = {\n
1281 type = 'command',\n
1282 build_command = 'foo',\n
1283 }\n
1284 "
1285 .to_string();
1286 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1287 let rockspec_content = "
1289 package = 'rocks'\n
1290 version = '3.0.0-1'\n
1291 dependencies = {\n
1292 'neorg ~> 6',\n
1293 'toml-edit ~> 1',\n
1294 platforms = {\n
1295 windows = {\n
1296 'neorg = 5.0.0',\n
1297 'toml = 1.0.0',\n
1298 },\n
1299 unix = {\n
1300 'neorg = 5.0.0',\n
1301 },\n
1302 linux = {\n
1303 'toml = 1.0.0',\n
1304 },\n
1305 },\n
1306 }\n
1307 source = {\n
1308 url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
1309 hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
1310 }\n
1311 "
1312 .to_string();
1313 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1314 let neorg_override = package::Spec::parse("neorg".into(), "5.0.0".into()).unwrap();
1315 let toml_edit = package::Spec::parse("toml-edit".into(), "1.0.0".into()).unwrap();
1316 let toml = package::Spec::parse("toml".into(), "1.0.0".into()).unwrap();
1317 assert_eq!(rockspec.local.dependencies.default.len(), 2);
1318 let per_platform = &rockspec.local.dependencies.per_platform;
1319 assert_eq!(
1320 per_platform
1321 .get(&PlatformIdentifier::Windows)
1322 .unwrap()
1323 .iter()
1324 .filter(|dep| dep.matches(&neorg_override)
1325 || dep.matches(&toml_edit)
1326 || dep.matches(&toml))
1327 .count(),
1328 3
1329 );
1330 assert_eq!(
1331 per_platform
1332 .get(&PlatformIdentifier::Unix)
1333 .unwrap()
1334 .iter()
1335 .filter(|dep| dep.matches(&neorg_override)
1336 || dep.matches(&toml_edit)
1337 || dep.matches(&toml))
1338 .count(),
1339 2
1340 );
1341 assert_eq!(
1342 per_platform
1343 .get(&PlatformIdentifier::Linux)
1344 .unwrap()
1345 .iter()
1346 .filter(|dep| dep.matches(&neorg_override)
1347 || dep.matches(&toml_edit)
1348 || dep.matches(&toml))
1349 .count(),
1350 3
1351 );
1352 let rockspec_content = "
1353 package = 'rocks'\n
1354 version = '3.0.0-1'\n
1355 external_dependencies = {\n
1356 FOO = { library = 'foo' },\n
1357 platforms = {\n
1358 windows = {\n
1359 FOO = { library = 'foo.dll' },\n
1360 },\n
1361 unix = {\n
1362 BAR = { header = 'bar.h' },\n
1363 },\n
1364 linux = {\n
1365 FOO = { library = 'foo.so' },\n
1366 },\n
1367 },\n
1368 }\n
1369 source = {\n
1370 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1371 }\n
1372 "
1373 .to_string();
1374 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1375 assert_eq!(
1376 *rockspec
1377 .local
1378 .external_dependencies
1379 .default
1380 .get("FOO")
1381 .unwrap(),
1382 ExternalDependencySpec {
1383 library: Some("foo".into()),
1384 header: None
1385 }
1386 );
1387 let per_platform = rockspec.local.external_dependencies.per_platform;
1388 assert_eq!(
1389 *per_platform
1390 .get(&PlatformIdentifier::Windows)
1391 .and_then(|it| it.get("FOO"))
1392 .unwrap(),
1393 ExternalDependencySpec {
1394 library: Some("foo.dll".into()),
1395 header: None
1396 }
1397 );
1398 assert_eq!(
1399 *per_platform
1400 .get(&PlatformIdentifier::Unix)
1401 .and_then(|it| it.get("FOO"))
1402 .unwrap(),
1403 ExternalDependencySpec {
1404 library: Some("foo".into()),
1405 header: None
1406 }
1407 );
1408 assert_eq!(
1409 *per_platform
1410 .get(&PlatformIdentifier::Unix)
1411 .and_then(|it| it.get("BAR"))
1412 .unwrap(),
1413 ExternalDependencySpec {
1414 header: Some("bar.h".into()),
1415 library: None
1416 }
1417 );
1418 assert_eq!(
1419 *per_platform
1420 .get(&PlatformIdentifier::Linux)
1421 .and_then(|it| it.get("BAR"))
1422 .unwrap(),
1423 ExternalDependencySpec {
1424 header: Some("bar.h".into()),
1425 library: None
1426 }
1427 );
1428 assert_eq!(
1429 *per_platform
1430 .get(&PlatformIdentifier::Linux)
1431 .and_then(|it| it.get("FOO"))
1432 .unwrap(),
1433 ExternalDependencySpec {
1434 library: Some("foo.so".into()),
1435 header: None
1436 }
1437 );
1438 let rockspec_content = "
1439 rockspec_format = '1.0'\n
1440 package = 'foo'\n
1441 version = '1.0.0-1'\n
1442 source = {\n
1443 url = 'git+https://hub.com/example-project/.git',\n
1444 branch = 'bar',\n
1445 platforms = {\n
1446 macosx = {\n
1447 branch = 'mac',\n
1448 },\n
1449 windows = {\n
1450 url = 'git+https://winhub.com/example-project/.git',\n
1451 branch = 'win',\n
1452 },\n
1453 },\n
1454 }\n
1455 "
1456 .to_string();
1457 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1458 assert_eq!(
1459 rockspec.local.source.default.source_spec,
1460 RockSourceSpec::Git(GitSource {
1461 url: "https://hub.com/example-project/.git".parse().unwrap(),
1462 git_ref: Some(GitRef::Branch("bar".into()))
1463 })
1464 );
1465 assert_eq!(
1466 rockspec
1467 .source
1468 .per_platform
1469 .get(&PlatformIdentifier::MacOSX)
1470 .map(|it| it.source_spec.clone())
1471 .unwrap(),
1472 RockSourceSpec::Git(GitSource {
1473 url: "https://hub.com/example-project/.git".parse().unwrap(),
1474 git_ref: Some(GitRef::Branch("mac".into()))
1475 })
1476 );
1477 assert_eq!(
1478 rockspec
1479 .source
1480 .per_platform
1481 .get(&PlatformIdentifier::Windows)
1482 .map(|it| it.source_spec.clone())
1483 .unwrap(),
1484 RockSourceSpec::Git(GitSource {
1485 url: "https://winhub.com/example-project/.git".parse().unwrap(),
1486 git_ref: Some(GitRef::Branch("win".into()))
1487 })
1488 );
1489 let rockspec_content = "
1490 rockspec_format = '1.0'\n
1491 package = 'foo'\n
1492 version = '1.0.0-1'\n
1493 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1494 build = {\n
1495 type = 'make',\n
1496 install = {\n
1497 lib = {['foo.bar'] = 'lib/bar.so'},\n
1498 },\n
1499 copy_directories = { 'plugin' },\n
1500 platforms = {\n
1501 unix = {\n
1502 copy_directories = { 'ftplugin' },\n
1503 },\n
1504 linux = {\n
1505 copy_directories = { 'foo' },\n
1506 },\n
1507 },\n
1508 }\n
1509 "
1510 .to_string();
1511 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1512 let per_platform = rockspec.local.build.per_platform;
1513 let unix = per_platform.get(&PlatformIdentifier::Unix).unwrap();
1514 assert_eq!(
1515 unix.copy_directories,
1516 vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1517 );
1518 let linux = per_platform.get(&PlatformIdentifier::Linux).unwrap();
1519 assert_eq!(
1520 linux.copy_directories,
1521 vec![
1522 PathBuf::from("plugin"),
1523 PathBuf::from("foo"),
1524 PathBuf::from("ftplugin")
1525 ]
1526 );
1527 let rockspec_content = "
1528 package = 'foo'\n
1529 version = '1.0.0-1'\n
1530 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1531 build = {\n
1532 type = 'builtin',\n
1533 modules = {\n
1534 cjson = {\n
1535 sources = { 'lua_cjson.c', 'strbuf.c', 'fpconv.c' },\n
1536 }\n
1537 },\n
1538 platforms = {\n
1539 win32 = { modules = { cjson = { defines = {\n
1540 'DISABLE_INVALID_NUMBERS', 'USE_INTERNAL_ISINF'\n
1541 } } } }\n
1542 },\n
1543 }\n
1544 "
1545 .to_string();
1546 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1547 let win32 = rockspec.local.build.get(&PlatformIdentifier::Windows);
1548 assert_eq!(
1549 win32.build_backend,
1550 Some(BuildBackendSpec::Builtin(BuiltinBuildSpec {
1551 modules: vec![(
1552 LuaModule::from_str("cjson").unwrap(),
1553 ModuleSpec::ModulePaths(ModulePaths {
1554 sources: vec!["lua_cjson.c".into(), "strbuf.c".into(), "fpconv.c".into()],
1555 libraries: Vec::default(),
1556 defines: vec![
1557 ("DISABLE_INVALID_NUMBERS".into(), None),
1558 ("USE_INTERNAL_ISINF".into(), None)
1559 ],
1560 incdirs: Vec::default(),
1561 libdirs: Vec::default(),
1562 })
1563 )]
1564 .into_iter()
1565 .collect()
1566 }))
1567 );
1568 let rockspec_content = "
1569 rockspec_format = '1.0'\n
1570 package = 'foo'\n
1571 version = '1.0.0-1'\n
1572 deploy = {\n
1573 wrap_bin_scripts = false,\n
1574 }\n
1575 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1576 ";
1577 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1578 let deploy_spec = &rockspec.deploy().current_platform();
1579 assert!(!deploy_spec.wrap_bin_scripts);
1580 }
1581
1582 #[test]
1583 pub fn parse_scm_rockspec() {
1584 let rockspec_content = "
1585 package = 'foo'\n
1586 version = 'scm-1'\n
1587 source = {\n
1588 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1589 }\n
1590 "
1591 .to_string();
1592 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1593 assert_eq!(rockspec.local.package, "foo".into());
1594 assert_eq!(rockspec.local.version, "scm-1".parse().unwrap());
1595 }
1596
1597 #[test]
1598 pub fn regression_luasystem() {
1599 let rockspec_content = String::from_utf8(
1600 std::fs::read(
1601 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1602 .join("resources/test/luasystem-0.4.4-1.rockspec"),
1603 )
1604 .unwrap(),
1605 )
1606 .unwrap();
1607 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1608 let build_spec = rockspec.local.build.current_platform();
1609 assert!(matches!(
1610 build_spec.build_backend,
1611 Some(BuildBackendSpec::Builtin { .. })
1612 ));
1613 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) =
1614 &build_spec.build_backend
1615 {
1616 assert_eq!(
1617 modules.get(&LuaModule::from_str("system.init").unwrap()),
1618 Some(&ModuleSpec::SourcePath("system/init.lua".into()))
1619 );
1620 assert_eq!(
1621 modules.get(&LuaModule::from_str("system.core").unwrap()),
1622 Some(&ModuleSpec::ModulePaths(ModulePaths {
1623 sources: vec![
1624 "src/core.c".into(),
1625 "src/compat.c".into(),
1626 "src/time.c".into(),
1627 "src/environment.c".into(),
1628 "src/random.c".into(),
1629 "src/term.c".into(),
1630 "src/bitflags.c".into(),
1631 "src/wcwidth.c".into(),
1632 ],
1633 defines: luasystem_expected_defines(),
1634 libraries: luasystem_expected_libraries(),
1635 incdirs: luasystem_expected_incdirs(),
1636 libdirs: luasystem_expected_libdirs(),
1637 }))
1638 );
1639 }
1640 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1641 .local
1642 .build
1643 .get(&PlatformIdentifier::Windows)
1644 .build_backend
1645 {
1646 if let ModuleSpec::ModulePaths(paths) = modules
1647 .get(&LuaModule::from_str("system.core").unwrap())
1648 .unwrap()
1649 {
1650 assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1651 };
1652 }
1653 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1654 .local
1655 .build
1656 .get(&PlatformIdentifier::Win32)
1657 .build_backend
1658 {
1659 if let ModuleSpec::ModulePaths(paths) = modules
1660 .get(&LuaModule::from_str("system.core").unwrap())
1661 .unwrap()
1662 {
1663 assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1664 };
1665 }
1666 }
1667
1668 fn luasystem_expected_defines() -> Vec<(String, Option<String>)> {
1669 if cfg!(target_os = "windows") {
1670 vec![
1671 ("WINVER".into(), Some("0x0600".into())),
1672 ("_WIN32_WINNT".into(), Some("0x0600".into())),
1673 ]
1674 } else {
1675 Vec::default()
1676 }
1677 }
1678
1679 fn luasystem_expected_windows_libraries() -> Vec<PathBuf> {
1680 vec!["advapi32".into(), "winmm".into()]
1681 }
1682 fn luasystem_expected_libraries() -> Vec<PathBuf> {
1683 if cfg!(any(target_os = "linux", target_os = "android")) {
1684 vec!["rt".into()]
1685 } else if cfg!(target_os = "windows") {
1686 luasystem_expected_windows_libraries()
1687 } else {
1688 Vec::default()
1689 }
1690 }
1691
1692 fn luasystem_expected_incdirs() -> Vec<PathBuf> {
1693 Vec::default()
1694 }
1695
1696 fn luasystem_expected_libdirs() -> Vec<PathBuf> {
1697 Vec::default()
1698 }
1699
1700 #[test]
1701 pub fn rust_mlua_rockspec() {
1702 let rockspec_content = "
1703 package = 'foo'\n
1704 version = 'scm-1'\n
1705 source = {\n
1706 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1707 }\n
1708 build = {
1709 type = 'rust-mlua',
1710 modules = {
1711 'foo',
1712 bar = 'baz',
1713 },
1714 target_path = 'path/to/cargo/target/directory',
1715 default_features = false,
1716 include = {
1717 'file.lua',
1718 ['path/to/another/file.lua'] = 'another-file.lua',
1719 },
1720 features = {'extra', 'features'},
1721 }
1722 ";
1723 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1724 let build_spec = rockspec.local.build.current_platform();
1725 if let Some(BuildBackendSpec::RustMlua(build_spec)) = build_spec.build_backend.to_owned() {
1726 assert_eq!(
1727 build_spec.modules.get("foo").unwrap(),
1728 &PathBuf::from(format!("libfoo.{}", std::env::consts::DLL_EXTENSION))
1729 );
1730 assert_eq!(
1731 build_spec.modules.get("bar").unwrap(),
1732 &PathBuf::from(format!("libbaz.{}", std::env::consts::DLL_EXTENSION))
1733 );
1734 assert_eq!(
1735 build_spec.include.get(&PathBuf::from("file.lua")).unwrap(),
1736 &PathBuf::from("file.lua")
1737 );
1738 assert_eq!(
1739 build_spec
1740 .include
1741 .get(&PathBuf::from("path/to/another/file.lua"))
1742 .unwrap(),
1743 &PathBuf::from("another-file.lua")
1744 );
1745 } else {
1746 panic!("Expected RustMlua build backend");
1747 }
1748 }
1749
1750 #[test]
1751 pub fn rust_binary_rockspec() {
1752 let rockspec_content = "
1753 package = 'foo'\n
1754 version = 'scm-1'\n
1755 source = {\n
1756 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1757 }\n
1758 build = {\n
1759 type = 'rust-binary',\n
1760 package = 'foo',\n
1761 features = {'extra', 'features'},\n
1762 }\n
1763 ";
1764 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1765 let build_spec = rockspec.local.build.current_platform();
1766 if let Some(BuildBackendSpec::RustBinary(build_spec)) = build_spec.build_backend.to_owned()
1767 {
1768 assert_eq!(build_spec.package, Some("foo".to_string()));
1769 assert_eq!(build_spec.features, vec!["extra", "features"]);
1770 } else {
1771 panic!("Expected RustBinary build backend");
1772 }
1773 }
1774
1775 #[tokio::test]
1776 pub async fn regression_ltui() {
1777 let content = String::from_utf8(
1778 std::fs::read(
1779 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1780 .join("resources/test/ltui-2.8-2.rockspec"),
1781 )
1782 .unwrap(),
1783 )
1784 .unwrap();
1785 RemoteLuaRockspec::new(&content).unwrap();
1786 }
1787
1788 #[test]
1791 pub fn regression_off_spec_install_binaries() {
1792 let rockspec_content = r#"
1793 package = "WSAPI"
1794 version = "1.7-1"
1795
1796 source = {
1797 url = "git://github.com/keplerproject/wsapi",
1798 tag = "v1.7",
1799 }
1800
1801 build = {
1802 type = "builtin",
1803 modules = {
1804 ["wsapi"] = "src/wsapi.lua",
1805 },
1806 -- Offending Line
1807 install = { bin = { "src/launcher/wsapi.cgi" } }
1808 }
1809 "#;
1810
1811 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1812
1813 assert_eq!(
1814 rockspec.build().current_platform().install.bin,
1815 HashMap::from([("wsapi.cgi".into(), PathBuf::from("src/launcher/wsapi.cgi"))])
1816 );
1817 }
1818
1819 #[test]
1820 pub fn regression_external_dependencies() {
1821 let content = String::from_utf8(
1822 std::fs::read(
1823 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1824 .join("resources/test/luaossl-20220711-0.rockspec"),
1825 )
1826 .unwrap(),
1827 )
1828 .unwrap();
1829 let rockspec = RemoteLuaRockspec::new(&content).unwrap();
1830 if cfg!(target_family = "unix") {
1831 assert_eq!(
1832 rockspec
1833 .local
1834 .external_dependencies
1835 .current_platform()
1836 .get("OPENSSL")
1837 .unwrap(),
1838 &ExternalDependencySpec {
1839 library: Some("ssl".into()),
1840 header: Some("openssl/ssl.h".into()),
1841 }
1842 );
1843 }
1844 let per_platform = rockspec.local.external_dependencies.per_platform;
1845 assert_eq!(
1846 *per_platform
1847 .get(&PlatformIdentifier::Windows)
1848 .and_then(|it| it.get("OPENSSL"))
1849 .unwrap(),
1850 ExternalDependencySpec {
1851 library: Some("libeay32".into()),
1852 header: Some("openssl/ssl.h".into()),
1853 }
1854 );
1855 }
1856
1857 #[test]
1858 pub fn remote_lua_rockspec_from_package_and_source_spec() {
1859 let package_req = "foo@1.0.5".parse().unwrap();
1860 let source = GitSource {
1861 url: "https://hub.com/owner/example-project.git".parse().unwrap(),
1862 git_ref: Some(GitRef::Tag("1.0.5".into())),
1863 };
1864 let source_spec = RockSourceSpec::Git(source);
1865 let rockspec = RemoteLuaRockspec::from_source_spec(
1866 package_req,
1867 source_spec.clone(),
1868 Vec::new(),
1869 HashMap::new(),
1870 );
1871 let generated_rockspec_str = rockspec.local.raw_content;
1872 let rockspec2 = RemoteLuaRockspec::new(&generated_rockspec_str).unwrap();
1873 assert_eq!(rockspec2.local.package, "foo".into());
1874 assert_eq!(rockspec2.local.version, "1.0.5".parse().unwrap());
1875 assert_eq!(rockspec2.local.source, PerPlatform::new(source_spec.into()));
1876 }
1877
1878 #[test]
1879 pub fn regression_complex_source_field() {
1880 let rockspec_content = r#"
1881 package = "say"
1882 local rock_version = "1.4.1"
1883 local rock_release = "3"
1884 local namespace = "lunarmodules"
1885 local repository = package
1886
1887 version = ("%s-%s"):format(rock_version, rock_release)
1888
1889 source = {
1890 url = ("git+https://github.com/%s/%s.git"):format(namespace, repository),
1891 branch = rock_version == "scm" and "master" or nil,
1892 tag = rock_version ~= "scm" and "v"..rock_version or nil,
1893 }
1894
1895 description = {
1896 summary = "Lua string hashing/indexing library",
1897 }
1898
1899 dependencies = {
1900 "lua >= 5.1",
1901 }
1902
1903 build = {
1904 type = "builtin",
1905 }
1906 "#
1907 .to_string();
1908 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1909 }
1910
1911 fn eval_lua_global<T: serde::de::DeserializeOwned>(code: &str, key: &'static str) -> T {
1912 use ottavino::{Closure, Executor, Fuel, Lua};
1913 use ottavino_util::serde::from_value;
1914 Lua::core()
1915 .try_enter(|ctx| {
1916 let closure = Closure::load(ctx, None, code.as_bytes())?;
1917 let executor = Executor::start(ctx, closure.into(), ());
1918 executor.step(ctx, &mut Fuel::with(i32::MAX))?;
1919 from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
1920 })
1921 .unwrap()
1922 }
1923
1924 #[test]
1925 pub fn rock_description_roundtrip() {
1926 let desc = RockDescription {
1927 summary: Some("A great package".into()),
1928 detailed: Some("Detailed description here".into()),
1929 license: Some("MIT".into()),
1930 homepage: Some("https://example.com".parse().unwrap()),
1931 issues_url: Some("https://example.com/issues".parse().unwrap()),
1932 maintainer: Some("Some Maintainer <maintainer@example.com>".into()),
1933 labels: vec!["neovim".into(), "lua".into()],
1934 };
1935 let lua = desc.display_lua().to_string();
1936 let restored: RockDescription = eval_lua_global(&lua, "description");
1937 assert_eq!(desc, restored);
1938 }
1939
1940 #[test]
1941 pub fn rock_description_empty_roundtrip() {
1942 let desc = RockDescription::default();
1943 let lua = desc.display_lua().to_string();
1944 let lua = if lua.trim().is_empty() {
1946 "description = {}".to_string()
1947 } else {
1948 lua
1949 };
1950 let restored: RockDescription = eval_lua_global(&lua, "description");
1951 assert_eq!(desc, restored);
1952 }
1953
1954 #[test]
1955 pub fn regression_luasec() {
1956 let rockspec_content = r#"
1957package = "LuaSec"
1958version = "1.3.2-1"
1959source = {
1960 url = "git+https://github.com/brunoos/luasec",
1961 tag = "v1.3.2",
1962}
1963external_dependencies = {
1964 platforms = {
1965 unix = {
1966 OPENSSL = {
1967 header = "openssl/ssl.h",
1968 library = "ssl"
1969 }
1970 },
1971 windows = {
1972 OPENSSL = {
1973 header = "openssl/ssl.h",
1974 }
1975 },
1976 }
1977}
1978"#;
1979 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1980 let linux_external_deps = rockspec
1981 .external_dependencies()
1982 .get(&PlatformIdentifier::Linux);
1983 assert!(linux_external_deps.get("OPENSSL").is_some());
1984 let windows_external_deps = rockspec
1985 .external_dependencies()
1986 .get(&PlatformIdentifier::Windows);
1987 assert!(windows_external_deps.get("OPENSSL").is_some());
1988 }
1989
1990 #[test]
1991 pub fn find_value_span_spaces() {
1992 let source = "package = 'foo'\nversion = '1.0'";
1993 let span = find_value_span(source, "package");
1994 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1995 }
1996
1997 #[test]
1998 pub fn find_value_span_no_spaces() {
1999 let source = "package='foo'";
2000 let span = find_value_span(source, "package");
2001 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2002 }
2003
2004 #[test]
2005 pub fn find_value_span_returns_empty_when_not_found() {
2006 let source = "version = '1.0'";
2007 let span = find_value_span(source, "package");
2008 assert_eq!(span.len(), 0);
2009 }
2010
2011 #[test]
2012 pub fn find_value_span_skips_url_containing_field() {
2013 let source = "homepage = 'https://luapackager.com'\npackage = 'foo'";
2014 let span = find_value_span(source, "package");
2015 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2016 }
2017
2018 #[test]
2019 pub fn find_value_span_double_quoted() {
2020 let source = r#"package = "foo""#;
2021 let span = find_value_span(source, "package");
2022 assert_eq!(
2023 &source[span.offset()..span.offset() + span.len()],
2024 r#""foo""#
2025 );
2026 }
2027
2028 #[test]
2029 pub fn find_value_span_table() {
2030 let source = "description = {\n summary = 'bar'\n}";
2031 let span = find_value_span(source, "description");
2032 assert_eq!(&source[span.offset()..span.offset() + span.len()], "{");
2033 }
2034
2035 #[test]
2036 pub fn parse_lua_rockspec_field_error() {
2037 let rockspec_content = "package = 'my-lua-lib'\nfoo = 123";
2038 let extern_err = ottavino::Lua::core()
2039 .try_enter(|_ctx| {
2040 Err(ParseLuaRockspecFieldError::Deserialize {
2041 field: "foo".into(),
2042 source: Box::new(ottavino_util::serde::de::Error::Message(
2043 "expected bar".into(),
2044 )),
2045 })?;
2046 Ok(())
2047 })
2048 .unwrap_err();
2049 let err = Error::from_ottavino(extern_err, rockspec_content);
2050 match err {
2051 Error::ExecutionError {
2052 cause,
2053 err_span: Some(span),
2054 ..
2055 } => {
2056 assert!(cause.contains("invalid rockspec field 'foo'"));
2057 assert_eq!(
2058 &rockspec_content[span.offset()..span.offset() + span.len()],
2059 "foo"
2060 );
2061 }
2062 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2063 }
2064 let extern_err = ottavino::Lua::core()
2065 .try_enter(|_ctx| {
2066 Err(ParseLuaRockspecFieldError::DeserializeTable {
2067 field: "foo".into(),
2068 source: Box::new(ottavino_util::serde::de::Error::Message(
2069 "expected bar".into(),
2070 )),
2071 })?;
2072 Ok(())
2073 })
2074 .unwrap_err();
2075 let err = Error::from_ottavino(extern_err, rockspec_content);
2076 match err {
2077 Error::ExecutionError {
2078 cause,
2079 err_span: Some(span),
2080 ..
2081 } => {
2082 assert!(cause.contains("invalid rockspec field 'foo'"));
2083 assert_eq!(
2084 &rockspec_content[span.offset()..span.offset() + span.len()],
2085 "foo"
2086 );
2087 }
2088 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2089 }
2090 let extern_err = ottavino::Lua::core()
2091 .try_enter(|_ctx| {
2092 Err(ParseLuaRockspecFieldError::NotATable {
2093 field: "foo".into(),
2094 invalid_type: "integer".into(),
2095 })?;
2096 Ok(())
2097 })
2098 .unwrap_err();
2099 let err = Error::from_ottavino(extern_err, rockspec_content);
2100 match err {
2101 Error::ExecutionError {
2102 cause,
2103 err_span: Some(span),
2104 ..
2105 } => {
2106 assert!(cause.contains("invalid rockspec field 'foo'"));
2107 assert_eq!(
2108 &rockspec_content[span.offset()..span.offset() + span.len()],
2109 "foo"
2110 );
2111 }
2112 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2113 }
2114 }
2115}