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