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