Skip to main content

lux_lib/project/
project_toml.rs

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