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