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