1use crate::fs;
4use crate::git::shorthand::RemoteGitUrlShorthand;
5use crate::git::{GitRef, GitSource};
6use crate::hash::HasIntegrity;
7use crate::lockfile::OptState;
8use crate::lockfile::PinnedState;
9use crate::lua_rockspec::DeploySpec;
10use crate::lua_rockspec::LocalLuaRockspec;
11use crate::lua_rockspec::LocalRockSource;
12use crate::lua_rockspec::LuaRockspecError;
13use crate::lua_rockspec::RemoteLuaRockspec;
14use crate::lua_rockspec::RockSourceSpec;
15use crate::lua_version::LuaVersion;
16use crate::operations::RunCommand;
17use crate::package::PackageNameList;
18use crate::package::SpecRev;
19use crate::project::TomlDeError;
20use crate::rockspec::lua_dependency::LuaDependencySpec;
21use crate::ROCKSPEC_FUEL_LIMIT;
22use std::io;
23use std::path::Path;
24use std::{collections::HashMap, path::PathBuf};
25
26use crate::{
27 build::autodetect_modules,
28 config::Config,
29 lua_rockspec::{
30 BuildSpec, BuildSpecInternal, BuildSpecInternalError, BuildType, DisplayAsLuaKV,
31 DisplayLuaKV, ExternalDependencies, ExternalDependencySpec, LuaTableKey, LuaVersionError,
32 ModuleSpec, ModuleSpecInternal, PartialLuaRockspec, PerPlatform, PlatformIdentifier,
33 PlatformSupport, PlatformValidationError, RemoteRockSource, RockDescription,
34 RockSourceError, RockspecFormat, TestSpec, TestSpecDecodeError, TestSpecInternal,
35 },
36 package::{
37 BuildDependencies, Dependencies, PackageName, PackageReq, PackageVersion, PackageVersionReq,
38 },
39 rockspec::{LuaVersionCompatibility, Rockspec},
40};
41use itertools::Itertools;
42use miette::Diagnostic;
43use nonempty::NonEmpty;
44
45use crate::package::wally::PackageReq as WallyPackageReq;
46use serde::de;
47use serde::{Deserialize, Deserializer};
48use ssri::Integrity;
49use thiserror::Error;
50
51use super::gen::GenerateSourceError;
52use super::gen::RockSourceTemplate;
53use super::r#gen::GenerateVersionError;
54use super::r#gen::PackageVersionTemplate;
55use super::ProjectRoot;
56
57pub const PROJECT_TOML: &str = "lux.toml";
58
59#[derive(Deserialize)]
60#[serde(untagged)]
61#[allow(clippy::large_enum_variant)] enum DependencyEntry {
63 Simple(PackageVersionReq),
64 Detailed(DependencyTableEntry),
65}
66
67#[derive(Debug, Deserialize)]
68struct DependencyTableEntry {
69 #[serde(default)]
70 version: Option<PackageVersionReq>,
71 #[serde(default)]
72 opt: Option<bool>,
73 #[serde(default)]
74 pin: Option<bool>,
75 #[serde(default)]
76 git: Option<RemoteGitUrlShorthand>,
77 #[serde(default)]
78 path: Option<PathBuf>,
79 #[serde(default)]
80 rev: Option<String>,
81 #[serde(default)]
82 wally: Option<WallyPackageReq>,
83}
84
85fn parse_map_to_dependency_vec_opt<'de, D>(
86 deserializer: D,
87) -> Result<Option<Vec<LuaDependencySpec>>, D::Error>
88where
89 D: Deserializer<'de>,
90{
91 let packages: Option<HashMap<PackageName, DependencyEntry>> =
92 Option::deserialize(deserializer)?;
93
94 match packages {
95 None => Ok(None),
96 Some(packages) => Ok(Some(
97 packages
98 .into_iter()
99 .map(|(name, spec)| match spec {
100 DependencyEntry::Simple(version_req) => {
101 Ok(PackageReq { name, version_req }.into())
102 }
103 DependencyEntry::Detailed(entry) => {
104 let DependencyTableEntry {
105 version,
106 opt,
107 pin,
108 git,
109 path,
110 rev,
111 wally,
112 } = entry;
113
114 let (version_req, source) = if let Some(wally) = wally {
115 if version.is_some() || git.is_some() || rev.is_some() || path.is_some() {
116 return Err(de::Error::custom(format!(
117 "dependency '{}' specifies 'wally', which cannot be combined with 'version', 'git', 'rev', or 'path'",
118 name
119 )));
120 }
121 (
122 PackageVersionReq::SemVer(wally.version_req().clone()),
123 Some(RockSourceSpec::Wally(wally)),
124 )
125 } else {
126 let version = version.ok_or_else(|| {
127 de::Error::custom(format!(
128 "dependency '{name}' is missing a 'version' or 'wally' field"
129 ))
130 })?;
131 let source = match (git, rev, path) {
132 (None, None, None) => Ok(None),
133 (None, Some(_), None) => Err(de::Error::custom(format!(
134 "dependency {} specifies a 'rev', but missing a 'git' field",
135 name
136 ))),
137 (Some(git), Some(rev), None) => {
138 Ok(Some(RockSourceSpec::Git(GitSource {
139 url: git.into(),
140 git_ref: Some(GitRef::Tag(rev)),
141 })))
142 }
143 (Some(git), None, None) => Ok(Some(RockSourceSpec::Git(GitSource {
144 url: git.into(),
145 git_ref: Some(GitRef::Tag(
146 version
147 .clone()
148 .to_string()
149 .trim_start_matches("=")
150 .to_string(),
151 )),
152 }))),
153 (None, None, Some(path)) => Ok(Some(RockSourceSpec::File(path))),
154 (_, _, Some(_)) => Err(de::Error::custom(format!(
155 "dependency '{}' specifies a 'path', which cannot be combined with 'git' or 'rev'",
156 name
157 ))),
158 }?;
159 (version, source)
160 };
161
162 Ok(LuaDependencySpec {
163 package_req: PackageReq { name, version_req },
164 opt: OptState::from(opt.unwrap_or(false)),
165 pin: PinnedState::from(pin.unwrap_or(false)),
166 source,
167 })
168 }
169 })
170 .try_collect()?,
171 )),
172 }
173}
174
175#[derive(Debug, Error, Diagnostic)]
176pub enum ProjectTomlError {
177 #[error("error generating rockspec source")]
178 #[diagnostic(forward(0))]
179 GenerateSource(#[from] GenerateSourceError),
180 #[error("error generating rockspec version")]
181 #[diagnostic(forward(0))]
182 GenerateVersion(#[from] GenerateVersionError),
183 #[error("generated rockspec exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
184 #[diagnostic(help(
185 r#"the rockspec may be too complex or contain too many dependencies.
186 Try reducing the number of dependencies or simplifying build instructions."#
187 ))]
188 FuelLimitExceeded,
189 #[error(
190 r#"generated invalid Lua from TOML:
191{0}
192"#
193 )]
194 #[diagnostic(help("this is a bug in Lux. please report it."))]
195 GeneratedInvalidLua(String),
196}
197
198#[derive(Debug, Error, Diagnostic)]
199pub enum LocalProjectTomlValidationError {
200 #[error("no lua version provided")]
201 #[diagnostic(help(
202 r#"add a `lua` field to your lux.toml, for example:
203
204 lua = ">=5.1""#
205 ))]
206 NoLuaVersion,
207 #[error("could not decode the test spec")]
208 #[diagnostic(forward(0))]
209 TestSpecError(#[from] TestSpecDecodeError),
210 #[error("could not decode the build spec")]
211 #[diagnostic(forward(0))]
212 BuildSpecInternal(#[from] BuildSpecInternalError),
213 #[error(transparent)]
214 #[diagnostic(transparent)]
215 PlatformValidation(#[from] PlatformValidationError),
216 #[error("{}copy_directories cannot contain a rockspec name", ._0.as_ref().map(|p| format!("{p}: ")).unwrap_or_default())]
217 CopyDirectoriesContainRockspecName(Option<String>),
218 #[error("could not decode the source spec")]
219 #[diagnostic(forward(0))]
220 RockSource(#[from] RockSourceError),
221 #[error("duplicate dependencies: {0}")]
222 #[diagnostic(help("remove the duplicate entries from the [dependencies] table."))]
223 DuplicateDependencies(PackageNameList),
224 #[error("duplicate test dependencies: {0}")]
225 #[diagnostic(help("remove the duplicate entries from the [test_dependencies] table."))]
226 DuplicateTestDependencies(PackageNameList),
227 #[error("duplicate build dependencies: {0}")]
228 #[diagnostic(help("remove the duplicate entries from the [build_dependencies] table."))]
229 DuplicateBuildDependencies(PackageNameList),
230 #[error(
231 r#"dependencies field cannot contain 'lua'.
232 Please provide the version in the top-level 'lua' field
233
234 Example:
235
236 ```toml
237 package = "my-package"
238 lua = ">=5.1"
239
240 [dependencies]
241 # do not add Lua here!
242 ```
243 "#
244 )]
245 DependenciesContainLua,
246 #[error("error generating rockspec source")]
247 #[diagnostic(forward(0))]
248 GenerateSource(#[from] GenerateSourceError),
249 #[error("error generating rockspec version")]
250 #[diagnostic(forward(0))]
251 GenerateVersion(#[from] GenerateVersionError),
252}
253
254#[derive(Debug, Error, Diagnostic)]
255pub enum RemoteProjectTomlValidationError {
256 #[error("error generating rockspec source")]
257 #[diagnostic(forward(0))]
258 GenerateSource(#[from] GenerateSourceError),
259 #[error("error generating rockspec version")]
260 #[diagnostic(forward(0))]
261 GenerateVersion(#[from] GenerateVersionError),
262 #[error(transparent)]
263 #[diagnostic(transparent)]
264 LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
265}
266
267#[derive(Clone, Debug, Deserialize)]
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
272pub struct PartialProjectToml {
273 #[cfg_attr(
275 feature = "schema",
276 schemars(extend("x-lux-toml-example" = r#"package = "my-lua-project""#))
277 )]
278 pub(crate) package: PackageName,
279 #[serde(default, rename = "version")]
286 #[cfg_attr(feature = "schema", schemars(with = "String"))]
287 #[cfg_attr(
288 feature = "schema",
289 schemars(extend("x-lux-toml-example" = r#"version = "1.0.0""#))
290 )]
291 pub(crate) version_template: PackageVersionTemplate,
292 #[serde(default)]
296 pub(crate) build: BuildSpecInternal,
297 #[cfg_attr(
300 feature = "schema",
301 schemars(extend("x-lux-toml-example" = r#"rockspec_format = "3.0""#))
302 )]
303 pub(crate) rockspec_format: Option<RockspecFormat>,
304 #[serde(default)]
306 #[cfg_attr(
307 feature = "schema",
308 schemars(extend("x-lux-toml-example" = r#"
309[run]
310args = ["src/main.lua"]
311"#))
312 )]
313 pub(crate) run: Option<RunSpec>,
314 #[serde(default)]
317 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
318 #[cfg_attr(
319 feature = "schema",
320 schemars(extend("x-lux-toml-example" = r#"lua = ">=5.1""#))
321 )]
322 pub(crate) lua: Option<PackageVersionReq>,
323 #[serde(default)]
325 #[cfg_attr(
326 feature = "schema",
327 schemars(extend("x-lux-toml-example" = r#"
328[description]
329summary = "A one-line description of the package"
330detailed = """
331A longer description of the package
332that often spans multiple lines.
333"""
334license = "GPLv2"
335homepage = "https://lumen-oss.github.io"
336issues_url = "https://github.com/lumen-oss/lux/issues"
337maintainer = "neorocks org"
338labels = ["neovim", "tls", "https"]
339"#))
340 )]
341 pub(crate) description: Option<RockDescription>,
342 #[serde(default)]
345 #[cfg_attr(
346 feature = "schema",
347 schemars(with = "Option<std::collections::BTreeMap<String, bool>>")
348 )]
349 #[cfg_attr(
350 feature = "schema",
351 schemars(extend("x-lux-toml-example" = r#"
352[supported_platforms]
353linux = true
354windows = false
355"#))
356 )]
357 pub(crate) supported_platforms: Option<HashMap<PlatformIdentifier, bool>>,
358 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
380 #[cfg_attr(
381 feature = "schema",
382 schemars(
383 with = "Option<std::collections::BTreeMap<String, crate::schema::DependencyEntry>>"
384 )
385 )]
386 #[cfg_attr(
387 feature = "schema",
388 schemars(extend("x-lux-toml-example" = r#"
389[dependencies]
390luafilesystem = "1.0.0"
391
392[dependencies.argparse]
393version = "0.7"
394opt = true
395
396[dependencies.foo]
397version = "09511782201302ade916d4b250d01a6c61b56844"
398git = "github:lunarmodules/foo"
399
400[dependencies.bar]
401path = "../bar"
402version = "1.0.0"
403"#))
404 )]
405 pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
406 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
408 #[cfg_attr(
409 feature = "schema",
410 schemars(
411 with = "Option<std::collections::BTreeMap<String, crate::schema::DependencyEntry>>"
412 )
413 )]
414 pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
415 #[serde(default)]
418 #[cfg_attr(
419 feature = "schema",
420 schemars(extend("x-lux-toml-example" = r#"
421[external_dependencies.OPENSSL]
422header = "openssl/ssl.h"
423library = "ssl"
424"#))
425 )]
426 pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
427 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
429 #[cfg_attr(
430 feature = "schema",
431 schemars(
432 with = "Option<std::collections::BTreeMap<String, crate::schema::DependencyEntry>>"
433 )
434 )]
435 #[cfg_attr(
436 feature = "schema",
437 schemars(extend("x-lux-toml-example" = r#"
438[test_dependencies]
439busted = ">=2.0.0"
440nlua = "~>0.3"
441"#))
442 )]
443 pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
444 #[serde(default, rename = "source")]
452 #[cfg_attr(
453 feature = "schema",
454 schemars(extend("x-lux-toml-example" = r#"
455[source]
456url = "https://host.com/owner/$(PACKAGE)/archive/refs/tags/$(REF).zip"
457dev = "git+https://host.com/owner/$(PACKAGE).git"
458"#))
459 )]
460 pub(crate) source_template: RockSourceTemplate,
461 #[serde(default)]
468 #[cfg_attr(
469 feature = "schema",
470 schemars(extend("x-lux-toml-example" = r#"
471[test]
472type = "busted"
473flags = []
474"#))
475 )]
476 pub(crate) test: Option<TestSpecInternal>,
477 #[serde(default)]
479 #[cfg_attr(
480 feature = "schema",
481 schemars(extend("x-lux-toml-example" = r#"
482[deploy]
483wrap_bin_scripts = false
484"#))
485 )]
486 pub(crate) deploy: Option<DeploySpec>,
487 #[serde(default)]
489 #[cfg_attr(
490 feature = "schema",
491 schemars(extend("x-lux-toml-example" = r#"
492[project]
493root_dir = ".."
494"#))
495 )]
496 pub(crate) project: ProjectSpec,
497
498 #[serde(skip, default = "PathBuf::new")]
500 lux_toml_dir: PathBuf,
501}
502
503impl HasIntegrity for PartialProjectToml {
504 async fn hash(&self) -> io::Result<Integrity> {
505 let toml_file = self.lux_toml_dir.join(PROJECT_TOML);
506 let content = fs::sync::read_to_string(&toml_file).map_err(io::Error::other)?;
507 Ok(Integrity::from(&content))
508 }
509}
510
511impl PartialProjectToml {
512 pub(crate) fn new(
513 name: &str,
514 str: &str,
515 lux_toml_dir: ProjectRoot,
516 ) -> Result<Self, TomlDeError> {
517 let mut this: Self = super::parse_toml(name, str)?;
518 this.lux_toml_dir = lux_toml_dir.to_path_buf();
519 this.project.root = match this.project.root_dir {
520 Some(ref root_dir) => {
521 let relative_project_root = lux_toml_dir.join(root_dir);
522 let project_root =
523 std::fs::canonicalize(&relative_project_root).unwrap_or(relative_project_root);
524 ProjectRoot(project_root)
525 }
526 None => lux_toml_dir,
527 };
528 Ok(this)
529 }
530
531 pub fn lua(&self) -> Option<&PackageVersionReq> {
533 self.lua.as_ref()
534 }
535
536 pub fn into_local(&self) -> Result<LocalProjectToml, LocalProjectTomlValidationError> {
539 let project_toml = self.clone();
540
541 if project_toml
543 .dependencies
544 .as_ref()
545 .is_some_and(|deps| deps.iter().any(|dep| dep.name() == &"lua".into()))
546 {
547 return Err(LocalProjectTomlValidationError::DependenciesContainLua);
548 }
549
550 let get_duplicates = |dependencies: &Option<Vec<LuaDependencySpec>>| {
551 dependencies
552 .iter()
553 .flat_map(|deps| {
554 deps.iter()
555 .map(|dep| dep.package_req().name())
556 .duplicates()
557 .cloned()
558 })
559 .collect_vec()
560 };
561 let duplicate_dependencies = get_duplicates(&self.dependencies);
562 if !duplicate_dependencies.is_empty() {
563 return Err(LocalProjectTomlValidationError::DuplicateDependencies(
564 PackageNameList::new(duplicate_dependencies),
565 ));
566 }
567 let duplicate_test_dependencies = get_duplicates(&self.test_dependencies);
568 if !duplicate_test_dependencies.is_empty() {
569 return Err(LocalProjectTomlValidationError::DuplicateTestDependencies(
570 PackageNameList::new(duplicate_test_dependencies),
571 ));
572 }
573 let duplicate_build_dependencies = get_duplicates(&self.build_dependencies);
574 if !duplicate_build_dependencies.is_empty() {
575 return Err(LocalProjectTomlValidationError::DuplicateBuildDependencies(
576 PackageNameList::new(duplicate_build_dependencies),
577 ));
578 }
579
580 let validated = LocalProjectToml {
581 internal: project_toml.clone(),
582
583 package: project_toml.package,
584 version: project_toml
585 .version_template
586 .try_generate(&self.lux_toml_dir, None)
587 .unwrap_or(PackageVersion::default_dev_version()),
588 lua: project_toml
589 .lua
590 .ok_or(LocalProjectTomlValidationError::NoLuaVersion)?,
591 description: project_toml.description.unwrap_or_default(),
592 run: project_toml.run.map(PerPlatform::new),
593 supported_platforms: PlatformSupport::parse(
594 &project_toml
595 .supported_platforms
596 .unwrap_or_default()
597 .into_iter()
598 .map(|(platform, supported)| {
599 if supported {
600 format!("{platform}")
601 } else {
602 format!("!{platform}")
603 }
604 })
605 .collect_vec(),
606 )?,
607 dependencies: PerPlatform::new(
610 project_toml
611 .dependencies
612 .unwrap_or_default()
613 .into_iter()
614 .map(|dep| self.resolve_lua_dependency_spec(dep))
615 .collect_vec(),
616 ),
617 test_dependencies: PerPlatform::new(
618 project_toml
619 .test_dependencies
620 .unwrap_or_default()
621 .into_iter()
622 .map(|dep| self.resolve_lua_dependency_spec(dep))
623 .collect_vec(),
624 ),
625 build_dependencies: PerPlatform::new(
626 project_toml
627 .build_dependencies
628 .unwrap_or_default()
629 .into_iter()
630 .map(|dep| self.resolve_lua_dependency_spec(dep))
631 .collect_vec(),
632 ),
633 external_dependencies: PerPlatform::new(
634 project_toml.external_dependencies.unwrap_or_default(),
635 ),
636 test: PerPlatform::new(TestSpec::try_from(
637 project_toml.test.clone().unwrap_or_default(),
638 )?),
639 build: PerPlatform::new(BuildSpec::from_internal_spec(project_toml.build.clone())?),
640 deploy: PerPlatform::new(project_toml.deploy.clone().unwrap_or_default()),
641 rockspec_format: project_toml.rockspec_format.clone(),
642
643 source: PerPlatform::new(RemoteRockSource {
644 local: LocalRockSource::default(),
645 source_spec: RockSourceSpec::File(self.project.root.to_path_buf()),
646 }),
647 };
648
649 let rockspec_file_name = format!("{}-{}.rockspec", validated.package, validated.version);
650
651 if validated
652 .build
653 .default
654 .copy_directories
655 .contains(&PathBuf::from(&rockspec_file_name))
656 {
657 return Err(LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(None));
658 }
659
660 for (platform, build_override) in &validated.build.per_platform {
661 if build_override
662 .copy_directories
663 .contains(&PathBuf::from(&rockspec_file_name))
664 {
665 return Err(
666 LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(Some(
667 platform.to_string(),
668 )),
669 );
670 }
671 }
672
673 Ok(validated)
674 }
675
676 pub fn into_remote(
680 &self,
681 specrev: Option<SpecRev>,
682 ) -> Result<RemoteProjectToml, RemoteProjectTomlValidationError> {
683 let version = self
684 .version_template
685 .try_generate(&self.lux_toml_dir, specrev)?;
686 let source =
687 self.source_template
688 .try_generate(&self.lux_toml_dir, &self.package, &version)?;
689 let source = PerPlatform::new(RemoteRockSource::try_from(source).map_err(|err| {
690 RemoteProjectTomlValidationError::LocalProjectTomlValidationError(
691 LocalProjectTomlValidationError::RockSource(err),
692 )
693 })?);
694 let mut local = self.into_local()?;
695 local.version = version;
696
697 let validated = RemoteProjectToml { source, local };
698
699 Ok(validated)
700 }
701
702 pub fn package(&self) -> &PackageName {
705 &self.package
706 }
707
708 pub fn version(&self) -> Result<PackageVersion, GenerateVersionError> {
710 self.version_template.try_generate(&self.lux_toml_dir, None)
711 }
712
713 pub fn merge(self, other: PartialLuaRockspec) -> Self {
716 PartialProjectToml {
717 package: other.package.unwrap_or(self.package),
718 version_template: self.version_template,
719 lua: other
720 .dependencies
721 .as_ref()
722 .and_then(|deps| {
723 deps.iter()
724 .find(|dep| dep.name() == &"lua".into())
725 .and_then(|dep| {
726 if dep.version_req().is_any() {
727 None
728 } else {
729 Some(dep.version_req().clone())
730 }
731 })
732 })
733 .or(self.lua),
734 build: other.build.unwrap_or(self.build),
735 run: self.run,
736 description: other.description.or(self.description),
737 supported_platforms: other
738 .supported_platforms
739 .map(|platform_support| platform_support.platforms().clone())
740 .or(self.supported_platforms),
741 dependencies: other
742 .dependencies
743 .map(|deps| {
744 deps.into_iter()
745 .filter(|dep| dep.name() != &"lua".into())
746 .collect()
747 })
748 .or(self.dependencies),
749 build_dependencies: other.build_dependencies.or(self.build_dependencies),
750 test_dependencies: other.test_dependencies.or(self.test_dependencies),
751 external_dependencies: other.external_dependencies.or(self.external_dependencies),
752 source_template: self.source_template,
753 test: other.test.or(self.test),
754 deploy: other.deploy.or(self.deploy),
755 rockspec_format: other.rockspec_format.or(self.rockspec_format),
756
757 project: self.project,
759 lux_toml_dir: self.lux_toml_dir.clone(),
760 }
761 }
762
763 fn resolve_lua_dependency_spec(&self, dep: LuaDependencySpec) -> LuaDependencySpec {
764 match &dep.source {
765 Some(RockSourceSpec::File(path)) if path.is_dir() => dep,
766 Some(RockSourceSpec::File(path)) => LuaDependencySpec {
767 source: Some(RockSourceSpec::File(self.lux_toml_dir.join(path))),
768 ..dep
769 },
770 _ => dep,
771 }
772 }
773}
774
775impl LuaVersionCompatibility for PartialProjectToml {
779 fn validate_lua_version(&self, version: &LuaVersion) -> Result<(), LuaVersionError> {
780 if self.supports_lua_version(version) {
781 Ok(())
782 } else {
783 Err(LuaVersionError::LuaVersionUnsupported(
784 version.clone(),
785 self.package().to_owned(),
786 self.version_template
787 .try_generate(&self.lux_toml_dir, None)
788 .unwrap_or(PackageVersion::default_dev_version()),
789 ))
790 }
791 }
792
793 fn validate_lua_version_from_config(&self, config: &Config) -> Result<(), LuaVersionError> {
794 let _ = self.lua_version_matches(config)?;
795 Ok(())
796 }
797
798 fn lua_version_matches(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
799 let version = LuaVersion::from(config)?.clone();
800 if self.supports_lua_version(&version) {
801 Ok(version)
802 } else {
803 Err(LuaVersionError::LuaVersionUnsupported(
804 version,
805 self.package.clone(),
806 self.version_template
807 .try_generate(&self.lux_toml_dir, None)
808 .unwrap_or(PackageVersion::default_dev_version()),
809 ))
810 }
811 }
812
813 fn supports_lua_version(&self, lua_version: &LuaVersion) -> bool {
814 self.lua.as_ref().is_none_or(|lua| {
815 if lua.is_luau() {
816 return matches!(lua_version, LuaVersion::Luau);
817 }
818 lua.matches(&lua_version.as_version())
819 })
820 }
821
822 fn lua_version(&self) -> Option<LuaVersion> {
823 if self.lua.as_ref().is_some_and(|lua| lua.is_luau()) {
824 return Some(LuaVersion::Luau);
825 }
826 for (possibility, version) in [
827 ("5.5.0", LuaVersion::Lua55),
828 ("5.4.0", LuaVersion::Lua54),
829 ("5.3.0", LuaVersion::Lua53),
830 ("5.2.0", LuaVersion::Lua52),
831 ("5.1.0", LuaVersion::Lua51),
832 ] {
833 let possibility = unsafe { possibility.parse().unwrap_unchecked() };
834 if self
835 .lua
836 .as_ref()
837 .is_none_or(|lua| lua.matches(&possibility))
838 {
839 return Some(version);
840 }
841 }
842 None
843 }
844}
845
846#[derive(Debug, Clone, Deserialize)]
850#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
851pub(crate) struct ProjectSpec {
852 root_dir: Option<PathBuf>,
856 #[serde(skip, default = "ProjectRoot::new")]
858 pub(crate) root: ProjectRoot,
859}
860
861impl Default for ProjectSpec {
862 fn default() -> Self {
863 Self {
864 root_dir: None,
865 root: ProjectRoot::new(),
866 }
867 }
868}
869
870#[derive(Debug, Clone, Deserialize)]
872#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
873pub struct RunSpec {
874 pub(crate) command: Option<RunCommand>,
876 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<String>>"))]
878 pub(crate) args: Option<NonEmpty<String>>,
879}
880
881#[derive(Debug)]
885pub struct LocalProjectToml {
886 package: PackageName,
887 version: PackageVersion,
888 lua: PackageVersionReq,
889 rockspec_format: Option<RockspecFormat>,
890 run: Option<PerPlatform<RunSpec>>,
891 description: RockDescription,
892 supported_platforms: PlatformSupport,
893 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
894 build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
895 external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
896 test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
897 test: PerPlatform<TestSpec>,
898 build: PerPlatform<BuildSpec>,
899 deploy: PerPlatform<DeploySpec>,
900
901 internal: PartialProjectToml,
903
904 source: PerPlatform<RemoteRockSource>,
906}
907
908impl LocalProjectToml {
909 pub fn run(&self) -> Option<&PerPlatform<RunSpec>> {
910 self.run.as_ref()
911 }
912
913 pub fn to_lua_rockspec(&self) -> Result<LocalLuaRockspec, LuaRockspecError> {
916 if let Some(dep) = self
917 .dependencies()
918 .per_platform
919 .values()
920 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
921 .collect_vec()
922 .first()
923 {
924 return Err(LuaRockspecError::OffSpecDependency(dep.name().clone()));
925 }
926 if let Some(dep) = self
927 .build_dependencies()
928 .per_platform
929 .values()
930 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
931 .collect_vec()
932 .first()
933 {
934 return Err(LuaRockspecError::OffSpecBuildDependency(dep.name().clone()));
935 }
936 if let Some(dep) = self
937 .test_dependencies()
938 .per_platform
939 .values()
940 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
941 .collect_vec()
942 .first()
943 {
944 return Err(LuaRockspecError::OffSpecTestDependency(dep.name().clone()));
945 }
946 LocalLuaRockspec::new(
947 &self.to_lua_remote_rockspec_string()?,
948 ProjectRoot(self.internal.lux_toml_dir.clone()),
949 )
950 }
951
952 pub fn to_local_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
955 Ok(LocalLuaRockspec::from_project_toml(self).into_remote_with_source(self.source.clone()))
956 }
957
958 fn render_rockspec(
959 &self,
960 version: &PackageVersion,
961 source: Option<DisplayLuaKV>,
962 ) -> Result<String, ProjectTomlError> {
963 let starter = format!(
964 r#"
965rockspec_format = "{}"
966package = "{}"
967version = "{}""#,
968 self.rockspec_format
969 .as_ref()
970 .unwrap_or(&RockspecFormat::default()),
971 self.package,
972 version
973 );
974
975 let mut template = Vec::new();
976
977 if self.description != RockDescription::default() {
978 template.push(self.description.display_lua());
979 }
980
981 if self.supported_platforms != PlatformSupport::default() {
982 template.push(self.supported_platforms.display_lua());
983 }
984
985 {
986 let mut dependencies = self.internal.dependencies.clone().unwrap_or_default();
987 if !self.lua.is_luau() {
988 dependencies.insert(
989 0,
990 PackageReq {
991 name: "lua".into(),
992 version_req: self.lua.clone(),
993 }
994 .into(),
995 );
996 }
997 template.push(Dependencies(&dependencies).display_lua());
998 }
999
1000 let mut build_dependencies = self
1001 .internal
1002 .build_dependencies
1003 .as_ref()
1004 .cloned()
1005 .unwrap_or_default();
1006
1007 let build_backend_dependency = self
1008 .internal
1009 .build
1010 .build_type
1011 .as_ref()
1012 .and_then(|build_type| build_type.luarocks_build_backend());
1013
1014 if let Some(build_backend_dependency) = build_backend_dependency {
1015 build_dependencies.push(build_backend_dependency);
1016 }
1017
1018 if !build_dependencies.is_empty() {
1019 template.push(BuildDependencies(&build_dependencies).display_lua());
1020 }
1021
1022 match self.internal.external_dependencies {
1023 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1024 template.push(ExternalDependencies(external_dependencies).display_lua());
1025 }
1026 _ => {}
1027 }
1028
1029 if let Some(source) = source {
1030 template.push(source);
1031 }
1032
1033 template.push(
1034 build_spec_with_autodetected_modules(&self.internal.build, &self.internal.lux_toml_dir)
1035 .display_lua(),
1036 );
1037
1038 let unformatted_code = std::iter::once(starter)
1039 .chain(template.into_iter().map(|kv| kv.to_string()))
1040 .join("\n\n");
1041 let result = match stylua_lib::format_code(
1042 &unformatted_code,
1043 stylua_lib::Config::default(),
1044 None,
1045 stylua_lib::OutputVerification::Full,
1046 ) {
1047 Ok(formatted_code) => formatted_code,
1048 Err(_) => unformatted_code,
1049 };
1050 validate_generated_lua(&result)?;
1051 Ok(result)
1052 }
1053}
1054
1055impl Rockspec for LocalProjectToml {
1056 type Error = ProjectTomlError;
1057
1058 fn package(&self) -> &PackageName {
1059 &self.package
1060 }
1061
1062 fn version(&self) -> &PackageVersion {
1063 &self.version
1064 }
1065
1066 fn description(&self) -> &RockDescription {
1067 &self.description
1068 }
1069
1070 fn supported_platforms(&self) -> &PlatformSupport {
1071 &self.supported_platforms
1072 }
1073
1074 fn lua(&self) -> &PackageVersionReq {
1075 &self.lua
1076 }
1077
1078 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1079 &self.dependencies
1080 }
1081
1082 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1083 &self.build_dependencies
1084 }
1085
1086 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
1087 &self.external_dependencies
1088 }
1089
1090 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1091 &self.test_dependencies
1092 }
1093
1094 fn build(&self) -> &PerPlatform<BuildSpec> {
1095 &self.build
1096 }
1097
1098 fn test(&self) -> &PerPlatform<TestSpec> {
1099 &self.test
1100 }
1101
1102 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
1103 &mut self.build
1104 }
1105
1106 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
1107 &mut self.test
1108 }
1109
1110 fn format(&self) -> &Option<RockspecFormat> {
1111 &self.rockspec_format
1112 }
1113
1114 fn source(&self) -> &PerPlatform<RemoteRockSource> {
1115 &self.source
1116 }
1117
1118 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
1119 &mut self.source
1120 }
1121
1122 fn deploy(&self) -> &PerPlatform<DeploySpec> {
1123 &self.deploy
1124 }
1125
1126 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1127 &mut self.deploy
1128 }
1129
1130 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1131 let lux_toml_dir = &self.internal.lux_toml_dir;
1132 let version = self
1133 .internal
1134 .version_template
1135 .try_generate(lux_toml_dir, None)?;
1136 let source =
1137 self.internal
1138 .source_template
1139 .try_generate(lux_toml_dir, &self.package, &version)?;
1140 self.render_rockspec(&version, Some(source.display_lua()))
1141 }
1142}
1143
1144fn build_spec_with_autodetected_modules(
1147 spec: &BuildSpecInternal,
1148 project_root: &Path,
1149) -> BuildSpecInternal {
1150 let builtin_capable = spec
1151 .build_type
1152 .as_ref()
1153 .is_none_or(|build_type| matches!(build_type, BuildType::Builtin | BuildType::None));
1154 if !builtin_capable {
1155 return spec.clone();
1156 }
1157 let detected = match autodetect_modules(project_root, Default::default()) {
1158 Ok(modules) => modules
1159 .into_iter()
1160 .filter_map(|(module, spec)| match spec {
1161 ModuleSpec::SourcePath(path)
1162 if path.extension().is_some_and(|ext| ext == "luau") =>
1163 {
1164 Some((module, path))
1165 }
1166 _ => None,
1167 })
1168 .collect_vec(),
1169 Err(_) => return spec.clone(),
1170 };
1171 if detected.is_empty() {
1172 spec.clone()
1173 } else {
1174 let mut new_spec = spec.clone();
1175 let modules = new_spec.builtin_spec.get_or_insert_with(Default::default);
1176 for (module, path) in detected {
1177 modules
1178 .entry(LuaTableKey::StringKey(module.to_string()))
1179 .or_insert(ModuleSpecInternal::SourcePath(path));
1180 }
1181 new_spec
1182 }
1183}
1184
1185fn validate_generated_lua(lua_str: &str) -> Result<(), ProjectTomlError> {
1186 let mut lua = ottavino::Lua::core();
1187 lua.try_enter(|ctx| {
1188 let closure = ottavino::Closure::load(ctx, None, lua_str.as_bytes())?;
1189 let executor = ottavino::Executor::start(ctx, closure.into(), ());
1190 if !executor.step(ctx, &mut ottavino::Fuel::with(ROCKSPEC_FUEL_LIMIT))? {
1191 return Ok(Err(ProjectTomlError::FuelLimitExceeded));
1192 }
1193 Ok(Ok(()))
1194 })
1195 .map_err(|err| ProjectTomlError::GeneratedInvalidLua(err.to_string()))?
1196}
1197
1198#[derive(Error, Debug, Diagnostic)]
1199#[non_exhaustive]
1200#[error(transparent)]
1201pub enum ProjectTomlIntegrityError {
1202 LuaRockspecError(#[from] LuaRockspecError),
1203 IoError(#[from] io::Error),
1204 #[error(transparent)]
1205 #[diagnostic(transparent)]
1206 Fs(#[from] fs::FsError),
1207}
1208
1209impl HasIntegrity for LocalProjectToml {
1210 async fn hash(&self) -> io::Result<Integrity> {
1211 match self.to_lua_rockspec() {
1212 Ok(lua_rockspec) => lua_rockspec.hash().await,
1213 Err(_) => self.internal.hash().await,
1214 }
1215 }
1216}
1217
1218#[derive(Debug)]
1220pub struct RemoteProjectToml {
1221 local: LocalProjectToml,
1222 source: PerPlatform<RemoteRockSource>,
1223}
1224
1225impl RemoteProjectToml {
1226 pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
1227 RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
1228 }
1229}
1230
1231impl Rockspec for RemoteProjectToml {
1232 type Error = ProjectTomlError;
1233
1234 fn package(&self) -> &PackageName {
1235 self.local.package()
1236 }
1237
1238 fn version(&self) -> &PackageVersion {
1239 self.local.version()
1240 }
1241
1242 fn description(&self) -> &RockDescription {
1243 self.local.description()
1244 }
1245
1246 fn supported_platforms(&self) -> &PlatformSupport {
1247 self.local.supported_platforms()
1248 }
1249
1250 fn lua(&self) -> &PackageVersionReq {
1251 self.local.lua()
1252 }
1253
1254 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1255 self.local.dependencies()
1256 }
1257
1258 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1259 self.local.build_dependencies()
1260 }
1261
1262 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
1263 self.local.external_dependencies()
1264 }
1265
1266 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1267 self.local.test_dependencies()
1268 }
1269
1270 fn build(&self) -> &PerPlatform<BuildSpec> {
1271 self.local.build()
1272 }
1273
1274 fn test(&self) -> &PerPlatform<TestSpec> {
1275 self.local.test()
1276 }
1277
1278 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
1279 self.local.build_mut()
1280 }
1281
1282 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
1283 self.local.test_mut()
1284 }
1285
1286 fn format(&self) -> &Option<RockspecFormat> {
1287 self.local.format()
1288 }
1289
1290 fn source(&self) -> &PerPlatform<RemoteRockSource> {
1291 &self.source
1292 }
1293
1294 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
1295 &mut self.source
1296 }
1297
1298 fn deploy(&self) -> &PerPlatform<DeploySpec> {
1299 self.local.deploy()
1300 }
1301
1302 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1303 self.local.deploy_mut()
1304 }
1305
1306 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1307 let lux_toml_dir = &self.local.internal.lux_toml_dir;
1308 let starter = format!(
1309 r#"
1310rockspec_format = "{}"
1311package = "{}"
1312version = "{}""#,
1313 self.local
1314 .rockspec_format
1315 .as_ref()
1316 .unwrap_or(&RockspecFormat::default()),
1317 self.local.package,
1318 self.version()
1319 );
1320
1321 let mut template = Vec::new();
1322
1323 if self.local.description != RockDescription::default() {
1324 template.push(self.local.description.display_lua());
1325 }
1326
1327 if self.local.supported_platforms != PlatformSupport::default() {
1328 template.push(self.local.supported_platforms.display_lua());
1329 }
1330
1331 {
1332 let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
1333 if !self.local.lua.is_luau() {
1334 dependencies.insert(
1335 0,
1336 PackageReq {
1337 name: "lua".into(),
1338 version_req: self.local.lua.clone(),
1339 }
1340 .into(),
1341 );
1342 }
1343 template.push(Dependencies(&dependencies).display_lua());
1344 }
1345
1346 let mut build_dependencies = self
1347 .local
1348 .internal
1349 .build_dependencies
1350 .as_ref()
1351 .cloned()
1352 .unwrap_or_default();
1353
1354 let build_backend_dependency = self
1355 .local
1356 .internal
1357 .build
1358 .build_type
1359 .as_ref()
1360 .and_then(|build_type| build_type.luarocks_build_backend());
1361
1362 if let Some(build_backend_dependency) = build_backend_dependency {
1363 build_dependencies.push(build_backend_dependency);
1364 }
1365
1366 if !build_dependencies.is_empty() {
1367 template.push(BuildDependencies(&build_dependencies).display_lua());
1368 }
1369
1370 match self.local.internal.external_dependencies {
1371 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1372 template.push(ExternalDependencies(external_dependencies).display_lua());
1373 }
1374 _ => {}
1375 }
1376
1377 let source = self.local.internal.source_template.try_generate(
1378 lux_toml_dir,
1379 &self.local.internal.package,
1380 self.version(),
1381 )?;
1382 template.push(source.display_lua());
1383
1384 if let Some(ref deploy) = self.local.internal.deploy {
1385 template.push(deploy.display_lua());
1386 }
1387
1388 template.push(
1389 build_spec_with_autodetected_modules(&self.local.internal.build, lux_toml_dir)
1390 .display_lua(),
1391 );
1392
1393 let unformatted_code = std::iter::once(starter)
1394 .chain(template.into_iter().map(|kv| kv.to_string()))
1395 .join("\n\n");
1396 let result = match stylua_lib::format_code(
1397 &unformatted_code,
1398 stylua_lib::Config::default(),
1399 None,
1400 stylua_lib::OutputVerification::Full,
1401 ) {
1402 Ok(formatted_code) => formatted_code,
1403 Err(_) => unformatted_code,
1404 };
1405 validate_generated_lua(&result)?;
1406 Ok(result)
1407 }
1408}
1409
1410impl HasIntegrity for RemoteProjectToml {
1411 async fn hash(&self) -> io::Result<Integrity> {
1412 self.to_lua_rockspec()
1413 .map_err(|err| {
1414 io::Error::other(format!(
1415 "unable to convert remote project to Lua rockspec:\n{}",
1416 err
1417 ))
1418 })?
1419 .hash()
1420 .await
1421 }
1422}
1423
1424#[cfg(test)]
1425mod tests {
1426 use std::path::PathBuf;
1427
1428 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1429 use git2::Repository;
1430 use url::Url;
1431
1432 use crate::{
1433 git::{url::RemoteGitUrl, GitRef, GitSource},
1434 lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1435 project::{Project, ProjectRoot, PROJECT_TOML},
1436 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1437 };
1438
1439 use super::PartialProjectToml;
1440
1441 #[test]
1442 fn project_toml_parsing() {
1443 let project_toml = r#"
1444 package = "my-package"
1445 version = "1.0.0"
1446 lua = "5.3"
1447
1448 rockspec_format = "1.0"
1449
1450 [source]
1451 url = "https://example.com"
1452
1453 [dependencies]
1454 foo = "1.0"
1455 bar = ">=2.0"
1456
1457 [run]
1458 args = ["--foo", "--bar"]
1459
1460 [build]
1461 type = "builtin"
1462 "#;
1463
1464 let project =
1465 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1466 let _ = project.into_remote(None).unwrap();
1467
1468 let project_toml = r#"
1469 package = "my-package"
1470 version = "1.0.0"
1471 lua = "5.1"
1472
1473 [description]
1474 summary = "A summary"
1475 detailed = "A detailed description"
1476 license = "MIT"
1477 homepage = "https://example.com"
1478 issues_url = "https://example.com/issues"
1479 maintainer = "John Doe"
1480 labels = ["label1", "label2"]
1481
1482 [supported_platforms]
1483 linux = true
1484 windows = false
1485
1486 [dependencies]
1487 foo = "1.0"
1488 bar = ">=2.0"
1489
1490 [build_dependencies]
1491 baz = "1.0"
1492
1493 [external_dependencies.foo]
1494 header = "foo.h"
1495
1496 [external_dependencies.bar]
1497 library = "libbar.so"
1498
1499 [test_dependencies]
1500 busted = "69.420"
1501
1502 [source]
1503 url = "https://example.com"
1504 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1505 file = "my-package-1.0.0.tar.gz"
1506 dir = "my-package-1.0.0"
1507
1508 [test]
1509 type = "command"
1510 script = "test.lua"
1511 flags = [ "foo", "bar" ]
1512
1513 [run]
1514 command = "my-command"
1515 args = ["--foo", "--bar"]
1516
1517 [build]
1518 type = "builtin"
1519 "#;
1520
1521 let project =
1522 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1523 let _ = project.into_remote(None).unwrap();
1524 }
1525
1526 #[test]
1527 fn compare_project_toml_with_rockspec() {
1528 let project_toml = r#"
1529 package = "my-package"
1530 version = "1.0.0"
1531 lua = "5.1"
1532
1533 # For testing, specify a custom rockspec format
1534 # (defaults to 3.0)
1535 rockspec_format = "1.0"
1536
1537 [description]
1538 summary = "A summary"
1539 detailed = "A detailed description"
1540 license = "MIT"
1541 homepage = "https://example.com"
1542 issues_url = "https://example.com/issues"
1543 maintainer = "John Doe"
1544 labels = ["label1", "label2"]
1545
1546 [supported_platforms]
1547 linux = true
1548 windows = false
1549
1550 [dependencies]
1551 foo = "1.0"
1552 bar = ">=2.0"
1553
1554 [build_dependencies]
1555 baz = "1.0"
1556
1557 [external_dependencies.foo]
1558 header = "foo.h"
1559
1560 [external_dependencies.bar]
1561 library = "libbar.so"
1562
1563 [test_dependencies]
1564 busted = "1.0"
1565
1566 [source]
1567 url = "https://example.com"
1568 file = "my-package-1.0.0.tar.gz"
1569 dir = "my-package-1.0.0"
1570
1571 [test]
1572 type = "command"
1573 script = "test.lua"
1574 flags = [ "foo", "bar" ]
1575
1576 [run]
1577 command = "my-command"
1578 args = ["--foo", "--bar"]
1579
1580 [deploy]
1581 wrap_bin_scripts = false
1582
1583 [build]
1584 type = "builtin"
1585
1586 [build.install.lua]
1587 "foo.bar" = "src/bar.lua"
1588
1589 [build.install.lib]
1590 "foo.baz" = "src/baz.c"
1591
1592 [build.install.bin]
1593 "bla" = "src/bla"
1594
1595 [build.install.conf]
1596 "cfg.conf" = "resources/config.conf"
1597 "#;
1598
1599 let expected_rockspec = r#"
1600 rockspec_format = "1.0"
1601 package = "my-package"
1602 version = "1.0.0"
1603
1604 source = {
1605 url = "https://example.com",
1606 file = "my-package-1.0.0.tar.gz",
1607 dir = "my-package-1.0.0",
1608 }
1609
1610 description = {
1611 summary = "A summary",
1612 detailed = "A detailed description",
1613 license = "MIT",
1614 homepage = "https://example.com",
1615 issues_url = "https://example.com/issues",
1616 maintainer = "John Doe",
1617 labels = {"label1", "label2"},
1618 }
1619
1620 supported_platforms = {"linux", "!windows"}
1621
1622 dependencies = {
1623 "lua ==5.1",
1624 "foo ==1.0",
1625 "bar >=2.0",
1626 }
1627
1628 build_dependencies = {
1629 "baz ==1.0",
1630 }
1631
1632 external_dependencies = {
1633 foo = { header = "foo.h" },
1634 bar = { library = "libbar.so" },
1635 }
1636
1637 source = {
1638 url = "https://example.com",
1639 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1640 file = "my-package-1.0.0.tar.gz",
1641 dir = "my-package-1.0.0",
1642 }
1643
1644 test = {
1645 type = "command",
1646 script = "test.lua",
1647 flags = {"foo", "bar"},
1648 }
1649
1650 deploy = {
1651 wrap_bin_scripts = false,
1652 }
1653
1654 build = {
1655 type = "builtin",
1656 install = {
1657 lua = {
1658 ["foo.bar"] = "src/bar.lua",
1659 },
1660 lib = {
1661 ["foo.baz"] = "src/baz.c",
1662 },
1663 bin = {
1664 bla = "src/bla",
1665 },
1666 conf = {
1667 ["cfg.conf"] = "resources/config.conf",
1668 },
1669 },
1670 }
1671 "#;
1672
1673 let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1674
1675 let project_toml =
1676 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1677 let rockspec = project_toml
1678 .into_remote(None)
1679 .unwrap()
1680 .to_lua_rockspec()
1681 .unwrap();
1682
1683 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1684 let mut v = v.current_platform().clone();
1685 v.sort_by(|a, b| a.name().cmp(b.name()));
1686 v
1687 };
1688
1689 assert_eq!(rockspec.package(), expected_rockspec.package());
1690 assert_eq!(rockspec.version(), expected_rockspec.version());
1691 assert_eq!(rockspec.description(), expected_rockspec.description());
1692 assert_eq!(
1693 rockspec.supported_platforms(),
1694 expected_rockspec.supported_platforms()
1695 );
1696 assert_eq!(
1697 sorted_package_reqs(rockspec.dependencies()),
1698 sorted_package_reqs(expected_rockspec.dependencies())
1699 );
1700 assert_eq!(
1701 sorted_package_reqs(rockspec.build_dependencies()),
1702 sorted_package_reqs(expected_rockspec.build_dependencies())
1703 );
1704 assert_eq!(
1705 rockspec.external_dependencies(),
1706 expected_rockspec.external_dependencies()
1707 );
1708 assert_eq!(rockspec.source(), expected_rockspec.source());
1709 assert_eq!(rockspec.build(), expected_rockspec.build());
1710 assert_eq!(rockspec.format(), expected_rockspec.format());
1711 }
1712
1713 #[test]
1714 fn merge_project_toml_with_partial_rockspec() {
1715 let project_toml = r#"
1716 package = "my-package"
1717 version = "1.0.0"
1718 lua = "5.1"
1719
1720 # For testing, specify a custom rockspec format
1721 # (defaults to 3.0)
1722 rockspec_format = "1.0"
1723
1724 [description]
1725 summary = "A summary"
1726 detailed = "A detailed description"
1727 license = "MIT"
1728 homepage = "https://example.com"
1729 issues_url = "https://example.com/issues"
1730 maintainer = "John Doe"
1731 labels = ["label1", "label2"]
1732
1733 [supported_platforms]
1734 linux = true
1735 windows = false
1736
1737 [dependencies]
1738 foo = "1.0"
1739 bar = ">=2.0"
1740
1741 [build_dependencies]
1742 baz = "1.0"
1743
1744 [external_dependencies.foo]
1745 header = "foo.h"
1746
1747 [external_dependencies.bar]
1748 library = "libbar.so"
1749
1750 [test_dependencies]
1751 busted = "1.0"
1752
1753 [source]
1754 url = "https://example.com"
1755 file = "my-package-1.0.0.tar.gz"
1756 dir = "my-package-1.0.0"
1757
1758 [test]
1759 type = "command"
1760 script = "test.lua"
1761 flags = [ "foo", "bar" ]
1762
1763 [run]
1764 command = "my-command"
1765 args = [ "--foo", "--bar" ]
1766
1767 [build]
1768 type = "builtin"
1769 "#;
1770
1771 let mergable_rockspec_content = r#"
1772 rockspec_format = "1.0"
1773 package = "my-package-overwritten"
1774
1775 description = {
1776 summary = "A summary overwritten",
1777 detailed = "A detailed description overwritten",
1778 license = "GPL-2.0",
1779 homepage = "https://example.com/overwritten",
1780 issues_url = "https://example.com/issues/overwritten",
1781 maintainer = "John Doe Overwritten",
1782 labels = {"over", "written"},
1783 }
1784
1785 -- Inverted supported platforms
1786 supported_platforms = {"!linux", "windows"}
1787
1788 dependencies = {
1789 "lua 5.1",
1790 "foo >1.0",
1791 "bar <=2.0",
1792 }
1793
1794 build_dependencies = {
1795 "baz >1.0",
1796 }
1797
1798 external_dependencies = {
1799 foo = { header = "overwritten.h" },
1800 bar = { library = "overwritten.so" },
1801 }
1802
1803 test = {
1804 type = "command",
1805 script = "overwritten.lua",
1806 flags = {"over", "written"},
1807 }
1808
1809 build = {
1810 type = "builtin",
1811 }
1812 "#;
1813
1814 let remote_rockspec_content = format!(
1815 r#"{}
1816 version = "1.0.0"
1817 source = {{
1818 url = "https://example.com",
1819 file = "my-package-1.0.0.tar.gz",
1820 dir = "my-package-1.0.0",
1821 }}
1822 "#,
1823 mergable_rockspec_content
1824 );
1825
1826 let project_toml =
1827 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1828 let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1829 let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1830
1831 let merged = project_toml
1832 .merge(partial_rockspec)
1833 .into_remote(None)
1834 .unwrap();
1835
1836 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1837 let mut v = v.current_platform().clone();
1838 v.sort_by(|a, b| a.name().cmp(b.name()));
1839 v
1840 };
1841
1842 assert_eq!(merged.package(), expected_rockspec.package());
1843 assert_eq!(merged.version(), expected_rockspec.version());
1844 assert_eq!(merged.description(), expected_rockspec.description());
1845 assert_eq!(
1846 merged.supported_platforms(),
1847 expected_rockspec.supported_platforms()
1848 );
1849 assert_eq!(
1850 sorted_package_reqs(merged.dependencies()),
1851 sorted_package_reqs(expected_rockspec.dependencies())
1852 );
1853 assert_eq!(
1854 sorted_package_reqs(merged.build_dependencies()),
1855 sorted_package_reqs(expected_rockspec.build_dependencies())
1856 );
1857 assert_eq!(
1858 merged.external_dependencies(),
1859 expected_rockspec.external_dependencies()
1860 );
1861 assert_eq!(merged.source(), expected_rockspec.source());
1862 assert_eq!(merged.build(), expected_rockspec.build());
1863 assert_eq!(merged.format(), expected_rockspec.format());
1864 assert!(merged.local.run().is_some());
1866 }
1867
1868 #[test]
1869 fn wally_dependency_parsing() {
1870 let project_toml = r#"
1871 package = "my-package"
1872 version = "1.0.0"
1873 lua = "5.1"
1874
1875 [dependencies.roact]
1876 wally = "jsdotperf/roact@^1.4.0"
1877
1878 [build]
1879 type = "builtin"
1880 "#;
1881
1882 let project =
1883 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1884 let deps = project.dependencies.unwrap();
1885 assert_eq!(deps.len(), 1);
1886 assert_eq!(deps[0].name().to_string(), "roact");
1887 assert!(matches!(
1888 deps[0].source(),
1889 Some(RockSourceSpec::Wally(req)) if req.name().to_string() == "jsdotperf/roact"
1890 ));
1891 }
1892
1893 #[test]
1894 fn wally_dependency_conflicts_with_version() {
1895 let project_toml = r#"
1896 package = "my-package"
1897 version = "1.0.0"
1898 lua = "5.1"
1899
1900 [dependencies.roact]
1901 wally = "jsdotperf/roact@^1.4.0"
1902 version = "1.4.0"
1903
1904 [build]
1905 type = "builtin"
1906 "#;
1907
1908 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap_err();
1909 }
1910
1911 #[test]
1912 fn project_toml_with_lua_in_dependencies() {
1913 let project_toml = r#"
1914 package = "my-package"
1915 version = "1.0.0"
1916 # lua = ">5.1"
1917
1918 [dependencies]
1919 lua = "5.1" # disallowed
1920
1921 [build]
1922 type = "builtin"
1923 "#;
1924
1925 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default())
1926 .unwrap()
1927 .into_local()
1928 .unwrap_err();
1929 }
1930
1931 #[test]
1932 fn project_toml_with_invalid_run_command() {
1933 for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1934 let project_toml = format!(
1935 r#"
1936 package = "my-package"
1937 version = "1.0.0"
1938 lua = "5.1"
1939
1940 [build]
1941 type = "builtin"
1942
1943 [run]
1944 command = "{command}"
1945 "#,
1946 );
1947
1948 PartialProjectToml::new(PROJECT_TOML, &project_toml, ProjectRoot::default())
1949 .unwrap_err();
1950 }
1951 }
1952
1953 #[test]
1954 fn project_root_dir_override_and_rockspec_generation() {
1955 let project_toml = r#"
1956 package = "my-package"
1957 version = "1.0.0"
1958 lua = "5.1"
1959
1960 [project]
1961 root_dir = ".."
1962
1963 [source]
1964 url = "https://example.com"
1965
1966 [build]
1967 type = "builtin"
1968 "#;
1969
1970 let workspace_root = assert_fs::TempDir::new().unwrap();
1971 let lux_toml_dir = workspace_root.child("proj");
1972 lux_toml_dir.create_dir_all().unwrap();
1973 std::fs::write(lux_toml_dir.join(PROJECT_TOML), project_toml).unwrap();
1974
1975 let project = PartialProjectToml::new(
1976 PROJECT_TOML,
1977 project_toml,
1978 ProjectRoot(lux_toml_dir.to_path_buf()),
1979 )
1980 .unwrap();
1981 let local = project.into_local().unwrap();
1982 let RockSourceSpec::File(root_dir) = &local.source().current_platform().source_spec else {
1983 panic!("expected a file source");
1984 };
1985 assert_eq!(
1986 root_dir,
1987 &std::fs::canonicalize(workspace_root.path()).unwrap()
1988 );
1989
1990 let rockspec = local.to_lua_rockspec().unwrap();
1992 assert!(!rockspec
1993 .to_lua_remote_rockspec_string()
1994 .unwrap()
1995 .contains("root_dir"));
1996 }
1997
1998 #[test]
1999 fn generate_non_deterministic_git_source() {
2000 let rockspec_content = r#"
2001 package = "test-package"
2002 version = "1.0.0"
2003 lua = ">=5.1"
2004
2005 [source]
2006 url = "git+https://exaple.com/repo.git"
2007
2008 [build]
2009 type = "builtin"
2010 "#;
2011
2012 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
2013 .unwrap()
2014 .into_remote(None)
2015 .unwrap_err();
2016 }
2017
2018 #[test]
2019 fn generate_deterministic_git_source() {
2020 let rockspec_content = r#"
2021 package = "test-package"
2022 version = "1.0.0"
2023 lua = ">=5.1"
2024
2025 [source]
2026 url = "git+https://exaple.com/owner/repo.git"
2027 tag = "v0.1.0"
2028
2029 [build]
2030 type = "builtin"
2031 "#;
2032
2033 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
2034 .unwrap()
2035 .into_remote(None)
2036 .unwrap();
2037 }
2038
2039 #[test]
2040 fn generate_git_source_with_branch() {
2041 let rockspec_content = r#"
2042 package = "test-package"
2043 version = "1.0.0"
2044 lua = ">=5.1"
2045
2046 [source]
2047 url = "git+https://example.com/owner/repo.git"
2048 branch = "main"
2049
2050 [build]
2051 type = "builtin"
2052 "#;
2053
2054 let toml = PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
2055 .unwrap()
2056 .into_remote(None)
2057 .unwrap();
2058
2059 let lua = toml.to_lua_remote_rockspec_string().unwrap();
2060 assert!(lua.contains("branch = \"main\""));
2061 assert!(!lua.contains("tag = "));
2062 }
2063
2064 fn init_git_repo(path: &std::path::Path) -> Repository {
2065 let repo = Repository::init(path).unwrap();
2066 {
2067 let tree_id = repo.index().unwrap().write_tree().unwrap();
2068 let tree = repo.find_tree(tree_id).unwrap();
2069 let sig = git2::Signature::now("name", "email").unwrap();
2070 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
2071 .unwrap();
2072 }
2073 repo
2074 }
2075
2076 fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
2077 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2078 .join("resources/test/sample-projects/source-template/");
2079 temp_dir.copy_from(&sample_project, &["**"]).unwrap();
2080 init_git_repo(temp_dir.path())
2081 }
2082
2083 fn create_tag(repo: &Repository, name: &str) {
2084 let sig = git2::Signature::now("name", "email").unwrap();
2085 let id = repo.head().unwrap().target().unwrap();
2086 let obj = repo.find_object(id, None).unwrap();
2087 repo.tag(name, &obj, &sig, "msg", true).unwrap();
2088 }
2089
2090 #[test]
2091 fn test_git_project_generate_dev_source() {
2092 let project_root = assert_fs::TempDir::new().unwrap();
2093 init_sample_project_repo(&project_root);
2094 let project = Project::from_exact(&project_root).unwrap().unwrap();
2095 let remote_project_toml = project.toml().into_remote(None).unwrap();
2096 let source = remote_project_toml.source.current_platform();
2097 let source_spec = &source.source_spec;
2098 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
2099 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
2100 let expected_url: RemoteGitUrl =
2101 "https://github.com/lumen-oss/lux.git".parse().unwrap();
2102 assert_eq!(url, &expected_url);
2103 assert!(git_ref.is_some());
2104 }
2105 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
2106 }
2107
2108 #[test]
2109 fn test_git_project_generate_non_semver_tag_source() {
2110 let project_root = assert_fs::TempDir::new().unwrap();
2111 let repo = init_sample_project_repo(&project_root);
2112 let tag_name = "bla";
2113 create_tag(&repo, tag_name);
2114 let project = Project::from_exact(&project_root).unwrap().unwrap();
2115 let remote_project_toml = project.toml().into_remote(None).unwrap();
2116 let source = remote_project_toml.source.current_platform();
2117 let source_spec = &source.source_spec;
2118 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
2119 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
2120 let expected_url: RemoteGitUrl =
2121 "https://github.com/lumen-oss/lux.git".parse().unwrap();
2122 assert_eq!(url, &expected_url);
2123 assert_eq!(git_ref, &Some(GitRef::Tag(tag_name.to_string())));
2124 }
2125 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
2126 }
2127
2128 #[test]
2129 fn test_git_project_generate_release_source_tag_with_v_prefix() {
2130 let project_root = assert_fs::TempDir::new().unwrap();
2131 let repo = init_sample_project_repo(&project_root);
2132 let tag_name = "v1.0.0";
2133 create_tag(&repo, "bla");
2134 create_tag(&repo, tag_name);
2135 let project = Project::from_exact(&project_root).unwrap().unwrap();
2136 let remote_project_toml = project.toml().into_remote(None).unwrap();
2137 let source = remote_project_toml.source.current_platform();
2138 let source_spec = &source.source_spec;
2139 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2140 if let RockSourceSpec::Url(url) = source_spec {
2141 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
2142 .parse()
2143 .unwrap();
2144 assert_eq!(url, &expected_url);
2145 }
2146 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2147 }
2148
2149 #[test]
2150 fn test_git_project_generate_release_source_tag_without_v_prefix() {
2151 let project_root = assert_fs::TempDir::new().unwrap();
2152 let repo = init_sample_project_repo(&project_root);
2153 create_tag(&repo, "bla");
2154 let tag_name = "1.0.0";
2155 create_tag(&repo, tag_name);
2156 let project = Project::from_exact(&project_root).unwrap().unwrap();
2157 let remote_project_toml = project.toml().into_remote(None).unwrap();
2158 let source = remote_project_toml.source.current_platform();
2159 let source_spec = &source.source_spec;
2160 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2161 if let RockSourceSpec::Url(url) = source_spec {
2162 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
2163 .parse()
2164 .unwrap();
2165 assert_eq!(url, &expected_url);
2166 }
2167 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2168 }
2169
2170 #[test]
2171 fn test_git_project_in_subdirectory() {
2172 let temp_dir = assert_fs::TempDir::new().unwrap();
2173 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2174 .join("resources/test/sample-projects/source-template/");
2175 let project_dir = temp_dir.child("lux");
2176 project_dir.create_dir_all().unwrap();
2177 project_dir.copy_from(&sample_project, &["**"]).unwrap();
2178 let repo = init_git_repo(temp_dir.path());
2179 create_tag(&repo, "bla");
2180 let tag_name = "1.0.0";
2181 create_tag(&repo, tag_name);
2182 let project = Project::from_exact(&project_dir).unwrap().unwrap();
2183 let remote_project_toml = project.toml().into_remote(None).unwrap();
2184 let source = remote_project_toml.source.current_platform();
2185 let source_spec = &source.source_spec;
2186 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2187 if let RockSourceSpec::Url(url) = source_spec {
2188 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
2189 .parse()
2190 .unwrap();
2191 assert_eq!(url, &expected_url);
2192 }
2193 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2194 }
2195
2196 #[test]
2197 fn test_git_project_in_subdirectory_generate_dev_source() {
2198 let temp_dir = assert_fs::TempDir::new().unwrap();
2199 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2200 .join("resources/test/sample-projects/source-template/");
2201 let project_dir = temp_dir.child("lux");
2202 project_dir.create_dir_all().unwrap();
2203 project_dir.copy_from(&sample_project, &["**"]).unwrap();
2204 init_git_repo(temp_dir.path());
2205
2206 let project = Project::from_exact(&project_dir).unwrap().unwrap();
2207 let remote_project_toml = project.toml().into_remote(None).unwrap();
2208 let RockSourceSpec::Git(GitSource { git_ref, .. }) =
2209 &remote_project_toml.source.current_platform().source_spec
2210 else {
2211 panic!("expected a git source");
2212 };
2213 assert!(git_ref.is_some());
2214 }
2215}