Skip to main content

lux_lib/project/
project_toml.rs

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