Skip to main content

lux_lib/project/
project_toml.rs

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