Skip to main content

lux_lib/lua_rockspec/
mod.rs

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