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