lux_lib/project/
project_toml.rs

1//! Structs and utilities for `lux.toml`
2
3use crate::git::shorthand::GitUrlShorthand;
4use crate::git::GitSource;
5use crate::hash::HasIntegrity;
6use crate::lockfile::OptState;
7use crate::lockfile::PinnedState;
8use crate::lua_rockspec::DeploySpec;
9use crate::lua_rockspec::LocalLuaRockspec;
10use crate::lua_rockspec::LocalRockSource;
11use crate::lua_rockspec::LuaRockspecError;
12use crate::lua_rockspec::RemoteLuaRockspec;
13use crate::lua_rockspec::RockSourceSpec;
14use crate::operations::RunCommand;
15use crate::package::PackageNameList;
16use crate::rockspec::lua_dependency::LuaDependencySpec;
17use std::io;
18use std::{collections::HashMap, path::PathBuf};
19
20use itertools::Itertools;
21use mlua::ExternalResult;
22use mlua::UserData;
23use nonempty::NonEmpty;
24use serde::de;
25use serde::{Deserialize, Deserializer};
26use ssri::Integrity;
27use thiserror::Error;
28
29use crate::{
30    config::{Config, LuaVersion},
31    lua_rockspec::{
32        BuildSpec, BuildSpecInternal, BuildSpecInternalError, DisplayAsLuaKV, ExternalDependencies,
33        ExternalDependencySpec, FromPlatformOverridable, LuaVersionError, PartialLuaRockspec,
34        PerPlatform, PlatformIdentifier, PlatformSupport, PlatformValidationError,
35        RemoteRockSource, RockDescription, RockSourceError, RockspecFormat, TestSpec,
36        TestSpecDecodeError, TestSpecInternal,
37    },
38    package::{
39        BuildDependencies, Dependencies, PackageName, PackageReq, PackageVersion,
40        PackageVersionReq, TestDependencies,
41    },
42    rockspec::{LuaVersionCompatibility, Rockspec},
43};
44
45use super::gen::GenerateSourceError;
46use super::gen::RockSourceTemplate;
47use super::r#gen::GenerateVersionError;
48use super::r#gen::PackageVersionTemplate;
49use super::ProjectRoot;
50
51pub const PROJECT_TOML: &str = "lux.toml";
52
53#[derive(Deserialize)]
54#[serde(untagged)]
55#[allow(clippy::large_enum_variant)] // This is ok because it's just a Deserialize helper
56enum DependencyEntry {
57    Simple(PackageVersionReq),
58    Detailed(DependencyTableEntry),
59}
60
61#[derive(Debug, Deserialize)]
62struct DependencyTableEntry {
63    version: PackageVersionReq,
64    #[serde(default)]
65    opt: Option<bool>,
66    #[serde(default)]
67    pin: Option<bool>,
68    #[serde(default)]
69    git: Option<GitUrlShorthand>,
70    #[serde(default)]
71    rev: Option<String>,
72}
73
74fn parse_map_to_dependency_vec_opt<'de, D>(
75    deserializer: D,
76) -> Result<Option<Vec<LuaDependencySpec>>, D::Error>
77where
78    D: Deserializer<'de>,
79{
80    let packages: Option<HashMap<PackageName, DependencyEntry>> =
81        Option::deserialize(deserializer)?;
82
83    match packages {
84        None => Ok(None),
85        Some(packages) => Ok(Some(
86            packages
87                .into_iter()
88                .map(|(name, spec)| match spec {
89                    DependencyEntry::Simple(version_req) => {
90                        Ok(PackageReq { name, version_req }.into())
91                    }
92                    DependencyEntry::Detailed(entry) => {
93                        let source = match (entry.git, entry.rev) {
94                            (None, None) => Ok(None),
95                            (None, Some(_)) => Err(de::Error::custom(format!(
96                                "dependency {} specifies a 'rev', but missing a 'git' field",
97                                &name
98                            ))),
99                            (Some(git), Some(rev)) => Ok(Some(RockSourceSpec::Git(GitSource {
100                                url: git.into(),
101                                checkout_ref: Some(rev),
102                            }))),
103                            (Some(git), None) => Ok(Some(RockSourceSpec::Git(GitSource {
104                                url: git.into(),
105                                checkout_ref: Some(
106                                    entry
107                                        .version
108                                        .clone()
109                                        .to_string()
110                                        .trim_start_matches("=")
111                                        .to_string(),
112                                ),
113                            }))),
114                        }?;
115                        Ok(LuaDependencySpec {
116                            package_req: PackageReq {
117                                name,
118                                version_req: entry.version,
119                            },
120                            opt: OptState::from(entry.opt.unwrap_or(false)),
121                            pin: PinnedState::from(entry.pin.unwrap_or(false)),
122                            source,
123                        })
124                    }
125                })
126                .try_collect()?,
127        )),
128    }
129}
130
131#[derive(Debug, Error)]
132pub enum ProjectTomlError {
133    #[error("error generating rockspec source:\n{0}")]
134    GenerateSource(#[from] GenerateSourceError),
135    #[error("error generating rockspec version:\n{0}")]
136    GenerateVersion(#[from] GenerateVersionError),
137}
138
139#[derive(Debug, Error)]
140pub enum LocalProjectTomlValidationError {
141    #[error("no lua version provided")]
142    NoLuaVersion,
143    #[error(transparent)]
144    TestSpecError(#[from] TestSpecDecodeError),
145    #[error(transparent)]
146    BuildSpecInternal(#[from] BuildSpecInternalError),
147    #[error(transparent)]
148    PlatformValidationError(#[from] PlatformValidationError),
149    #[error("{}copy_directories cannot contain a rockspec name", ._0.as_ref().map(|p| format!("{p}: ")).unwrap_or_default())]
150    CopyDirectoriesContainRockspecName(Option<String>),
151    #[error(transparent)]
152    RockSourceError(#[from] RockSourceError),
153    #[error("duplicate dependencies: {0}")]
154    DuplicateDependencies(PackageNameList),
155    #[error("duplicate test dependencies: {0}")]
156    DuplicateTestDependencies(PackageNameList),
157    #[error("duplicate build dependencies: {0}")]
158    DuplicateBuildDependencies(PackageNameList),
159    #[error("dependencies field cannot contain lua - please provide the version in the top-level lua field")]
160    DependenciesContainLua,
161    #[error("error generating rockspec source:\n{0}")]
162    GenerateSource(#[from] GenerateSourceError),
163    #[error("error generating rockspec version:\n{0}")]
164    GenerateVersion(#[from] GenerateVersionError),
165}
166
167#[derive(Debug, Error)]
168pub enum RemoteProjectTomlValidationError {
169    #[error("error generating rockspec source:\n{0}")]
170    GenerateSource(#[from] GenerateSourceError),
171    #[error("error generating rockspec version:\n{0}")]
172    GenerateVersion(#[from] GenerateVersionError),
173    #[error(transparent)]
174    LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
175}
176
177/// The `lux.toml` file.
178/// The only required fields are `package` and `build`, which are required to build a project using `lux build`.
179/// The rest of the fields are optional, but are required to build a rockspec.
180#[derive(Clone, Debug, Deserialize)]
181pub struct PartialProjectToml {
182    pub(crate) package: PackageName,
183    #[serde(default, rename = "version")]
184    pub(crate) version_template: PackageVersionTemplate,
185    #[serde(default)]
186    pub(crate) build: BuildSpecInternal,
187    pub(crate) rockspec_format: Option<RockspecFormat>,
188    #[serde(default)]
189    pub(crate) run: Option<RunSpec>,
190    #[serde(default)]
191    pub(crate) lua: Option<PackageVersionReq>,
192    #[serde(default)]
193    pub(crate) description: Option<RockDescription>,
194    #[serde(default)]
195    pub(crate) supported_platforms: Option<HashMap<PlatformIdentifier, bool>>,
196    #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
197    pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
198    #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
199    pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
200    #[serde(default)]
201    pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
202    #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
203    pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
204    #[serde(default, rename = "source")]
205    pub(crate) source_template: RockSourceTemplate,
206    #[serde(default)]
207    pub(crate) test: Option<TestSpecInternal>,
208    #[serde(default)]
209    pub(crate) deploy: Option<DeploySpec>,
210
211    /// Used to bind the project TOML to a project root
212    #[serde(skip, default = "ProjectRoot::new")]
213    pub(crate) project_root: ProjectRoot,
214}
215
216impl UserData for PartialProjectToml {
217    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
218        methods.add_method("package", |_, this, _: ()| Ok(this.package().clone()));
219        methods.add_method("to_local", |_, this, _: ()| {
220            this.into_local().into_lua_err()
221        });
222        methods.add_method("to_remote", |_, this, _: ()| {
223            this.into_remote().into_lua_err()
224        });
225        //TODO:
226        //methods.add_method("merge", |_, this, other: PartialLuaRockspec| {
227        //    this.merge(other).into_lua_err()
228        //});
229    }
230}
231
232impl HasIntegrity for PartialProjectToml {
233    fn hash(&self) -> io::Result<Integrity> {
234        let toml_file = self.project_root.join(PROJECT_TOML);
235        let content = std::fs::read_to_string(&toml_file)?;
236        Ok(Integrity::from(&content))
237    }
238}
239
240impl PartialProjectToml {
241    pub(crate) fn new(str: &str, project_root: ProjectRoot) -> Result<Self, toml::de::Error> {
242        Ok(Self {
243            project_root,
244            ..toml::from_str(str)?
245        })
246    }
247
248    /// Convert the `PartialProjectToml` struct into a `LocalProjectToml` struct, making
249    /// it ready to be used for building a project.
250    pub fn into_local(&self) -> Result<LocalProjectToml, LocalProjectTomlValidationError> {
251        let project_toml = self.clone();
252
253        // Disallow `lua` to be part of the `dependencies` field
254        if project_toml
255            .dependencies
256            .as_ref()
257            .is_some_and(|deps| deps.iter().any(|dep| dep.name() == &"lua".into()))
258        {
259            return Err(LocalProjectTomlValidationError::DependenciesContainLua);
260        }
261
262        let get_duplicates = |dependencies: &Option<Vec<LuaDependencySpec>>| {
263            dependencies
264                .iter()
265                .flat_map(|deps| {
266                    deps.iter()
267                        .map(|dep| dep.package_req().name())
268                        .duplicates()
269                        .cloned()
270                })
271                .collect_vec()
272        };
273        let duplicate_dependencies = get_duplicates(&self.dependencies);
274        if !duplicate_dependencies.is_empty() {
275            return Err(LocalProjectTomlValidationError::DuplicateDependencies(
276                PackageNameList::new(duplicate_dependencies),
277            ));
278        }
279        let duplicate_test_dependencies = get_duplicates(&self.test_dependencies);
280        if !duplicate_test_dependencies.is_empty() {
281            return Err(LocalProjectTomlValidationError::DuplicateTestDependencies(
282                PackageNameList::new(duplicate_test_dependencies),
283            ));
284        }
285        let duplicate_build_dependencies = get_duplicates(&self.build_dependencies);
286        if !duplicate_build_dependencies.is_empty() {
287            return Err(LocalProjectTomlValidationError::DuplicateBuildDependencies(
288                PackageNameList::new(duplicate_build_dependencies),
289            ));
290        }
291
292        let validated = LocalProjectToml {
293            internal: project_toml.clone(),
294
295            package: project_toml.package,
296            version: project_toml
297                .version_template
298                .try_generate(&self.project_root)
299                .unwrap_or(PackageVersion::default_dev_version()),
300            lua: project_toml
301                .lua
302                .ok_or(LocalProjectTomlValidationError::NoLuaVersion)?,
303            description: project_toml.description.unwrap_or_default(),
304            run: project_toml.run.map(PerPlatform::new),
305            supported_platforms: PlatformSupport::parse(
306                &project_toml
307                    .supported_platforms
308                    .unwrap_or_default()
309                    .into_iter()
310                    .map(|(platform, supported)| {
311                        if supported {
312                            format!("{}", platform)
313                        } else {
314                            format!("!{}", platform)
315                        }
316                    })
317                    .collect_vec(),
318            )?,
319            // Merge dependencies internally with lua version
320            // so the output of `dependencies()` is consistent
321            dependencies: PerPlatform::new(project_toml.dependencies.unwrap_or_default()),
322            build_dependencies: PerPlatform::new(
323                project_toml.build_dependencies.unwrap_or_default(),
324            ),
325            external_dependencies: PerPlatform::new(
326                project_toml.external_dependencies.unwrap_or_default(),
327            ),
328            test_dependencies: PerPlatform::new(project_toml.test_dependencies.unwrap_or_default()),
329            test: PerPlatform::new(TestSpec::from_platform_overridable(
330                project_toml.test.clone().unwrap_or_default(),
331            )?),
332            build: PerPlatform::new(BuildSpec::from_internal_spec(project_toml.build.clone())?),
333            deploy: PerPlatform::new(project_toml.deploy.clone().unwrap_or_default()),
334            rockspec_format: project_toml.rockspec_format.clone(),
335
336            source: PerPlatform::new(RemoteRockSource {
337                local: LocalRockSource::default(),
338                source_spec: RockSourceSpec::File(self.project_root.to_path_buf()),
339            }),
340        };
341
342        let rockspec_file_name = format!("{}-{}.rockspec", validated.package, validated.version);
343
344        if validated
345            .build
346            .default
347            .copy_directories
348            .contains(&PathBuf::from(&rockspec_file_name))
349        {
350            return Err(LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(None));
351        }
352
353        for (platform, build_override) in &validated.build.per_platform {
354            if build_override
355                .copy_directories
356                .contains(&PathBuf::from(&rockspec_file_name))
357            {
358                return Err(
359                    LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(Some(
360                        platform.to_string(),
361                    )),
362                );
363            }
364        }
365
366        Ok(validated)
367    }
368
369    /// Convert the `PartialProjectToml` struct into a `RemoteProjectToml` struct, making
370    /// it ready to be serialized into a rockspec.
371    /// A source must be provided for the rockspec to be valid.
372    pub fn into_remote(&self) -> Result<RemoteProjectToml, RemoteProjectTomlValidationError> {
373        let version = self.version_template.try_generate(&self.project_root)?;
374        let source =
375            self.source_template
376                .try_generate(&self.project_root, &self.package, &version)?;
377        let source = PerPlatform::new(
378            RemoteRockSource::from_platform_overridable(source).map_err(|err| {
379                RemoteProjectTomlValidationError::LocalProjectTomlValidationError(
380                    LocalProjectTomlValidationError::RockSourceError(err),
381                )
382            })?,
383        );
384        let local = self.into_local()?;
385
386        let validated = RemoteProjectToml { source, local };
387
388        Ok(validated)
389    }
390
391    // In the not-yet-validated struct, we create getters only
392    // for the non-optional fields.
393    pub fn package(&self) -> &PackageName {
394        &self.package
395    }
396
397    /// Returns the current package version, which may be generated from a template
398    pub fn version(&self) -> Result<PackageVersion, GenerateVersionError> {
399        self.version_template.try_generate(&self.project_root)
400    }
401
402    /// Merge the `ProjectToml` struct with an unvalidated `LuaRockspec`.
403    /// The final merged struct can then be validated.
404    pub fn merge(self, other: PartialLuaRockspec) -> Self {
405        PartialProjectToml {
406            package: other.package.unwrap_or(self.package),
407            version_template: self.version_template,
408            lua: other
409                .dependencies
410                .as_ref()
411                .and_then(|deps| {
412                    deps.iter()
413                        .find(|dep| dep.name() == &"lua".into())
414                        .and_then(|dep| {
415                            if dep.version_req().is_any() {
416                                None
417                            } else {
418                                Some(dep.version_req().clone())
419                            }
420                        })
421                })
422                .or(self.lua),
423            build: other.build.unwrap_or(self.build),
424            run: self.run,
425            description: other.description.or(self.description),
426            supported_platforms: other
427                .supported_platforms
428                .map(|platform_support| platform_support.platforms().clone())
429                .or(self.supported_platforms),
430            dependencies: other
431                .dependencies
432                .map(|deps| {
433                    deps.into_iter()
434                        .filter(|dep| dep.name() != &"lua".into())
435                        .collect()
436                })
437                .or(self.dependencies),
438            build_dependencies: other.build_dependencies.or(self.build_dependencies),
439            test_dependencies: other.test_dependencies.or(self.test_dependencies),
440            external_dependencies: other.external_dependencies.or(self.external_dependencies),
441            source_template: self.source_template,
442            test: other.test.or(self.test),
443            deploy: other.deploy.or(self.deploy),
444            rockspec_format: other.rockspec_format.or(self.rockspec_format),
445
446            // Keep the project root the same, as it is not part of the lua rockspec
447            project_root: self.project_root,
448        }
449    }
450}
451
452// This is automatically implemented for `RemoteProjectToml`,
453// but we also add a special implementation for `ProjectToml` (as providing a lua version
454// is required even by the non-validated struct).
455impl LuaVersionCompatibility for PartialProjectToml {
456    fn validate_lua_version(&self, config: &Config) -> Result<(), LuaVersionError> {
457        let _ = self.lua_version_matches(config)?;
458        Ok(())
459    }
460
461    fn lua_version_matches(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
462        let version = LuaVersion::from(config)?.clone();
463        if self.supports_lua_version(&version) {
464            Ok(version)
465        } else {
466            Err(LuaVersionError::LuaVersionUnsupported(
467                version,
468                self.package.clone(),
469                self.version_template
470                    .try_generate(&self.project_root)
471                    .unwrap_or(PackageVersion::default_dev_version()),
472            ))
473        }
474    }
475
476    fn supports_lua_version(&self, lua_version: &LuaVersion) -> bool {
477        self.lua
478            .as_ref()
479            .is_none_or(|lua| lua.matches(&lua_version.as_version()))
480    }
481
482    fn lua_version(&self) -> Option<LuaVersion> {
483        for (possibility, version) in [
484            ("5.4.0", LuaVersion::Lua54),
485            ("5.3.0", LuaVersion::Lua53),
486            ("5.2.0", LuaVersion::Lua52),
487            ("5.1.0", LuaVersion::Lua51),
488        ] {
489            if self
490                .lua
491                .as_ref()
492                .is_none_or(|lua| lua.matches(&possibility.parse().unwrap()))
493            {
494                return Some(version);
495            }
496        }
497        None
498    }
499}
500
501// TODO(vhyrro): Move this struct into a different directory.
502#[derive(Debug, Clone, Deserialize)]
503pub struct RunSpec {
504    /// The command to execute when running the project
505    pub(crate) command: Option<RunCommand>,
506    /// Arguments to pass to the command
507    pub(crate) args: Option<NonEmpty<String>>,
508}
509
510/// The `lux.toml` file, after being properly deserialized.
511/// This struct may be used to build a local version of a project.
512/// To build a rockspec, use `RemoteProjectToml`.
513#[derive(Debug)]
514pub struct LocalProjectToml {
515    package: PackageName,
516    version: PackageVersion,
517    lua: PackageVersionReq,
518    rockspec_format: Option<RockspecFormat>,
519    run: Option<PerPlatform<RunSpec>>,
520    description: RockDescription,
521    supported_platforms: PlatformSupport,
522    dependencies: PerPlatform<Vec<LuaDependencySpec>>,
523    build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
524    external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
525    test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
526    test: PerPlatform<TestSpec>,
527    build: PerPlatform<BuildSpec>,
528    deploy: PerPlatform<DeploySpec>,
529
530    // Used for simpler serialization
531    internal: PartialProjectToml,
532
533    /// A source pointing to the current project's root.
534    source: PerPlatform<RemoteRockSource>,
535}
536
537impl LocalProjectToml {
538    pub fn run(&self) -> Option<&PerPlatform<RunSpec>> {
539        self.run.as_ref()
540    }
541
542    /// Convert this project TOML to a Lua rockspec.
543    /// Fails if there is no valid project root or if there are off-spec dependencies.
544    pub fn to_lua_rockspec(&self) -> Result<LocalLuaRockspec, LuaRockspecError> {
545        if let Some(dep) = self
546            .dependencies()
547            .per_platform
548            .iter()
549            .filter_map(|(_, deps)| deps.iter().find(|dep| dep.source().is_some()))
550            .collect_vec()
551            .first()
552        {
553            return Err(LuaRockspecError::OffSpecDependency(dep.name().clone()));
554        }
555        if let Some(dep) = self
556            .build_dependencies()
557            .per_platform
558            .iter()
559            .filter_map(|(_, deps)| deps.iter().find(|dep| dep.source().is_some()))
560            .collect_vec()
561            .first()
562        {
563            return Err(LuaRockspecError::OffSpecBuildDependency(dep.name().clone()));
564        }
565        if let Some(dep) = self
566            .test_dependencies()
567            .per_platform
568            .iter()
569            .filter_map(|(_, deps)| deps.iter().find(|dep| dep.source().is_some()))
570            .collect_vec()
571            .first()
572        {
573            return Err(LuaRockspecError::OffSpecTestDependency(dep.name().clone()));
574        }
575        LocalLuaRockspec::new(
576            &self.to_lua_remote_rockspec_string()?,
577            self.internal.project_root.clone(),
578        )
579    }
580}
581
582impl Rockspec for LocalProjectToml {
583    type Error = ProjectTomlError;
584
585    fn package(&self) -> &PackageName {
586        &self.package
587    }
588
589    fn version(&self) -> &PackageVersion {
590        &self.version
591    }
592
593    fn description(&self) -> &RockDescription {
594        &self.description
595    }
596
597    fn supported_platforms(&self) -> &PlatformSupport {
598        &self.supported_platforms
599    }
600
601    fn lua(&self) -> &PackageVersionReq {
602        &self.lua
603    }
604
605    fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
606        &self.dependencies
607    }
608
609    fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
610        &self.build_dependencies
611    }
612
613    fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
614        &self.external_dependencies
615    }
616
617    fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
618        &self.test_dependencies
619    }
620
621    fn build(&self) -> &PerPlatform<BuildSpec> {
622        &self.build
623    }
624
625    fn test(&self) -> &PerPlatform<TestSpec> {
626        &self.test
627    }
628
629    fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
630        &mut self.build
631    }
632
633    fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
634        &mut self.test
635    }
636
637    fn format(&self) -> &Option<RockspecFormat> {
638        &self.rockspec_format
639    }
640
641    fn source(&self) -> &PerPlatform<RemoteRockSource> {
642        &self.source
643    }
644
645    fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
646        &mut self.source
647    }
648
649    fn deploy(&self) -> &PerPlatform<DeploySpec> {
650        &self.deploy
651    }
652
653    fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
654        &mut self.deploy
655    }
656
657    fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
658        let project_root = &self.internal.project_root;
659        let version = self.internal.version_template.try_generate(project_root)?;
660        let starter = format!(
661            r#"
662rockspec_format = "{}"
663package = "{}"
664version = "{}""#,
665            self.rockspec_format.as_ref().unwrap_or(&"3.0".into()),
666            self.package,
667            &version
668        );
669
670        let mut template = Vec::new();
671
672        if self.description != RockDescription::default() {
673            template.push(self.description.display_lua());
674        }
675
676        if self.supported_platforms != PlatformSupport::default() {
677            template.push(self.supported_platforms.display_lua());
678        }
679
680        {
681            let mut dependencies = self.internal.dependencies.clone().unwrap_or_default();
682            dependencies.insert(
683                0,
684                PackageReq {
685                    name: "lua".into(),
686                    version_req: self.lua.clone(),
687                }
688                .into(),
689            );
690            template.push(Dependencies(&dependencies).display_lua());
691        }
692
693        match self.internal.build_dependencies {
694            Some(ref build_dependencies) if !build_dependencies.is_empty() => {
695                template.push(BuildDependencies(build_dependencies).display_lua());
696            }
697            _ => {}
698        }
699
700        match self.internal.external_dependencies {
701            Some(ref external_dependencies) if !external_dependencies.is_empty() => {
702                template.push(ExternalDependencies(external_dependencies).display_lua());
703            }
704            _ => {}
705        }
706
707        match self.internal.test_dependencies {
708            Some(ref test_dependencies) if !test_dependencies.is_empty() => {
709                template.push(TestDependencies(test_dependencies).display_lua());
710            }
711            _ => {}
712        }
713
714        let source =
715            self.internal
716                .source_template
717                .try_generate(project_root, &self.package, &version)?;
718        template.push(source.display_lua());
719
720        if let Some(ref test) = self.internal.test {
721            template.push(test.display_lua());
722        }
723
724        template.push(self.internal.build.display_lua());
725
726        Ok(std::iter::once(starter)
727            .chain(template.into_iter().map(|kv| kv.to_string()))
728            .join("\n\n"))
729    }
730}
731
732#[derive(Error, Debug)]
733#[error(transparent)]
734pub enum ProjectTomlIntegrityError {
735    LuaRockspecError(#[from] LuaRockspecError),
736    IoError(#[from] io::Error),
737}
738
739impl HasIntegrity for LocalProjectToml {
740    fn hash(&self) -> io::Result<Integrity> {
741        match self.to_lua_rockspec() {
742            Ok(lua_rockspec) => lua_rockspec.hash(),
743            Err(_) => self.internal.hash(),
744        }
745    }
746}
747
748#[derive(Debug)]
749pub struct RemoteProjectToml {
750    local: LocalProjectToml,
751    source: PerPlatform<RemoteRockSource>,
752}
753
754impl RemoteProjectToml {
755    pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
756        RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
757    }
758}
759
760impl Rockspec for RemoteProjectToml {
761    type Error = ProjectTomlError;
762
763    fn package(&self) -> &PackageName {
764        self.local.package()
765    }
766
767    fn version(&self) -> &PackageVersion {
768        self.local.version()
769    }
770
771    fn description(&self) -> &RockDescription {
772        self.local.description()
773    }
774
775    fn supported_platforms(&self) -> &PlatformSupport {
776        self.local.supported_platforms()
777    }
778
779    fn lua(&self) -> &PackageVersionReq {
780        self.local.lua()
781    }
782
783    fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
784        self.local.dependencies()
785    }
786
787    fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
788        self.local.build_dependencies()
789    }
790
791    fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
792        self.local.external_dependencies()
793    }
794
795    fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
796        self.local.test_dependencies()
797    }
798
799    fn build(&self) -> &PerPlatform<BuildSpec> {
800        self.local.build()
801    }
802
803    fn test(&self) -> &PerPlatform<TestSpec> {
804        self.local.test()
805    }
806
807    fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
808        self.local.build_mut()
809    }
810
811    fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
812        self.local.test_mut()
813    }
814
815    fn format(&self) -> &Option<RockspecFormat> {
816        self.local.format()
817    }
818
819    fn source(&self) -> &PerPlatform<RemoteRockSource> {
820        &self.source
821    }
822
823    fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
824        &mut self.source
825    }
826
827    fn deploy(&self) -> &PerPlatform<DeploySpec> {
828        self.local.deploy()
829    }
830
831    fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
832        self.local.deploy_mut()
833    }
834
835    fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
836        let project_root = &self.local.internal.project_root;
837        let version = self
838            .local
839            .internal
840            .version_template
841            .try_generate(project_root)?;
842
843        let starter = format!(
844            r#"
845rockspec_format = "{}"
846package = "{}"
847version = "{}""#,
848            self.local.rockspec_format.as_ref().unwrap_or(&"3.0".into()),
849            self.local.package,
850            &version
851        );
852
853        let mut template = Vec::new();
854
855        if self.local.description != RockDescription::default() {
856            template.push(self.local.description.display_lua());
857        }
858
859        if self.local.supported_platforms != PlatformSupport::default() {
860            template.push(self.local.supported_platforms.display_lua());
861        }
862
863        {
864            let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
865            dependencies.insert(
866                0,
867                PackageReq {
868                    name: "lua".into(),
869                    version_req: self.local.lua.clone(),
870                }
871                .into(),
872            );
873            template.push(Dependencies(&dependencies).display_lua());
874        }
875
876        match self.local.internal.build_dependencies {
877            Some(ref build_dependencies) if !build_dependencies.is_empty() => {
878                template.push(BuildDependencies(build_dependencies).display_lua());
879            }
880            _ => {}
881        }
882
883        match self.local.internal.external_dependencies {
884            Some(ref external_dependencies) if !external_dependencies.is_empty() => {
885                template.push(ExternalDependencies(external_dependencies).display_lua());
886            }
887            _ => {}
888        }
889
890        match self.local.internal.test_dependencies {
891            Some(ref test_dependencies) if !test_dependencies.is_empty() => {
892                template.push(TestDependencies(test_dependencies).display_lua());
893            }
894            _ => {}
895        }
896
897        let source = self.local.internal.source_template.try_generate(
898            project_root,
899            &self.local.internal.package,
900            &version,
901        )?;
902        template.push(source.display_lua());
903
904        if let Some(ref test) = self.local.internal.test {
905            template.push(test.display_lua());
906        }
907
908        template.push(self.local.internal.build.display_lua());
909
910        Ok(std::iter::once(starter)
911            .chain(template.into_iter().map(|kv| kv.to_string()))
912            .join("\n\n"))
913    }
914}
915
916impl HasIntegrity for RemoteProjectToml {
917    fn hash(&self) -> io::Result<Integrity> {
918        self.to_lua_rockspec()
919            .expect("unable to convert remote project to rockspec")
920            .hash()
921    }
922}
923
924impl UserData for LocalProjectToml {
925    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
926        methods.add_method("package", |_, this, _: ()| Ok(this.package().clone()));
927        methods.add_method("version", |_, this, _: ()| Ok(this.version().clone()));
928        methods.add_method("description", |_, this, _: ()| {
929            Ok(this.description().clone())
930        });
931        methods.add_method("supported_platforms", |_, this, _: ()| {
932            Ok(this.supported_platforms().clone())
933        });
934        methods.add_method("dependencies", |_, this, _: ()| {
935            Ok(this.dependencies().clone())
936        });
937        methods.add_method("build_dependencies", |_, this, _: ()| {
938            Ok(this.build_dependencies().clone())
939        });
940        methods.add_method("external_dependencies", |_, this, _: ()| {
941            Ok(this.external_dependencies().clone())
942        });
943        methods.add_method("test_dependencies", |_, this, _: ()| {
944            Ok(this.test_dependencies().clone())
945        });
946        methods.add_method("build", |_, this, _: ()| Ok(this.build().clone()));
947        methods.add_method("test", |_, this, _: ()| Ok(this.test().clone()));
948        methods.add_method("format", |_, this, _: ()| Ok(this.format().clone()));
949        methods.add_method("source", |_, this, _: ()| Ok(this.source().clone()));
950        methods.add_method("to_lua_rockspec_string", |_, this, _: ()| {
951            this.to_lua_remote_rockspec_string()
952                .map_err(|err| mlua::Error::RuntimeError(err.to_string()))
953        });
954        methods.add_method("to_lua_rockspec", |_, this, _: ()| {
955            this.to_lua_rockspec().into_lua_err()
956        });
957    }
958}
959
960impl UserData for RemoteProjectToml {
961    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
962        methods.add_method("package", |_, this, _: ()| Ok(this.package().clone()));
963        methods.add_method("version", |_, this, _: ()| Ok(this.version().clone()));
964        methods.add_method("description", |_, this, _: ()| {
965            Ok(this.description().clone())
966        });
967        methods.add_method("supported_platforms", |_, this, _: ()| {
968            Ok(this.supported_platforms().clone())
969        });
970        methods.add_method("dependencies", |_, this, _: ()| {
971            Ok(this.dependencies().clone())
972        });
973        methods.add_method("build_dependencies", |_, this, _: ()| {
974            Ok(this.build_dependencies().clone())
975        });
976        methods.add_method("external_dependencies", |_, this, _: ()| {
977            Ok(this.external_dependencies().clone())
978        });
979        methods.add_method("test_dependencies", |_, this, _: ()| {
980            Ok(this.test_dependencies().clone())
981        });
982        methods.add_method("build", |_, this, _: ()| Ok(this.build().clone()));
983        methods.add_method("test", |_, this, _: ()| Ok(this.test().clone()));
984        methods.add_method("format", |_, this, _: ()| Ok(this.format().clone()));
985        methods.add_method("source", |_, this, _: ()| Ok(this.source().clone()));
986        methods.add_method("to_lua_rockspec_string", |_, this, _: ()| {
987            this.to_lua_remote_rockspec_string()
988                .map_err(|err| mlua::Error::RuntimeError(err.to_string()))
989        });
990        methods.add_method("to_lua_rockspec", |_, this, _: ()| {
991            this.to_lua_rockspec().into_lua_err()
992        });
993    }
994}
995
996#[cfg(test)]
997mod tests {
998    use std::path::PathBuf;
999
1000    use assert_fs::prelude::PathCopy;
1001    use git2::{Repository, RepositoryInitOptions};
1002    use git_url_parse::GitUrl;
1003    use url::Url;
1004
1005    use crate::{
1006        git::GitSource,
1007        lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1008        project::{Project, ProjectRoot},
1009        rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1010    };
1011
1012    use super::PartialProjectToml;
1013
1014    #[test]
1015    fn project_toml_parsing() {
1016        let project_toml = r#"
1017        package = "my-package"
1018        version = "1.0.0"
1019        lua = "5.3"
1020
1021        rockspec_format = "1.0"
1022
1023        [source]
1024        url = "https://example.com"
1025
1026        [dependencies]
1027        foo = "1.0"
1028        bar = ">=2.0"
1029
1030        [run]
1031        args = ["--foo", "--bar"]
1032
1033        [build]
1034        type = "builtin"
1035        "#;
1036
1037        let project = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1038        let _ = project.into_remote().unwrap();
1039
1040        let project_toml = r#"
1041        package = "my-package"
1042        version = "1.0.0"
1043        lua = "5.1"
1044
1045        [description]
1046        summary = "A summary"
1047        detailed = "A detailed description"
1048        license = "MIT"
1049        homepage = "https://example.com"
1050        issues_url = "https://example.com/issues"
1051        maintainer = "John Doe"
1052        labels = ["label1", "label2"]
1053
1054        [supported_platforms]
1055        linux = true
1056        windows = false
1057
1058        [dependencies]
1059        foo = "1.0"
1060        bar = ">=2.0"
1061
1062        [build_dependencies]
1063        baz = "1.0"
1064
1065        [external_dependencies.foo]
1066        header = "foo.h"
1067
1068        [external_dependencies.bar]
1069        library = "libbar.so"
1070
1071        [test_dependencies]
1072        busted = "69.420"
1073
1074        [source]
1075        url = "https://example.com"
1076        hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1077        file = "my-package-1.0.0.tar.gz"
1078        dir = "my-package-1.0.0"
1079
1080        [test]
1081        type = "command"
1082        script = "test.lua"
1083        flags = [ "foo", "bar" ]
1084
1085        [run]
1086        command = "my-command"
1087        args = ["--foo", "--bar"]
1088
1089        [build]
1090        type = "builtin"
1091        "#;
1092
1093        let project = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1094        let _ = project.into_remote().unwrap();
1095    }
1096
1097    #[test]
1098    fn compare_project_toml_with_rockspec() {
1099        let project_toml = r#"
1100        package = "my-package"
1101        version = "1.0.0"
1102        lua = "5.1"
1103
1104        # For testing, specify a custom rockspec format
1105        # (defaults to 3.0)
1106        rockspec_format = "1.0"
1107
1108        [description]
1109        summary = "A summary"
1110        detailed = "A detailed description"
1111        license = "MIT"
1112        homepage = "https://example.com"
1113        issues_url = "https://example.com/issues"
1114        maintainer = "John Doe"
1115        labels = ["label1", "label2"]
1116
1117        [supported_platforms]
1118        linux = true
1119        windows = false
1120
1121        [dependencies]
1122        foo = "1.0"
1123        bar = ">=2.0"
1124
1125        [build_dependencies]
1126        baz = "1.0"
1127
1128        [external_dependencies.foo]
1129        header = "foo.h"
1130
1131        [external_dependencies.bar]
1132        library = "libbar.so"
1133
1134        [test_dependencies]
1135        busted = "1.0"
1136
1137        [source]
1138        url = "https://example.com"
1139        file = "my-package-1.0.0.tar.gz"
1140        dir = "my-package-1.0.0"
1141
1142        [test]
1143        type = "command"
1144        script = "test.lua"
1145        flags = [ "foo", "bar" ]
1146
1147        [run]
1148        command = "my-command"
1149        args = ["--foo", "--bar"]
1150
1151        [build]
1152        type = "builtin"
1153        "#;
1154
1155        let expected_rockspec = r#"
1156            rockspec_format = "1.0"
1157            package = "my-package"
1158            version = "1.0.0"
1159
1160            source = {
1161                url = "https://example.com",
1162                file = "my-package-1.0.0.tar.gz",
1163                dir = "my-package-1.0.0",
1164            }
1165
1166            description = {
1167                summary = "A summary",
1168                detailed = "A detailed description",
1169                license = "MIT",
1170                homepage = "https://example.com",
1171                issues_url = "https://example.com/issues",
1172                maintainer = "John Doe",
1173                labels = {"label1", "label2"},
1174            }
1175
1176            supported_platforms = {"linux", "!windows"}
1177
1178            dependencies = {
1179                "lua ==5.1",
1180                "foo ==1.0",
1181                "bar >=2.0",
1182            }
1183
1184            build_dependencies = {
1185                "baz ==1.0",
1186            }
1187
1188            external_dependencies = {
1189                foo = { header = "foo.h" },
1190                bar = { library = "libbar.so" },
1191            }
1192
1193            test_dependencies = {
1194                "busted ==1.0",
1195            }
1196
1197            source = {
1198                url = "https://example.com",
1199                hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1200                file = "my-package-1.0.0.tar.gz",
1201                dir = "my-package-1.0.0",
1202            }
1203
1204            test = {
1205                type = "command",
1206                script = "test.lua",
1207                flags = {"foo", "bar"},
1208            }
1209
1210            build = {
1211                type = "builtin",
1212            }
1213        "#;
1214
1215        let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1216
1217        let project_toml = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1218        let rockspec = project_toml
1219            .into_remote()
1220            .unwrap()
1221            .to_lua_rockspec()
1222            .unwrap();
1223
1224        let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1225            let mut v = v.current_platform().clone();
1226            v.sort_by(|a, b| a.name().cmp(b.name()));
1227            v
1228        };
1229
1230        assert_eq!(rockspec.package(), expected_rockspec.package());
1231        assert_eq!(rockspec.version(), expected_rockspec.version());
1232        assert_eq!(rockspec.description(), expected_rockspec.description());
1233        assert_eq!(
1234            rockspec.supported_platforms(),
1235            expected_rockspec.supported_platforms()
1236        );
1237        assert_eq!(
1238            sorted_package_reqs(rockspec.dependencies()),
1239            sorted_package_reqs(expected_rockspec.dependencies())
1240        );
1241        assert_eq!(
1242            sorted_package_reqs(rockspec.build_dependencies()),
1243            sorted_package_reqs(expected_rockspec.build_dependencies())
1244        );
1245        assert_eq!(
1246            rockspec.external_dependencies(),
1247            expected_rockspec.external_dependencies()
1248        );
1249        assert_eq!(
1250            sorted_package_reqs(rockspec.test_dependencies()),
1251            sorted_package_reqs(expected_rockspec.test_dependencies())
1252        );
1253        assert_eq!(rockspec.source(), expected_rockspec.source());
1254        assert_eq!(rockspec.test(), expected_rockspec.test());
1255        assert_eq!(rockspec.build(), expected_rockspec.build());
1256        assert_eq!(rockspec.format(), expected_rockspec.format());
1257    }
1258
1259    #[test]
1260    fn merge_project_toml_with_partial_rockspec() {
1261        let project_toml = r#"
1262        package = "my-package"
1263        version = "1.0.0"
1264        lua = "5.1"
1265
1266        # For testing, specify a custom rockspec format
1267        # (defaults to 3.0)
1268        rockspec_format = "1.0"
1269
1270        [description]
1271        summary = "A summary"
1272        detailed = "A detailed description"
1273        license = "MIT"
1274        homepage = "https://example.com"
1275        issues_url = "https://example.com/issues"
1276        maintainer = "John Doe"
1277        labels = ["label1", "label2"]
1278
1279        [supported_platforms]
1280        linux = true
1281        windows = false
1282
1283        [dependencies]
1284        foo = "1.0"
1285        bar = ">=2.0"
1286
1287        [build_dependencies]
1288        baz = "1.0"
1289
1290        [external_dependencies.foo]
1291        header = "foo.h"
1292
1293        [external_dependencies.bar]
1294        library = "libbar.so"
1295
1296        [test_dependencies]
1297        busted = "1.0"
1298
1299        [source]
1300        url = "https://example.com"
1301        file = "my-package-1.0.0.tar.gz"
1302        dir = "my-package-1.0.0"
1303
1304        [test]
1305        type = "command"
1306        script = "test.lua"
1307        flags = [ "foo", "bar" ]
1308
1309        [run]
1310        command = "my-command"
1311        args = [ "--foo", "--bar" ]
1312
1313        [build]
1314        type = "builtin"
1315        "#;
1316
1317        let mergable_rockspec_content = r#"
1318            rockspec_format = "1.0"
1319            package = "my-package-overwritten"
1320
1321            description = {
1322                summary = "A summary overwritten",
1323                detailed = "A detailed description overwritten",
1324                license = "GPL-2.0",
1325                homepage = "https://example.com/overwritten",
1326                issues_url = "https://example.com/issues/overwritten",
1327                maintainer = "John Doe Overwritten",
1328                labels = {"over", "written"},
1329            }
1330
1331            -- Inverted supported platforms
1332            supported_platforms = {"!linux", "windows"}
1333
1334            dependencies = {
1335                "lua 5.1",
1336                "foo >1.0",
1337                "bar <=2.0",
1338            }
1339
1340            build_dependencies = {
1341                "baz >1.0",
1342            }
1343
1344            external_dependencies = {
1345                foo = { header = "overwritten.h" },
1346                bar = { library = "overwritten.so" },
1347            }
1348
1349            test_dependencies = {
1350                "busted >1.0",
1351            }
1352
1353            test = {
1354                type = "command",
1355                script = "overwritten.lua",
1356                flags = {"over", "written"},
1357            }
1358
1359            build = {
1360                type = "builtin",
1361            }
1362        "#;
1363
1364        let remote_rockspec_content = format!(
1365            r#"{}
1366            version = "1.0.0"
1367            source = {{
1368                url = "https://example.com",
1369                file = "my-package-1.0.0.tar.gz",
1370                dir = "my-package-1.0.0",
1371            }}
1372        "#,
1373            &mergable_rockspec_content
1374        );
1375
1376        let project_toml = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1377        let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1378        let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1379
1380        let merged = project_toml.merge(partial_rockspec).into_remote().unwrap();
1381
1382        let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1383            let mut v = v.current_platform().clone();
1384            v.sort_by(|a, b| a.name().cmp(b.name()));
1385            v
1386        };
1387
1388        assert_eq!(merged.package(), expected_rockspec.package());
1389        assert_eq!(merged.version(), expected_rockspec.version());
1390        assert_eq!(merged.description(), expected_rockspec.description());
1391        assert_eq!(
1392            merged.supported_platforms(),
1393            expected_rockspec.supported_platforms()
1394        );
1395        assert_eq!(
1396            sorted_package_reqs(merged.dependencies()),
1397            sorted_package_reqs(expected_rockspec.dependencies())
1398        );
1399        assert_eq!(
1400            sorted_package_reqs(merged.build_dependencies()),
1401            sorted_package_reqs(expected_rockspec.build_dependencies())
1402        );
1403        assert_eq!(
1404            merged.external_dependencies(),
1405            expected_rockspec.external_dependencies()
1406        );
1407        assert_eq!(
1408            sorted_package_reqs(merged.test_dependencies()),
1409            sorted_package_reqs(expected_rockspec.test_dependencies())
1410        );
1411        assert_eq!(merged.source(), expected_rockspec.source());
1412        assert_eq!(merged.test(), expected_rockspec.test());
1413        assert_eq!(merged.build(), expected_rockspec.build());
1414        assert_eq!(merged.format(), expected_rockspec.format());
1415        // Ensure that the run command is retained after merge.
1416        assert!(merged.local.run().is_some());
1417    }
1418
1419    #[test]
1420    fn project_toml_with_lua_in_dependencies() {
1421        let project_toml = r#"
1422        package = "my-package"
1423        version = "1.0.0"
1424        # lua = ">5.1"
1425
1426        [dependencies]
1427        lua = "5.1" # disallowed
1428
1429        [build]
1430        type = "builtin"
1431        "#;
1432
1433        PartialProjectToml::new(project_toml, ProjectRoot::default())
1434            .unwrap()
1435            .into_local()
1436            .unwrap_err();
1437    }
1438
1439    #[test]
1440    fn project_toml_with_invalid_run_command() {
1441        for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1442            let project_toml = format!(
1443                r#"
1444                package = "my-package"
1445                version = "1.0.0"
1446                lua = "5.1"
1447
1448                [build]
1449                type = "builtin"
1450
1451                [run]
1452                command = "{command}"
1453                "#,
1454            );
1455
1456            PartialProjectToml::new(&project_toml, ProjectRoot::default()).unwrap_err();
1457        }
1458    }
1459
1460    #[test]
1461    fn generate_non_deterministic_git_source() {
1462        let rockspec_content = r#"
1463            package = "test-package"
1464            version = "1.0.0"
1465            lua = ">=5.1"
1466
1467            [source]
1468            url = "git+https://exaple.com/repo.git"
1469
1470            [build]
1471            type = "builtin"
1472        "#;
1473
1474        PartialProjectToml::new(rockspec_content, ProjectRoot::default())
1475            .unwrap()
1476            .into_remote()
1477            .unwrap_err();
1478    }
1479
1480    #[test]
1481    fn generate_deterministic_git_source() {
1482        let rockspec_content = r#"
1483            package = "test-package"
1484            version = "1.0.0"
1485            lua = ">=5.1"
1486
1487            [source]
1488            url = "git+https://exaple.com/repo.git"
1489            tag = "v0.1.0"
1490
1491            [build]
1492            type = "builtin"
1493        "#;
1494
1495        PartialProjectToml::new(rockspec_content, ProjectRoot::default())
1496            .unwrap()
1497            .into_remote()
1498            .unwrap();
1499    }
1500
1501    fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
1502        let sample_project: PathBuf = "resources/test/sample-project-source-template/".into();
1503        temp_dir.copy_from(&sample_project, &["**"]).unwrap();
1504        let repo = Repository::init(temp_dir).unwrap();
1505        let mut opts = RepositoryInitOptions::new();
1506        opts.initial_head("main");
1507        {
1508            let mut config = repo.config().unwrap();
1509            config.set_str("user.name", "name").unwrap();
1510            config.set_str("user.email", "email").unwrap();
1511            let mut index = repo.index().unwrap();
1512            let id = index.write_tree().unwrap();
1513
1514            let tree = repo.find_tree(id).unwrap();
1515            let sig = repo.signature().unwrap();
1516            repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
1517                .unwrap();
1518        }
1519        repo
1520    }
1521
1522    fn create_tag(repo: &Repository, name: &str) {
1523        let sig = repo.signature().unwrap();
1524        let id = repo.head().unwrap().target().unwrap();
1525        let obj = repo.find_object(id, None).unwrap();
1526        repo.tag(name, &obj, &sig, "msg", true).unwrap();
1527    }
1528
1529    #[test]
1530    fn test_git_project_generate_dev_source() {
1531        let project_root = assert_fs::TempDir::new().unwrap();
1532        init_sample_project_repo(&project_root);
1533        let project = Project::from(&project_root).unwrap().unwrap();
1534        let remote_project_toml = project.toml().into_remote().unwrap();
1535        let source_spec = &remote_project_toml.source.current_platform().source_spec;
1536        assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1537        if let RockSourceSpec::Git(GitSource { url, checkout_ref }) = source_spec {
1538            let expected_url: GitUrl = "https://github.com/nvim-neorocks/lux.git".parse().unwrap();
1539            assert_eq!(url, &expected_url);
1540            assert!(checkout_ref.is_some());
1541        }
1542    }
1543
1544    #[test]
1545    fn test_git_project_generate_non_semver_tag_source() {
1546        let project_root = assert_fs::TempDir::new().unwrap();
1547        let repo = init_sample_project_repo(&project_root);
1548        let tag_name = "bla";
1549        create_tag(&repo, tag_name);
1550        let project = Project::from(&project_root).unwrap().unwrap();
1551        let remote_project_toml = project.toml().into_remote().unwrap();
1552        let source_spec = &remote_project_toml.source.current_platform().source_spec;
1553        assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1554        if let RockSourceSpec::Git(GitSource { url, checkout_ref }) = source_spec {
1555            let expected_url: GitUrl = "https://github.com/nvim-neorocks/lux.git".parse().unwrap();
1556            assert_eq!(url, &expected_url);
1557            assert_eq!(checkout_ref, &Some(tag_name.to_string()));
1558        }
1559    }
1560
1561    #[test]
1562    fn test_git_project_generate_release_source_tag_with_v_prefix() {
1563        let project_root = assert_fs::TempDir::new().unwrap();
1564        let repo = init_sample_project_repo(&project_root);
1565        let tag_name = "v1.0.0";
1566        create_tag(&repo, "bla");
1567        create_tag(&repo, tag_name);
1568        let project = Project::from(&project_root).unwrap().unwrap();
1569        let remote_project_toml = project.toml().into_remote().unwrap();
1570        let source_spec = &remote_project_toml.source.current_platform().source_spec;
1571        assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1572        if let RockSourceSpec::Url(url) = source_spec {
1573            let expected_url: Url =
1574                "https://github.com/nvim-neorocks/lux/archive/refs/tags/v1.0.0.zip"
1575                    .parse()
1576                    .unwrap();
1577            assert_eq!(url, &expected_url);
1578        }
1579    }
1580
1581    #[test]
1582    fn test_git_project_generate_release_source_tag_without_v_prefix() {
1583        let project_root = assert_fs::TempDir::new().unwrap();
1584        let repo = init_sample_project_repo(&project_root);
1585        create_tag(&repo, "bla");
1586        let tag_name = "1.0.0";
1587        create_tag(&repo, tag_name);
1588        let project = Project::from(&project_root).unwrap().unwrap();
1589        let remote_project_toml = project.toml().into_remote().unwrap();
1590        let source_spec = &remote_project_toml.source.current_platform().source_spec;
1591        assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1592        if let RockSourceSpec::Url(url) = source_spec {
1593            let expected_url: Url =
1594                "https://github.com/nvim-neorocks/lux/archive/refs/tags/1.0.0.zip"
1595                    .parse()
1596                    .unwrap();
1597            assert_eq!(url, &expected_url);
1598        }
1599    }
1600}