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