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