Skip to main content

lux_lib/lua_rockspec/
mod.rs

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