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