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