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