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/// The `lux.toml`, after being validated and prepared for upload
845#[derive(Debug)]
846pub struct RemoteProjectToml {
847    local: LocalProjectToml,
848    source: PerPlatform<RemoteRockSource>,
849}
850
851impl RemoteProjectToml {
852    pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
853        RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
854    }
855}
856
857impl Rockspec for RemoteProjectToml {
858    type Error = ProjectTomlError;
859
860    fn package(&self) -> &PackageName {
861        self.local.package()
862    }
863
864    fn version(&self) -> &PackageVersion {
865        self.local.version()
866    }
867
868    fn description(&self) -> &RockDescription {
869        self.local.description()
870    }
871
872    fn supported_platforms(&self) -> &PlatformSupport {
873        self.local.supported_platforms()
874    }
875
876    fn lua(&self) -> &PackageVersionReq {
877        self.local.lua()
878    }
879
880    fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
881        self.local.dependencies()
882    }
883
884    fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
885        self.local.build_dependencies()
886    }
887
888    fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
889        self.local.external_dependencies()
890    }
891
892    fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
893        self.local.test_dependencies()
894    }
895
896    fn build(&self) -> &PerPlatform<BuildSpec> {
897        self.local.build()
898    }
899
900    fn test(&self) -> &PerPlatform<TestSpec> {
901        self.local.test()
902    }
903
904    fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
905        self.local.build_mut()
906    }
907
908    fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
909        self.local.test_mut()
910    }
911
912    fn format(&self) -> &Option<RockspecFormat> {
913        self.local.format()
914    }
915
916    fn source(&self) -> &PerPlatform<RemoteRockSource> {
917        &self.source
918    }
919
920    fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
921        &mut self.source
922    }
923
924    fn deploy(&self) -> &PerPlatform<DeploySpec> {
925        self.local.deploy()
926    }
927
928    fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
929        self.local.deploy_mut()
930    }
931
932    fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
933        let project_root = &self.local.internal.project_root;
934        let starter = format!(
935            r#"
936rockspec_format = "{}"
937package = "{}"
938version = "{}""#,
939            self.local
940                .rockspec_format
941                .as_ref()
942                .unwrap_or(&RockspecFormat::default()),
943            self.local.package,
944            self.version()
945        );
946
947        let mut template = Vec::new();
948
949        if self.local.description != RockDescription::default() {
950            template.push(self.local.description.display_lua());
951        }
952
953        if self.local.supported_platforms != PlatformSupport::default() {
954            template.push(self.local.supported_platforms.display_lua());
955        }
956
957        {
958            let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
959            dependencies.insert(
960                0,
961                PackageReq {
962                    name: "lua".into(),
963                    version_req: self.local.lua.clone(),
964                }
965                .into(),
966            );
967            template.push(Dependencies(&dependencies).display_lua());
968        }
969
970        let mut build_dependencies = self
971            .local
972            .internal
973            .build_dependencies
974            .as_ref()
975            .cloned()
976            .unwrap_or_default();
977
978        let build_backend_dependency = self
979            .local
980            .internal
981            .build
982            .build_type
983            .as_ref()
984            .and_then(|build_type| build_type.luarocks_build_backend());
985
986        if let Some(build_backend_dependency) = build_backend_dependency {
987            build_dependencies.push(build_backend_dependency);
988        }
989
990        if !build_dependencies.is_empty() {
991            template.push(BuildDependencies(&build_dependencies).display_lua());
992        }
993
994        match self.local.internal.external_dependencies {
995            Some(ref external_dependencies) if !external_dependencies.is_empty() => {
996                template.push(ExternalDependencies(external_dependencies).display_lua());
997            }
998            _ => {}
999        }
1000
1001        let source = self.local.internal.source_template.try_generate(
1002            project_root,
1003            &self.local.internal.package,
1004            self.version(),
1005        )?;
1006        template.push(source.display_lua());
1007
1008        if let Some(ref deploy) = self.local.internal.deploy {
1009            template.push(deploy.display_lua());
1010        }
1011
1012        template.push(self.local.internal.build.display_lua());
1013
1014        let unformatted_code = std::iter::once(starter)
1015            .chain(template.into_iter().map(|kv| kv.to_string()))
1016            .join("\n\n");
1017        let result = match stylua_lib::format_code(
1018            &unformatted_code,
1019            stylua_lib::Config::default(),
1020            None,
1021            stylua_lib::OutputVerification::Full,
1022        ) {
1023            Ok(formatted_code) => formatted_code,
1024            Err(_) => unformatted_code,
1025        };
1026        validate_generated_lua(&result)?;
1027        Ok(result)
1028    }
1029}
1030
1031impl HasIntegrity for RemoteProjectToml {
1032    fn hash(&self) -> io::Result<Integrity> {
1033        self.to_lua_rockspec()
1034            .map_err(|err| {
1035                io::Error::other(format!(
1036                    "unable to convert remote project to Lua rockspec:\n{}",
1037                    err
1038                ))
1039            })?
1040            .hash()
1041    }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use std::path::PathBuf;
1047
1048    use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1049    use git2::{Repository, RepositoryInitOptions};
1050    use url::Url;
1051
1052    use crate::{
1053        git::{url::RemoteGitUrl, GitSource},
1054        lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1055        project::{Project, ProjectRoot},
1056        rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1057    };
1058
1059    use super::PartialProjectToml;
1060
1061    #[test]
1062    fn project_toml_parsing() {
1063        let project_toml = r#"
1064        package = "my-package"
1065        version = "1.0.0"
1066        lua = "5.3"
1067
1068        rockspec_format = "1.0"
1069
1070        [source]
1071        url = "https://example.com"
1072
1073        [dependencies]
1074        foo = "1.0"
1075        bar = ">=2.0"
1076
1077        [run]
1078        args = ["--foo", "--bar"]
1079
1080        [build]
1081        type = "builtin"
1082        "#;
1083
1084        let project = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1085        let _ = project.into_remote(None).unwrap();
1086
1087        let project_toml = r#"
1088        package = "my-package"
1089        version = "1.0.0"
1090        lua = "5.1"
1091
1092        [description]
1093        summary = "A summary"
1094        detailed = "A detailed description"
1095        license = "MIT"
1096        homepage = "https://example.com"
1097        issues_url = "https://example.com/issues"
1098        maintainer = "John Doe"
1099        labels = ["label1", "label2"]
1100
1101        [supported_platforms]
1102        linux = true
1103        windows = false
1104
1105        [dependencies]
1106        foo = "1.0"
1107        bar = ">=2.0"
1108
1109        [build_dependencies]
1110        baz = "1.0"
1111
1112        [external_dependencies.foo]
1113        header = "foo.h"
1114
1115        [external_dependencies.bar]
1116        library = "libbar.so"
1117
1118        [test_dependencies]
1119        busted = "69.420"
1120
1121        [source]
1122        url = "https://example.com"
1123        hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
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        [build]
1137        type = "builtin"
1138        "#;
1139
1140        let project = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1141        let _ = project.into_remote(None).unwrap();
1142    }
1143
1144    #[test]
1145    fn compare_project_toml_with_rockspec() {
1146        let project_toml = r#"
1147        package = "my-package"
1148        version = "1.0.0"
1149        lua = "5.1"
1150
1151        # For testing, specify a custom rockspec format
1152        # (defaults to 3.0)
1153        rockspec_format = "1.0"
1154
1155        [description]
1156        summary = "A summary"
1157        detailed = "A detailed description"
1158        license = "MIT"
1159        homepage = "https://example.com"
1160        issues_url = "https://example.com/issues"
1161        maintainer = "John Doe"
1162        labels = ["label1", "label2"]
1163
1164        [supported_platforms]
1165        linux = true
1166        windows = false
1167
1168        [dependencies]
1169        foo = "1.0"
1170        bar = ">=2.0"
1171
1172        [build_dependencies]
1173        baz = "1.0"
1174
1175        [external_dependencies.foo]
1176        header = "foo.h"
1177
1178        [external_dependencies.bar]
1179        library = "libbar.so"
1180
1181        [test_dependencies]
1182        busted = "1.0"
1183
1184        [source]
1185        url = "https://example.com"
1186        file = "my-package-1.0.0.tar.gz"
1187        dir = "my-package-1.0.0"
1188
1189        [test]
1190        type = "command"
1191        script = "test.lua"
1192        flags = [ "foo", "bar" ]
1193
1194        [run]
1195        command = "my-command"
1196        args = ["--foo", "--bar"]
1197
1198        [deploy]
1199        wrap_bin_scripts = false
1200
1201        [build]
1202        type = "builtin"
1203
1204        [build.install.lua]
1205        "foo.bar" = "src/bar.lua"
1206
1207        [build.install.lib]
1208        "foo.baz" = "src/baz.c"
1209
1210        [build.install.bin]
1211        "bla" = "src/bla"
1212
1213        [build.install.conf]
1214        "cfg.conf" = "resources/config.conf"
1215        "#;
1216
1217        let expected_rockspec = r#"
1218            rockspec_format = "1.0"
1219            package = "my-package"
1220            version = "1.0.0"
1221
1222            source = {
1223                url = "https://example.com",
1224                file = "my-package-1.0.0.tar.gz",
1225                dir = "my-package-1.0.0",
1226            }
1227
1228            description = {
1229                summary = "A summary",
1230                detailed = "A detailed description",
1231                license = "MIT",
1232                homepage = "https://example.com",
1233                issues_url = "https://example.com/issues",
1234                maintainer = "John Doe",
1235                labels = {"label1", "label2"},
1236            }
1237
1238            supported_platforms = {"linux", "!windows"}
1239
1240            dependencies = {
1241                "lua ==5.1",
1242                "foo ==1.0",
1243                "bar >=2.0",
1244            }
1245
1246            build_dependencies = {
1247                "baz ==1.0",
1248            }
1249
1250            external_dependencies = {
1251                foo = { header = "foo.h" },
1252                bar = { library = "libbar.so" },
1253            }
1254
1255            source = {
1256                url = "https://example.com",
1257                hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1258                file = "my-package-1.0.0.tar.gz",
1259                dir = "my-package-1.0.0",
1260            }
1261
1262            test = {
1263                type = "command",
1264                script = "test.lua",
1265                flags = {"foo", "bar"},
1266            }
1267
1268            deploy = {
1269                wrap_bin_scripts = false,
1270            }
1271
1272            build = {
1273                type = "builtin",
1274                install = {
1275                    lua = {
1276                        ["foo.bar"] = "src/bar.lua",
1277                    },
1278                    lib = {
1279                        ["foo.baz"] = "src/baz.c",
1280                    },
1281                    bin = {
1282                        bla = "src/bla",
1283                    },
1284                    conf = {
1285                        ["cfg.conf"] = "resources/config.conf",
1286                    },
1287                },
1288            }
1289        "#;
1290
1291        let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1292
1293        let project_toml = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1294        let rockspec = project_toml
1295            .into_remote(None)
1296            .unwrap()
1297            .to_lua_rockspec()
1298            .unwrap();
1299
1300        let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1301            let mut v = v.current_platform().clone();
1302            v.sort_by(|a, b| a.name().cmp(b.name()));
1303            v
1304        };
1305
1306        assert_eq!(rockspec.package(), expected_rockspec.package());
1307        assert_eq!(rockspec.version(), expected_rockspec.version());
1308        assert_eq!(rockspec.description(), expected_rockspec.description());
1309        assert_eq!(
1310            rockspec.supported_platforms(),
1311            expected_rockspec.supported_platforms()
1312        );
1313        assert_eq!(
1314            sorted_package_reqs(rockspec.dependencies()),
1315            sorted_package_reqs(expected_rockspec.dependencies())
1316        );
1317        assert_eq!(
1318            sorted_package_reqs(rockspec.build_dependencies()),
1319            sorted_package_reqs(expected_rockspec.build_dependencies())
1320        );
1321        assert_eq!(
1322            rockspec.external_dependencies(),
1323            expected_rockspec.external_dependencies()
1324        );
1325        assert_eq!(rockspec.source(), expected_rockspec.source());
1326        assert_eq!(rockspec.build(), expected_rockspec.build());
1327        assert_eq!(rockspec.format(), expected_rockspec.format());
1328    }
1329
1330    #[test]
1331    fn merge_project_toml_with_partial_rockspec() {
1332        let project_toml = r#"
1333        package = "my-package"
1334        version = "1.0.0"
1335        lua = "5.1"
1336
1337        # For testing, specify a custom rockspec format
1338        # (defaults to 3.0)
1339        rockspec_format = "1.0"
1340
1341        [description]
1342        summary = "A summary"
1343        detailed = "A detailed description"
1344        license = "MIT"
1345        homepage = "https://example.com"
1346        issues_url = "https://example.com/issues"
1347        maintainer = "John Doe"
1348        labels = ["label1", "label2"]
1349
1350        [supported_platforms]
1351        linux = true
1352        windows = false
1353
1354        [dependencies]
1355        foo = "1.0"
1356        bar = ">=2.0"
1357
1358        [build_dependencies]
1359        baz = "1.0"
1360
1361        [external_dependencies.foo]
1362        header = "foo.h"
1363
1364        [external_dependencies.bar]
1365        library = "libbar.so"
1366
1367        [test_dependencies]
1368        busted = "1.0"
1369
1370        [source]
1371        url = "https://example.com"
1372        file = "my-package-1.0.0.tar.gz"
1373        dir = "my-package-1.0.0"
1374
1375        [test]
1376        type = "command"
1377        script = "test.lua"
1378        flags = [ "foo", "bar" ]
1379
1380        [run]
1381        command = "my-command"
1382        args = [ "--foo", "--bar" ]
1383
1384        [build]
1385        type = "builtin"
1386        "#;
1387
1388        let mergable_rockspec_content = r#"
1389            rockspec_format = "1.0"
1390            package = "my-package-overwritten"
1391
1392            description = {
1393                summary = "A summary overwritten",
1394                detailed = "A detailed description overwritten",
1395                license = "GPL-2.0",
1396                homepage = "https://example.com/overwritten",
1397                issues_url = "https://example.com/issues/overwritten",
1398                maintainer = "John Doe Overwritten",
1399                labels = {"over", "written"},
1400            }
1401
1402            -- Inverted supported platforms
1403            supported_platforms = {"!linux", "windows"}
1404
1405            dependencies = {
1406                "lua 5.1",
1407                "foo >1.0",
1408                "bar <=2.0",
1409            }
1410
1411            build_dependencies = {
1412                "baz >1.0",
1413            }
1414
1415            external_dependencies = {
1416                foo = { header = "overwritten.h" },
1417                bar = { library = "overwritten.so" },
1418            }
1419
1420            test = {
1421                type = "command",
1422                script = "overwritten.lua",
1423                flags = {"over", "written"},
1424            }
1425
1426            build = {
1427                type = "builtin",
1428            }
1429        "#;
1430
1431        let remote_rockspec_content = format!(
1432            r#"{}
1433            version = "1.0.0"
1434            source = {{
1435                url = "https://example.com",
1436                file = "my-package-1.0.0.tar.gz",
1437                dir = "my-package-1.0.0",
1438            }}
1439        "#,
1440            &mergable_rockspec_content
1441        );
1442
1443        let project_toml = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1444        let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1445        let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1446
1447        let merged = project_toml
1448            .merge(partial_rockspec)
1449            .into_remote(None)
1450            .unwrap();
1451
1452        let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1453            let mut v = v.current_platform().clone();
1454            v.sort_by(|a, b| a.name().cmp(b.name()));
1455            v
1456        };
1457
1458        assert_eq!(merged.package(), expected_rockspec.package());
1459        assert_eq!(merged.version(), expected_rockspec.version());
1460        assert_eq!(merged.description(), expected_rockspec.description());
1461        assert_eq!(
1462            merged.supported_platforms(),
1463            expected_rockspec.supported_platforms()
1464        );
1465        assert_eq!(
1466            sorted_package_reqs(merged.dependencies()),
1467            sorted_package_reqs(expected_rockspec.dependencies())
1468        );
1469        assert_eq!(
1470            sorted_package_reqs(merged.build_dependencies()),
1471            sorted_package_reqs(expected_rockspec.build_dependencies())
1472        );
1473        assert_eq!(
1474            merged.external_dependencies(),
1475            expected_rockspec.external_dependencies()
1476        );
1477        assert_eq!(merged.source(), expected_rockspec.source());
1478        assert_eq!(merged.build(), expected_rockspec.build());
1479        assert_eq!(merged.format(), expected_rockspec.format());
1480        // Ensure that the run command is retained after merge.
1481        assert!(merged.local.run().is_some());
1482    }
1483
1484    #[test]
1485    fn project_toml_with_lua_in_dependencies() {
1486        let project_toml = r#"
1487        package = "my-package"
1488        version = "1.0.0"
1489        # lua = ">5.1"
1490
1491        [dependencies]
1492        lua = "5.1" # disallowed
1493
1494        [build]
1495        type = "builtin"
1496        "#;
1497
1498        PartialProjectToml::new(project_toml, ProjectRoot::default())
1499            .unwrap()
1500            .into_local()
1501            .unwrap_err();
1502    }
1503
1504    #[test]
1505    fn project_toml_with_invalid_run_command() {
1506        for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1507            let project_toml = format!(
1508                r#"
1509                package = "my-package"
1510                version = "1.0.0"
1511                lua = "5.1"
1512
1513                [build]
1514                type = "builtin"
1515
1516                [run]
1517                command = "{command}"
1518                "#,
1519            );
1520
1521            PartialProjectToml::new(&project_toml, ProjectRoot::default()).unwrap_err();
1522        }
1523    }
1524
1525    #[test]
1526    fn generate_non_deterministic_git_source() {
1527        let rockspec_content = r#"
1528            package = "test-package"
1529            version = "1.0.0"
1530            lua = ">=5.1"
1531
1532            [source]
1533            url = "git+https://exaple.com/repo.git"
1534
1535            [build]
1536            type = "builtin"
1537        "#;
1538
1539        PartialProjectToml::new(rockspec_content, ProjectRoot::default())
1540            .unwrap()
1541            .into_remote(None)
1542            .unwrap_err();
1543    }
1544
1545    #[test]
1546    fn generate_deterministic_git_source() {
1547        let rockspec_content = r#"
1548            package = "test-package"
1549            version = "1.0.0"
1550            lua = ">=5.1"
1551
1552            [source]
1553            url = "git+https://exaple.com/owner/repo.git"
1554            tag = "v0.1.0"
1555
1556            [build]
1557            type = "builtin"
1558        "#;
1559
1560        PartialProjectToml::new(rockspec_content, ProjectRoot::default())
1561            .unwrap()
1562            .into_remote(None)
1563            .unwrap();
1564    }
1565
1566    fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
1567        let sample_project: PathBuf = "resources/test/sample-projects/source-template/".into();
1568        temp_dir.copy_from(&sample_project, &["**"]).unwrap();
1569        let repo = Repository::init(temp_dir).unwrap();
1570        let mut opts = RepositoryInitOptions::new();
1571        opts.initial_head("main");
1572        {
1573            let mut config = repo.config().unwrap();
1574            config.set_str("user.name", "name").unwrap();
1575            config.set_str("user.email", "email").unwrap();
1576            let mut index = repo.index().unwrap();
1577            let id = index.write_tree().unwrap();
1578
1579            let tree = repo.find_tree(id).unwrap();
1580            let sig = repo.signature().unwrap();
1581            repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
1582                .unwrap();
1583        }
1584        repo
1585    }
1586
1587    fn create_tag(repo: &Repository, name: &str) {
1588        let sig = repo.signature().unwrap();
1589        let id = repo.head().unwrap().target().unwrap();
1590        let obj = repo.find_object(id, None).unwrap();
1591        repo.tag(name, &obj, &sig, "msg", true).unwrap();
1592    }
1593
1594    #[test]
1595    fn test_git_project_generate_dev_source() {
1596        let project_root = assert_fs::TempDir::new().unwrap();
1597        init_sample_project_repo(&project_root);
1598        let project = Project::from_exact(&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::Git { .. }));
1603        if let RockSourceSpec::Git(GitSource { url, checkout_ref }) = source_spec {
1604            let expected_url: RemoteGitUrl =
1605                "https://github.com/lumen-oss/lux.git".parse().unwrap();
1606            assert_eq!(url, &expected_url);
1607            assert!(checkout_ref.is_some());
1608        }
1609        assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1610    }
1611
1612    #[test]
1613    fn test_git_project_generate_non_semver_tag_source() {
1614        let project_root = assert_fs::TempDir::new().unwrap();
1615        let repo = init_sample_project_repo(&project_root);
1616        let tag_name = "bla";
1617        create_tag(&repo, tag_name);
1618        let project = Project::from_exact(&project_root).unwrap().unwrap();
1619        let remote_project_toml = project.toml().into_remote(None).unwrap();
1620        let source = remote_project_toml.source.current_platform();
1621        let source_spec = &source.source_spec;
1622        assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1623        if let RockSourceSpec::Git(GitSource { url, checkout_ref }) = source_spec {
1624            let expected_url: RemoteGitUrl =
1625                "https://github.com/lumen-oss/lux.git".parse().unwrap();
1626            assert_eq!(url, &expected_url);
1627            assert_eq!(checkout_ref, &Some(tag_name.to_string()));
1628        }
1629        assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1630    }
1631
1632    #[test]
1633    fn test_git_project_generate_release_source_tag_with_v_prefix() {
1634        let project_root = assert_fs::TempDir::new().unwrap();
1635        let repo = init_sample_project_repo(&project_root);
1636        let tag_name = "v1.0.0";
1637        create_tag(&repo, "bla");
1638        create_tag(&repo, tag_name);
1639        let project = Project::from_exact(&project_root).unwrap().unwrap();
1640        let remote_project_toml = project.toml().into_remote(None).unwrap();
1641        let source = remote_project_toml.source.current_platform();
1642        let source_spec = &source.source_spec;
1643        assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1644        if let RockSourceSpec::Url(url) = source_spec {
1645            let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
1646                .parse()
1647                .unwrap();
1648            assert_eq!(url, &expected_url);
1649        }
1650        assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1651    }
1652
1653    #[test]
1654    fn test_git_project_generate_release_source_tag_without_v_prefix() {
1655        let project_root = assert_fs::TempDir::new().unwrap();
1656        let repo = init_sample_project_repo(&project_root);
1657        create_tag(&repo, "bla");
1658        let tag_name = "1.0.0";
1659        create_tag(&repo, tag_name);
1660        let project = Project::from_exact(&project_root).unwrap().unwrap();
1661        let remote_project_toml = project.toml().into_remote(None).unwrap();
1662        let source = remote_project_toml.source.current_platform();
1663        let source_spec = &source.source_spec;
1664        assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1665        if let RockSourceSpec::Url(url) = source_spec {
1666            let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1667                .parse()
1668                .unwrap();
1669            assert_eq!(url, &expected_url);
1670        }
1671        assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1672    }
1673
1674    #[test]
1675    fn test_git_project_in_subdirectory() {
1676        let temp_dir = assert_fs::TempDir::new().unwrap();
1677        let sample_project: PathBuf = "resources/test/sample-projects/source-template/".into();
1678        let project_dir = temp_dir.child("lux");
1679        project_dir.create_dir_all().unwrap();
1680        project_dir.copy_from(&sample_project, &["**"]).unwrap();
1681        let repo = Repository::init(&temp_dir).unwrap();
1682        let mut opts = RepositoryInitOptions::new();
1683        opts.initial_head("main");
1684        {
1685            let mut config = repo.config().unwrap();
1686            config.set_str("user.name", "name").unwrap();
1687            config.set_str("user.email", "email").unwrap();
1688            let mut index = repo.index().unwrap();
1689            let id = index.write_tree().unwrap();
1690
1691            let tree = repo.find_tree(id).unwrap();
1692            let sig = repo.signature().unwrap();
1693            repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
1694                .unwrap();
1695        }
1696        create_tag(&repo, "bla");
1697        let tag_name = "1.0.0";
1698        create_tag(&repo, tag_name);
1699        let project = Project::from_exact(&project_dir).unwrap().unwrap();
1700        let remote_project_toml = project.toml().into_remote(None).unwrap();
1701        let source = remote_project_toml.source.current_platform();
1702        let source_spec = &source.source_spec;
1703        assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1704        if let RockSourceSpec::Url(url) = source_spec {
1705            let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1706                .parse()
1707                .unwrap();
1708            assert_eq!(url, &expected_url);
1709        }
1710        assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1711    }
1712}