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