Skip to main content

lux_lib/lua_rockspec/
mod.rs

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