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