Skip to main content

lux_lib/project/
project_toml.rs

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