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;
10use crate::lua_rockspec::DeploySpec;
11use crate::lua_rockspec::LocalLuaRockspec;
12use crate::lua_rockspec::LocalRockSource;
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::new(
644 LocalRockSource::default(),
645 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 self.into_remote_with_version(None, specrev)
684 }
685
686 pub fn into_remote_with_version(
689 &self,
690 version: Option<PackageVersion>,
691 specrev: Option<SpecRev>,
692 ) -> Result<RemoteProjectToml, RemoteProjectTomlValidationError> {
693 let version = match version {
694 Some(version) => version,
695 None => self
696 .version_template
697 .try_generate(&self.lux_toml_dir, specrev)?,
698 };
699 let source =
700 self.source_template
701 .try_generate(&self.lux_toml_dir, &self.package, &version)?;
702 let source = PerPlatform::new(RemoteRockSource::try_from(source).map_err(|err| {
703 RemoteProjectTomlValidationError::LocalProjectTomlValidationError(
704 LocalProjectTomlValidationError::RockSource(err),
705 )
706 })?);
707 let mut local = self.into_local()?;
708 local.version = version;
709
710 let validated = RemoteProjectToml { source, local };
711
712 Ok(validated)
713 }
714
715 pub fn package(&self) -> &PackageName {
718 &self.package
719 }
720
721 pub fn version(&self) -> Result<PackageVersion, GenerateVersionError> {
723 self.version_template.try_generate(&self.lux_toml_dir, None)
724 }
725
726 pub fn merge(self, other: PartialLuaRockspec) -> Self {
729 PartialProjectToml {
730 package: other.package.unwrap_or(self.package),
731 version_template: self.version_template,
732 lua: other
733 .dependencies
734 .as_ref()
735 .and_then(|deps| {
736 deps.iter()
737 .find(|dep| dep.name() == &"lua".into())
738 .and_then(|dep| {
739 if dep.version_req().is_any() {
740 None
741 } else {
742 Some(dep.version_req().clone())
743 }
744 })
745 })
746 .or(self.lua),
747 build: other.build.unwrap_or(self.build),
748 run: self.run,
749 description: other.description.or(self.description),
750 supported_platforms: other
751 .supported_platforms
752 .map(|platform_support| platform_support.platforms().clone())
753 .or(self.supported_platforms),
754 dependencies: other
755 .dependencies
756 .map(|deps| {
757 deps.into_iter()
758 .filter(|dep| dep.name() != &"lua".into())
759 .collect()
760 })
761 .or(self.dependencies),
762 build_dependencies: other.build_dependencies.or(self.build_dependencies),
763 test_dependencies: other.test_dependencies.or(self.test_dependencies),
764 external_dependencies: other.external_dependencies.or(self.external_dependencies),
765 source_template: self.source_template,
766 test: other.test.or(self.test),
767 deploy: other.deploy.or(self.deploy),
768 rockspec_format: other.rockspec_format.or(self.rockspec_format),
769
770 project: self.project,
772 lux_toml_dir: self.lux_toml_dir.clone(),
773 }
774 }
775
776 fn resolve_lua_dependency_spec(&self, dep: LuaDependencySpec) -> LuaDependencySpec {
777 match &dep.source {
778 Some(RockSourceSpec::File(path)) if path.is_dir() => dep,
779 Some(RockSourceSpec::File(path)) => LuaDependencySpec {
780 source: Some(RockSourceSpec::File(self.lux_toml_dir.join(path))),
781 ..dep
782 },
783 _ => dep,
784 }
785 }
786}
787
788impl LuaVersionCompatibility for PartialProjectToml {
792 fn validate_lua_version(&self, version: &LuaVersion) -> Result<(), LuaVersionError> {
793 if self.supports_lua_version(version) {
794 Ok(())
795 } else {
796 Err(LuaVersionError::LuaVersionUnsupported(
797 version.clone(),
798 self.package().to_owned(),
799 self.version_template
800 .try_generate(&self.lux_toml_dir, None)
801 .unwrap_or(PackageVersion::default_dev_version()),
802 ))
803 }
804 }
805
806 fn validate_lua_version_from_config(&self, config: &Config) -> Result<(), LuaVersionError> {
807 let _ = self.lua_version_matches(config)?;
808 Ok(())
809 }
810
811 fn lua_version_matches(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
812 let version = LuaVersion::from(config)?.clone();
813 if self.supports_lua_version(&version) {
814 Ok(version)
815 } else {
816 Err(LuaVersionError::LuaVersionUnsupported(
817 version,
818 self.package.clone(),
819 self.version_template
820 .try_generate(&self.lux_toml_dir, None)
821 .unwrap_or(PackageVersion::default_dev_version()),
822 ))
823 }
824 }
825
826 fn supports_lua_version(&self, lua_version: &LuaVersion) -> bool {
827 self.lua.as_ref().is_none_or(|lua| {
828 if lua.is_luau() {
829 return matches!(lua_version, LuaVersion::Luau);
830 }
831 lua.matches(&lua_version.as_version())
832 })
833 }
834
835 fn lua_version(&self) -> Option<LuaVersion> {
836 if self.lua.as_ref().is_some_and(|lua| lua.is_luau()) {
837 return Some(LuaVersion::Luau);
838 }
839 for (possibility, version) in [
840 ("5.5.0", LuaVersion::Lua55),
841 ("5.4.0", LuaVersion::Lua54),
842 ("5.3.0", LuaVersion::Lua53),
843 ("5.2.0", LuaVersion::Lua52),
844 ("5.1.0", LuaVersion::Lua51),
845 ] {
846 let possibility = unsafe { possibility.parse().unwrap_unchecked() };
847 if self
848 .lua
849 .as_ref()
850 .is_none_or(|lua| lua.matches(&possibility))
851 {
852 return Some(version);
853 }
854 }
855 None
856 }
857}
858
859#[derive(Debug, Clone, Deserialize)]
863#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
864pub(crate) struct ProjectSpec {
865 root_dir: Option<PathBuf>,
869 #[serde(skip, default = "ProjectRoot::new")]
871 pub(crate) root: ProjectRoot,
872}
873
874impl Default for ProjectSpec {
875 fn default() -> Self {
876 Self {
877 root_dir: None,
878 root: ProjectRoot::new(),
879 }
880 }
881}
882
883#[derive(Debug, Clone, Deserialize)]
885#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
886pub struct RunSpec {
887 pub(crate) command: Option<RunCommand>,
889 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<String>>"))]
891 pub(crate) args: Option<NonEmpty<String>>,
892}
893
894#[derive(Debug)]
898pub struct LocalProjectToml {
899 package: PackageName,
900 version: PackageVersion,
901 lua: PackageVersionReq,
902 rockspec_format: Option<RockspecFormat>,
903 run: Option<PerPlatform<RunSpec>>,
904 description: RockDescription,
905 supported_platforms: PlatformSupport,
906 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
907 build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
908 external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
909 test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
910 test: PerPlatform<TestSpec>,
911 build: PerPlatform<BuildSpec>,
912 deploy: PerPlatform<DeploySpec>,
913
914 internal: PartialProjectToml,
916
917 source: PerPlatform<RemoteRockSource>,
919}
920
921impl LocalProjectToml {
922 pub fn run(&self) -> Option<&PerPlatform<RunSpec>> {
923 self.run.as_ref()
924 }
925
926 pub fn to_lua_rockspec(&self) -> Result<LocalLuaRockspec, lua_rockspec::Error> {
929 if let Some(dep) = self
930 .dependencies()
931 .per_platform
932 .values()
933 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
934 .collect_vec()
935 .first()
936 {
937 return Err(lua_rockspec::Error::OffSpecDependency(dep.name().clone()));
938 }
939 if let Some(dep) = self
940 .build_dependencies()
941 .per_platform
942 .values()
943 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
944 .collect_vec()
945 .first()
946 {
947 return Err(lua_rockspec::Error::OffSpecBuildDependency(
948 dep.name().clone(),
949 ));
950 }
951 if let Some(dep) = self
952 .test_dependencies()
953 .per_platform
954 .values()
955 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
956 .collect_vec()
957 .first()
958 {
959 return Err(lua_rockspec::Error::OffSpecTestDependency(
960 dep.name().clone(),
961 ));
962 }
963 LocalLuaRockspec::new(
964 &self.to_lua_remote_rockspec_string()?,
965 ProjectRoot(self.internal.lux_toml_dir.clone()),
966 )
967 }
968
969 pub fn to_local_rockspec(&self) -> Result<RemoteLuaRockspec, lua_rockspec::Error> {
972 let content = self.to_lua_local_rockspec_string()?;
973 Ok(LocalLuaRockspec::from_project_toml(self, content)
974 .into_remote_with_source(self.source.clone()))
975 }
976
977 fn to_lua_local_rockspec_string(&self) -> Result<String, ProjectTomlError> {
980 self.render_rockspec(
981 &self.version,
982 Some(self.source.current_platform().source_spec.display_lua()),
983 )
984 }
985
986 fn render_rockspec(
987 &self,
988 version: &PackageVersion,
989 source: Option<DisplayLuaKV>,
990 ) -> Result<String, ProjectTomlError> {
991 let starter = format!(
992 r#"
993rockspec_format = "{}"
994package = "{}"
995version = "{}""#,
996 self.rockspec_format
997 .as_ref()
998 .unwrap_or(&RockspecFormat::default()),
999 self.package,
1000 version
1001 );
1002
1003 let mut template = Vec::new();
1004
1005 if self.description != RockDescription::default() {
1006 template.push(self.description.display_lua());
1007 }
1008
1009 if self.supported_platforms != PlatformSupport::default() {
1010 template.push(self.supported_platforms.display_lua());
1011 }
1012
1013 {
1014 let mut dependencies = self.internal.dependencies.clone().unwrap_or_default();
1015 if !self.lua.is_luau() {
1016 dependencies.insert(
1017 0,
1018 PackageReq {
1019 name: "lua".into(),
1020 version_req: self.lua.clone(),
1021 }
1022 .into(),
1023 );
1024 }
1025 template.push(Dependencies(&dependencies).display_lua());
1026 }
1027
1028 let mut build_dependencies = self
1029 .internal
1030 .build_dependencies
1031 .as_ref()
1032 .cloned()
1033 .unwrap_or_default();
1034
1035 let build_backend_dependency = self
1036 .internal
1037 .build
1038 .build_type
1039 .as_ref()
1040 .and_then(|build_type| build_type.luarocks_build_backend());
1041
1042 if let Some(build_backend_dependency) = build_backend_dependency {
1043 build_dependencies.push(build_backend_dependency);
1044 }
1045
1046 if !build_dependencies.is_empty() {
1047 template.push(BuildDependencies(&build_dependencies).display_lua());
1048 }
1049
1050 match self.internal.external_dependencies {
1051 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1052 template.push(ExternalDependencies(external_dependencies).display_lua());
1053 }
1054 _ => {}
1055 }
1056
1057 if let Some(source) = source {
1058 template.push(source);
1059 }
1060
1061 template.push(
1062 build_spec_with_autodetected_modules(&self.internal.build, &self.internal.lux_toml_dir)
1063 .display_lua(),
1064 );
1065
1066 let unformatted_code = std::iter::once(starter)
1067 .chain(template.into_iter().map(|kv| kv.to_string()))
1068 .join("\n\n");
1069 let result = match stylua_lib::format_code(
1070 &unformatted_code,
1071 stylua_lib::Config::default(),
1072 None,
1073 stylua_lib::OutputVerification::Full,
1074 ) {
1075 Ok(formatted_code) => formatted_code,
1076 Err(_) => unformatted_code,
1077 };
1078 validate_generated_lua(&result)?;
1079 Ok(result)
1080 }
1081}
1082
1083impl Rockspec for LocalProjectToml {
1084 type Error = ProjectTomlError;
1085
1086 fn package(&self) -> &PackageName {
1087 &self.package
1088 }
1089
1090 fn version(&self) -> &PackageVersion {
1091 &self.version
1092 }
1093
1094 fn description(&self) -> &RockDescription {
1095 &self.description
1096 }
1097
1098 fn supported_platforms(&self) -> &PlatformSupport {
1099 &self.supported_platforms
1100 }
1101
1102 fn lua(&self) -> &PackageVersionReq {
1103 &self.lua
1104 }
1105
1106 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1107 &self.dependencies
1108 }
1109
1110 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1111 &self.build_dependencies
1112 }
1113
1114 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
1115 &self.external_dependencies
1116 }
1117
1118 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1119 &self.test_dependencies
1120 }
1121
1122 fn build(&self) -> &PerPlatform<BuildSpec> {
1123 &self.build
1124 }
1125
1126 fn test(&self) -> &PerPlatform<TestSpec> {
1127 &self.test
1128 }
1129
1130 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
1131 &mut self.build
1132 }
1133
1134 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
1135 &mut self.test
1136 }
1137
1138 fn format(&self) -> &Option<RockspecFormat> {
1139 &self.rockspec_format
1140 }
1141
1142 fn source(&self) -> &PerPlatform<RemoteRockSource> {
1143 &self.source
1144 }
1145
1146 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
1147 &mut self.source
1148 }
1149
1150 fn deploy(&self) -> &PerPlatform<DeploySpec> {
1151 &self.deploy
1152 }
1153
1154 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1155 &mut self.deploy
1156 }
1157
1158 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1159 let lux_toml_dir = &self.internal.lux_toml_dir;
1160 let version = self
1161 .internal
1162 .version_template
1163 .try_generate(lux_toml_dir, None)?;
1164 let source =
1165 self.internal
1166 .source_template
1167 .try_generate(lux_toml_dir, &self.package, &version)?;
1168 self.render_rockspec(&version, Some(source.display_lua()))
1169 }
1170}
1171
1172fn build_spec_with_autodetected_modules(
1175 spec: &BuildSpecInternal,
1176 project_root: &Path,
1177) -> BuildSpecInternal {
1178 let builtin_capable = spec
1179 .build_type
1180 .as_ref()
1181 .is_none_or(|build_type| matches!(build_type, BuildType::Builtin | BuildType::None));
1182 if !builtin_capable {
1183 return spec.clone();
1184 }
1185 let detected = match autodetect_modules(project_root, Default::default()) {
1186 Ok(modules) => modules
1187 .into_iter()
1188 .filter_map(|(module, spec)| match spec {
1189 ModuleSpec::SourcePath(path)
1190 if path.extension().is_some_and(|ext| ext == "luau") =>
1191 {
1192 Some((module, path))
1193 }
1194 _ => None,
1195 })
1196 .collect_vec(),
1197 Err(_) => return spec.clone(),
1198 };
1199 if detected.is_empty() {
1200 spec.clone()
1201 } else {
1202 let mut new_spec = spec.clone();
1203 let modules = new_spec.builtin_spec.get_or_insert_with(Default::default);
1204 for (module, path) in detected {
1205 modules
1206 .entry(LuaTableKey::StringKey(module.to_string()))
1207 .or_insert(ModuleSpecInternal::SourcePath(path));
1208 }
1209 new_spec
1210 }
1211}
1212
1213fn validate_generated_lua(lua_str: &str) -> Result<(), ProjectTomlError> {
1214 let mut lua = ottavino::Lua::core();
1215 lua.try_enter(|ctx| {
1216 let closure = ottavino::Closure::load(ctx, None, lua_str.as_bytes())?;
1217 let executor = ottavino::Executor::start(ctx, closure.into(), ());
1218 if !executor.step(ctx, &mut ottavino::Fuel::with(ROCKSPEC_FUEL_LIMIT))? {
1219 return Ok(Err(ProjectTomlError::FuelLimitExceeded));
1220 }
1221 Ok(Ok(()))
1222 })
1223 .map_err(|err| ProjectTomlError::GeneratedInvalidLua(err.to_string()))?
1224}
1225
1226#[derive(Error, Debug, Diagnostic)]
1227#[non_exhaustive]
1228#[error(transparent)]
1229pub enum ProjectTomlIntegrityError {
1230 LuaRockspec(#[from] lua_rockspec::Error),
1231 IoError(#[from] io::Error),
1232 #[error(transparent)]
1233 #[diagnostic(transparent)]
1234 Fs(#[from] fs::Error),
1235}
1236
1237impl HasIntegrity for LocalProjectToml {
1238 async fn hash(&self) -> io::Result<Integrity> {
1239 match self.to_lua_rockspec() {
1240 Ok(lua_rockspec) => lua_rockspec.hash().await,
1241 Err(_) => self.internal.hash().await,
1242 }
1243 }
1244}
1245
1246#[derive(Debug)]
1248pub struct RemoteProjectToml {
1249 local: LocalProjectToml,
1250 source: PerPlatform<RemoteRockSource>,
1251}
1252
1253impl RemoteProjectToml {
1254 pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, lua_rockspec::Error> {
1255 RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
1256 }
1257}
1258
1259impl Rockspec for RemoteProjectToml {
1260 type Error = ProjectTomlError;
1261
1262 fn package(&self) -> &PackageName {
1263 self.local.package()
1264 }
1265
1266 fn version(&self) -> &PackageVersion {
1267 self.local.version()
1268 }
1269
1270 fn description(&self) -> &RockDescription {
1271 self.local.description()
1272 }
1273
1274 fn supported_platforms(&self) -> &PlatformSupport {
1275 self.local.supported_platforms()
1276 }
1277
1278 fn lua(&self) -> &PackageVersionReq {
1279 self.local.lua()
1280 }
1281
1282 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1283 self.local.dependencies()
1284 }
1285
1286 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1287 self.local.build_dependencies()
1288 }
1289
1290 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
1291 self.local.external_dependencies()
1292 }
1293
1294 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1295 self.local.test_dependencies()
1296 }
1297
1298 fn build(&self) -> &PerPlatform<BuildSpec> {
1299 self.local.build()
1300 }
1301
1302 fn test(&self) -> &PerPlatform<TestSpec> {
1303 self.local.test()
1304 }
1305
1306 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
1307 self.local.build_mut()
1308 }
1309
1310 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
1311 self.local.test_mut()
1312 }
1313
1314 fn format(&self) -> &Option<RockspecFormat> {
1315 self.local.format()
1316 }
1317
1318 fn source(&self) -> &PerPlatform<RemoteRockSource> {
1319 &self.source
1320 }
1321
1322 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
1323 &mut self.source
1324 }
1325
1326 fn deploy(&self) -> &PerPlatform<DeploySpec> {
1327 self.local.deploy()
1328 }
1329
1330 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1331 self.local.deploy_mut()
1332 }
1333
1334 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1335 let lux_toml_dir = &self.local.internal.lux_toml_dir;
1336 let starter = format!(
1337 r#"
1338rockspec_format = "{}"
1339package = "{}"
1340version = "{}""#,
1341 self.local
1342 .rockspec_format
1343 .as_ref()
1344 .unwrap_or(&RockspecFormat::default()),
1345 self.local.package,
1346 self.version()
1347 );
1348
1349 let mut template = Vec::new();
1350
1351 if self.local.description != RockDescription::default() {
1352 template.push(self.local.description.display_lua());
1353 }
1354
1355 if self.local.supported_platforms != PlatformSupport::default() {
1356 template.push(self.local.supported_platforms.display_lua());
1357 }
1358
1359 {
1360 let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
1361 if !self.local.lua.is_luau() {
1362 dependencies.insert(
1363 0,
1364 PackageReq {
1365 name: "lua".into(),
1366 version_req: self.local.lua.clone(),
1367 }
1368 .into(),
1369 );
1370 }
1371 template.push(Dependencies(&dependencies).display_lua());
1372 }
1373
1374 let mut build_dependencies = self
1375 .local
1376 .internal
1377 .build_dependencies
1378 .as_ref()
1379 .cloned()
1380 .unwrap_or_default();
1381
1382 let build_backend_dependency = self
1383 .local
1384 .internal
1385 .build
1386 .build_type
1387 .as_ref()
1388 .and_then(|build_type| build_type.luarocks_build_backend());
1389
1390 if let Some(build_backend_dependency) = build_backend_dependency {
1391 build_dependencies.push(build_backend_dependency);
1392 }
1393
1394 if !build_dependencies.is_empty() {
1395 template.push(BuildDependencies(&build_dependencies).display_lua());
1396 }
1397
1398 match self.local.internal.external_dependencies {
1399 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1400 template.push(ExternalDependencies(external_dependencies).display_lua());
1401 }
1402 _ => {}
1403 }
1404
1405 let source = self.local.internal.source_template.try_generate(
1406 lux_toml_dir,
1407 &self.local.internal.package,
1408 self.version(),
1409 )?;
1410 template.push(source.display_lua());
1411
1412 if let Some(ref deploy) = self.local.internal.deploy {
1413 template.push(deploy.display_lua());
1414 }
1415
1416 template.push(
1417 build_spec_with_autodetected_modules(&self.local.internal.build, lux_toml_dir)
1418 .display_lua(),
1419 );
1420
1421 let unformatted_code = std::iter::once(starter)
1422 .chain(template.into_iter().map(|kv| kv.to_string()))
1423 .join("\n\n");
1424 let result = match stylua_lib::format_code(
1425 &unformatted_code,
1426 stylua_lib::Config::default(),
1427 None,
1428 stylua_lib::OutputVerification::Full,
1429 ) {
1430 Ok(formatted_code) => formatted_code,
1431 Err(_) => unformatted_code,
1432 };
1433 validate_generated_lua(&result)?;
1434 Ok(result)
1435 }
1436}
1437
1438impl HasIntegrity for RemoteProjectToml {
1439 async fn hash(&self) -> io::Result<Integrity> {
1440 self.to_lua_rockspec()
1441 .map_err(|err| {
1442 io::Error::other(format!(
1443 "unable to convert remote project to Lua rockspec:\n{}",
1444 err
1445 ))
1446 })?
1447 .hash()
1448 .await
1449 }
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454 use std::path::PathBuf;
1455
1456 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1457 use git2::Repository;
1458 use url::Url;
1459
1460 use crate::{
1461 git::{url::RemoteGitUrl, GitRef, GitSource},
1462 lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1463 project::{Project, ProjectRoot, PROJECT_TOML},
1464 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1465 };
1466
1467 use super::PartialProjectToml;
1468
1469 #[test]
1470 fn project_toml_parsing() {
1471 let project_toml = r#"
1472 package = "my-package"
1473 version = "1.0.0"
1474 lua = "5.3"
1475
1476 rockspec_format = "1.0"
1477
1478 [source]
1479 url = "https://example.com"
1480
1481 [dependencies]
1482 foo = "1.0"
1483 bar = ">=2.0"
1484
1485 [run]
1486 args = ["--foo", "--bar"]
1487
1488 [build]
1489 type = "builtin"
1490 "#;
1491
1492 let project =
1493 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1494 let _ = project.into_remote(None).unwrap();
1495
1496 let project_toml = r#"
1497 package = "my-package"
1498 version = "1.0.0"
1499 lua = "5.1"
1500
1501 [description]
1502 summary = "A summary"
1503 detailed = "A detailed description"
1504 license = "MIT"
1505 homepage = "https://example.com"
1506 issues_url = "https://example.com/issues"
1507 maintainer = "John Doe"
1508 labels = ["label1", "label2"]
1509
1510 [supported_platforms]
1511 linux = true
1512 windows = false
1513
1514 [dependencies]
1515 foo = "1.0"
1516 bar = ">=2.0"
1517
1518 [build_dependencies]
1519 baz = "1.0"
1520
1521 [external_dependencies.foo]
1522 header = "foo.h"
1523
1524 [external_dependencies.bar]
1525 library = "libbar.so"
1526
1527 [test_dependencies]
1528 busted = "69.420"
1529
1530 [source]
1531 url = "https://example.com"
1532 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1533 file = "my-package-1.0.0.tar.gz"
1534 dir = "my-package-1.0.0"
1535
1536 [test]
1537 type = "command"
1538 script = "test.lua"
1539 flags = [ "foo", "bar" ]
1540
1541 [run]
1542 command = "my-command"
1543 args = ["--foo", "--bar"]
1544
1545 [build]
1546 type = "builtin"
1547 "#;
1548
1549 let project =
1550 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1551 let _ = project.into_remote(None).unwrap();
1552 }
1553
1554 #[test]
1555 fn compare_project_toml_with_rockspec() {
1556 let project_toml = r#"
1557 package = "my-package"
1558 version = "1.0.0"
1559 lua = "5.1"
1560
1561 # For testing, specify a custom rockspec format
1562 # (defaults to 3.0)
1563 rockspec_format = "1.0"
1564
1565 [description]
1566 summary = "A summary"
1567 detailed = "A detailed description"
1568 license = "MIT"
1569 homepage = "https://example.com"
1570 issues_url = "https://example.com/issues"
1571 maintainer = "John Doe"
1572 labels = ["label1", "label2"]
1573
1574 [supported_platforms]
1575 linux = true
1576 windows = false
1577
1578 [dependencies]
1579 foo = "1.0"
1580 bar = ">=2.0"
1581
1582 [build_dependencies]
1583 baz = "1.0"
1584
1585 [external_dependencies.foo]
1586 header = "foo.h"
1587
1588 [external_dependencies.bar]
1589 library = "libbar.so"
1590
1591 [test_dependencies]
1592 busted = "1.0"
1593
1594 [source]
1595 url = "https://example.com"
1596 file = "my-package-1.0.0.tar.gz"
1597 dir = "my-package-1.0.0"
1598
1599 [test]
1600 type = "command"
1601 script = "test.lua"
1602 flags = [ "foo", "bar" ]
1603
1604 [run]
1605 command = "my-command"
1606 args = ["--foo", "--bar"]
1607
1608 [deploy]
1609 wrap_bin_scripts = false
1610
1611 [build]
1612 type = "builtin"
1613
1614 [build.install.lua]
1615 "foo.bar" = "src/bar.lua"
1616
1617 [build.install.lib]
1618 "foo.baz" = "src/baz.c"
1619
1620 [build.install.bin]
1621 "bla" = "src/bla"
1622
1623 [build.install.conf]
1624 "cfg.conf" = "resources/config.conf"
1625 "#;
1626
1627 let expected_rockspec = r#"
1628 rockspec_format = "1.0"
1629 package = "my-package"
1630 version = "1.0.0"
1631
1632 source = {
1633 url = "https://example.com",
1634 file = "my-package-1.0.0.tar.gz",
1635 dir = "my-package-1.0.0",
1636 }
1637
1638 description = {
1639 summary = "A summary",
1640 detailed = "A detailed description",
1641 license = "MIT",
1642 homepage = "https://example.com",
1643 issues_url = "https://example.com/issues",
1644 maintainer = "John Doe",
1645 labels = {"label1", "label2"},
1646 }
1647
1648 supported_platforms = {"linux", "!windows"}
1649
1650 dependencies = {
1651 "lua ==5.1",
1652 "foo ==1.0",
1653 "bar >=2.0",
1654 }
1655
1656 build_dependencies = {
1657 "baz ==1.0",
1658 }
1659
1660 external_dependencies = {
1661 foo = { header = "foo.h" },
1662 bar = { library = "libbar.so" },
1663 }
1664
1665 source = {
1666 url = "https://example.com",
1667 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1668 file = "my-package-1.0.0.tar.gz",
1669 dir = "my-package-1.0.0",
1670 }
1671
1672 test = {
1673 type = "command",
1674 script = "test.lua",
1675 flags = {"foo", "bar"},
1676 }
1677
1678 deploy = {
1679 wrap_bin_scripts = false,
1680 }
1681
1682 build = {
1683 type = "builtin",
1684 install = {
1685 lua = {
1686 ["foo.bar"] = "src/bar.lua",
1687 },
1688 lib = {
1689 ["foo.baz"] = "src/baz.c",
1690 },
1691 bin = {
1692 bla = "src/bla",
1693 },
1694 conf = {
1695 ["cfg.conf"] = "resources/config.conf",
1696 },
1697 },
1698 }
1699 "#;
1700
1701 let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1702
1703 let project_toml =
1704 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1705 let rockspec = project_toml
1706 .into_remote(None)
1707 .unwrap()
1708 .to_lua_rockspec()
1709 .unwrap();
1710
1711 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1712 let mut v = v.current_platform().clone();
1713 v.sort_by(|a, b| a.name().cmp(b.name()));
1714 v
1715 };
1716
1717 assert_eq!(rockspec.package(), expected_rockspec.package());
1718 assert_eq!(rockspec.version(), expected_rockspec.version());
1719 assert_eq!(rockspec.description(), expected_rockspec.description());
1720 assert_eq!(
1721 rockspec.supported_platforms(),
1722 expected_rockspec.supported_platforms()
1723 );
1724 assert_eq!(
1725 sorted_package_reqs(rockspec.dependencies()),
1726 sorted_package_reqs(expected_rockspec.dependencies())
1727 );
1728 assert_eq!(
1729 sorted_package_reqs(rockspec.build_dependencies()),
1730 sorted_package_reqs(expected_rockspec.build_dependencies())
1731 );
1732 assert_eq!(
1733 rockspec.external_dependencies(),
1734 expected_rockspec.external_dependencies()
1735 );
1736 assert_eq!(rockspec.source(), expected_rockspec.source());
1737 assert_eq!(rockspec.build(), expected_rockspec.build());
1738 assert_eq!(rockspec.format(), expected_rockspec.format());
1739 }
1740
1741 #[test]
1742 fn merge_project_toml_with_partial_rockspec() {
1743 let project_toml = r#"
1744 package = "my-package"
1745 version = "1.0.0"
1746 lua = "5.1"
1747
1748 # For testing, specify a custom rockspec format
1749 # (defaults to 3.0)
1750 rockspec_format = "1.0"
1751
1752 [description]
1753 summary = "A summary"
1754 detailed = "A detailed description"
1755 license = "MIT"
1756 homepage = "https://example.com"
1757 issues_url = "https://example.com/issues"
1758 maintainer = "John Doe"
1759 labels = ["label1", "label2"]
1760
1761 [supported_platforms]
1762 linux = true
1763 windows = false
1764
1765 [dependencies]
1766 foo = "1.0"
1767 bar = ">=2.0"
1768
1769 [build_dependencies]
1770 baz = "1.0"
1771
1772 [external_dependencies.foo]
1773 header = "foo.h"
1774
1775 [external_dependencies.bar]
1776 library = "libbar.so"
1777
1778 [test_dependencies]
1779 busted = "1.0"
1780
1781 [source]
1782 url = "https://example.com"
1783 file = "my-package-1.0.0.tar.gz"
1784 dir = "my-package-1.0.0"
1785
1786 [test]
1787 type = "command"
1788 script = "test.lua"
1789 flags = [ "foo", "bar" ]
1790
1791 [run]
1792 command = "my-command"
1793 args = [ "--foo", "--bar" ]
1794
1795 [build]
1796 type = "builtin"
1797 "#;
1798
1799 let mergable_rockspec_content = r#"
1800 rockspec_format = "1.0"
1801 package = "my-package-overwritten"
1802
1803 description = {
1804 summary = "A summary overwritten",
1805 detailed = "A detailed description overwritten",
1806 license = "GPL-2.0",
1807 homepage = "https://example.com/overwritten",
1808 issues_url = "https://example.com/issues/overwritten",
1809 maintainer = "John Doe Overwritten",
1810 labels = {"over", "written"},
1811 }
1812
1813 -- Inverted supported platforms
1814 supported_platforms = {"!linux", "windows"}
1815
1816 dependencies = {
1817 "lua 5.1",
1818 "foo >1.0",
1819 "bar <=2.0",
1820 }
1821
1822 build_dependencies = {
1823 "baz >1.0",
1824 }
1825
1826 external_dependencies = {
1827 foo = { header = "overwritten.h" },
1828 bar = { library = "overwritten.so" },
1829 }
1830
1831 test = {
1832 type = "command",
1833 script = "overwritten.lua",
1834 flags = {"over", "written"},
1835 }
1836
1837 build = {
1838 type = "builtin",
1839 }
1840 "#;
1841
1842 let remote_rockspec_content = format!(
1843 r#"{}
1844 version = "1.0.0"
1845 source = {{
1846 url = "https://example.com",
1847 file = "my-package-1.0.0.tar.gz",
1848 dir = "my-package-1.0.0",
1849 }}
1850 "#,
1851 mergable_rockspec_content
1852 );
1853
1854 let project_toml =
1855 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1856 let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1857 let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1858
1859 let merged = project_toml
1860 .merge(partial_rockspec)
1861 .into_remote(None)
1862 .unwrap();
1863
1864 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1865 let mut v = v.current_platform().clone();
1866 v.sort_by(|a, b| a.name().cmp(b.name()));
1867 v
1868 };
1869
1870 assert_eq!(merged.package(), expected_rockspec.package());
1871 assert_eq!(merged.version(), expected_rockspec.version());
1872 assert_eq!(merged.description(), expected_rockspec.description());
1873 assert_eq!(
1874 merged.supported_platforms(),
1875 expected_rockspec.supported_platforms()
1876 );
1877 assert_eq!(
1878 sorted_package_reqs(merged.dependencies()),
1879 sorted_package_reqs(expected_rockspec.dependencies())
1880 );
1881 assert_eq!(
1882 sorted_package_reqs(merged.build_dependencies()),
1883 sorted_package_reqs(expected_rockspec.build_dependencies())
1884 );
1885 assert_eq!(
1886 merged.external_dependencies(),
1887 expected_rockspec.external_dependencies()
1888 );
1889 assert_eq!(merged.source(), expected_rockspec.source());
1890 assert_eq!(merged.build(), expected_rockspec.build());
1891 assert_eq!(merged.format(), expected_rockspec.format());
1892 assert!(merged.local.run().is_some());
1894 }
1895
1896 #[test]
1897 fn wally_dependency_parsing() {
1898 let project_toml = r#"
1899 package = "my-package"
1900 version = "1.0.0"
1901 lua = "5.1"
1902
1903 [dependencies.roact]
1904 wally = "jsdotperf/roact@^1.4.0"
1905
1906 [build]
1907 type = "builtin"
1908 "#;
1909
1910 let project =
1911 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1912 let deps = project.dependencies.unwrap();
1913 assert_eq!(deps.len(), 1);
1914 assert_eq!(deps[0].name().to_string(), "roact");
1915 assert!(matches!(
1916 deps[0].source(),
1917 Some(RockSourceSpec::Wally(req)) if req.name().to_string() == "jsdotperf/roact"
1918 ));
1919 }
1920
1921 #[test]
1922 fn wally_dependency_conflicts_with_version() {
1923 let project_toml = r#"
1924 package = "my-package"
1925 version = "1.0.0"
1926 lua = "5.1"
1927
1928 [dependencies.roact]
1929 wally = "jsdotperf/roact@^1.4.0"
1930 version = "1.4.0"
1931
1932 [build]
1933 type = "builtin"
1934 "#;
1935
1936 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap_err();
1937 }
1938
1939 #[test]
1940 fn project_toml_with_lua_in_dependencies() {
1941 let project_toml = r#"
1942 package = "my-package"
1943 version = "1.0.0"
1944 # lua = ">5.1"
1945
1946 [dependencies]
1947 lua = "5.1" # disallowed
1948
1949 [build]
1950 type = "builtin"
1951 "#;
1952
1953 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default())
1954 .unwrap()
1955 .into_local()
1956 .unwrap_err();
1957 }
1958
1959 #[test]
1960 fn project_toml_with_invalid_run_command() {
1961 for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1962 let project_toml = format!(
1963 r#"
1964 package = "my-package"
1965 version = "1.0.0"
1966 lua = "5.1"
1967
1968 [build]
1969 type = "builtin"
1970
1971 [run]
1972 command = "{command}"
1973 "#,
1974 );
1975
1976 PartialProjectToml::new(PROJECT_TOML, &project_toml, ProjectRoot::default())
1977 .unwrap_err();
1978 }
1979 }
1980
1981 #[test]
1982 fn project_root_dir_override_and_rockspec_generation() {
1983 let project_toml = r#"
1984 package = "my-package"
1985 version = "1.0.0"
1986 lua = "5.1"
1987
1988 [project]
1989 root_dir = ".."
1990
1991 [source]
1992 url = "https://example.com"
1993
1994 [build]
1995 type = "builtin"
1996 "#;
1997
1998 let workspace_root = assert_fs::TempDir::new().unwrap();
1999 let lux_toml_dir = workspace_root.child("proj");
2000 lux_toml_dir.create_dir_all().unwrap();
2001 std::fs::write(lux_toml_dir.join(PROJECT_TOML), project_toml).unwrap();
2002
2003 let project = PartialProjectToml::new(
2004 PROJECT_TOML,
2005 project_toml,
2006 ProjectRoot(lux_toml_dir.to_path_buf()),
2007 )
2008 .unwrap();
2009 let local = project.into_local().unwrap();
2010 let RockSourceSpec::File(root_dir) = &local.source().current_platform().source_spec else {
2011 panic!("expected a file source");
2012 };
2013 assert_eq!(
2014 root_dir,
2015 &std::fs::canonicalize(workspace_root.path()).unwrap()
2016 );
2017
2018 let rockspec = local.to_lua_rockspec().unwrap();
2020 assert!(!rockspec
2021 .to_lua_remote_rockspec_string()
2022 .unwrap()
2023 .contains("root_dir"));
2024 }
2025
2026 #[test]
2027 fn generate_non_deterministic_git_source() {
2028 let rockspec_content = r#"
2029 package = "test-package"
2030 version = "1.0.0"
2031 lua = ">=5.1"
2032
2033 [source]
2034 url = "git+https://exaple.com/repo.git"
2035
2036 [build]
2037 type = "builtin"
2038 "#;
2039
2040 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
2041 .unwrap()
2042 .into_remote(None)
2043 .unwrap_err();
2044 }
2045
2046 #[test]
2047 fn generate_deterministic_git_source() {
2048 let rockspec_content = r#"
2049 package = "test-package"
2050 version = "1.0.0"
2051 lua = ">=5.1"
2052
2053 [source]
2054 url = "git+https://exaple.com/owner/repo.git"
2055 tag = "v0.1.0"
2056
2057 [build]
2058 type = "builtin"
2059 "#;
2060
2061 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
2062 .unwrap()
2063 .into_remote(None)
2064 .unwrap();
2065 }
2066
2067 #[test]
2068 fn generate_git_source_with_branch() {
2069 let rockspec_content = r#"
2070 package = "test-package"
2071 version = "1.0.0"
2072 lua = ">=5.1"
2073
2074 [source]
2075 url = "git+https://example.com/owner/repo.git"
2076 branch = "main"
2077
2078 [build]
2079 type = "builtin"
2080 "#;
2081
2082 let toml = PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
2083 .unwrap()
2084 .into_remote(None)
2085 .unwrap();
2086
2087 let lua = toml.to_lua_remote_rockspec_string().unwrap();
2088 assert!(lua.contains("branch = \"main\""));
2089 assert!(!lua.contains("tag = "));
2090 }
2091
2092 fn init_git_repo(path: &std::path::Path) -> Repository {
2093 let repo = Repository::init(path).unwrap();
2094 {
2095 let tree_id = repo.index().unwrap().write_tree().unwrap();
2096 let tree = repo.find_tree(tree_id).unwrap();
2097 let sig = git2::Signature::now("name", "email").unwrap();
2098 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
2099 .unwrap();
2100 }
2101 repo
2102 }
2103
2104 fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
2105 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2106 .join("resources/test/sample-projects/source-template/");
2107 temp_dir.copy_from(&sample_project, &["**"]).unwrap();
2108 init_git_repo(temp_dir.path())
2109 }
2110
2111 fn create_tag(repo: &Repository, name: &str) {
2112 let sig = git2::Signature::now("name", "email").unwrap();
2113 let id = repo.head().unwrap().target().unwrap();
2114 let obj = repo.find_object(id, None).unwrap();
2115 repo.tag(name, &obj, &sig, "msg", true).unwrap();
2116 }
2117
2118 #[test]
2119 fn test_git_project_generate_dev_source() {
2120 let project_root = assert_fs::TempDir::new().unwrap();
2121 init_sample_project_repo(&project_root);
2122 let project = Project::from_exact(&project_root).unwrap().unwrap();
2123 let remote_project_toml = project.toml().into_remote(None).unwrap();
2124 let source = remote_project_toml.source.current_platform();
2125 let source_spec = &source.source_spec;
2126 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
2127 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
2128 let expected_url: RemoteGitUrl =
2129 "https://github.com/lumen-oss/lux.git".parse().unwrap();
2130 assert_eq!(url, &expected_url);
2131 assert!(git_ref.is_some());
2132 }
2133 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
2134 }
2135
2136 #[test]
2137 fn test_git_project_generate_non_semver_tag_source() {
2138 let project_root = assert_fs::TempDir::new().unwrap();
2139 let repo = init_sample_project_repo(&project_root);
2140 let tag_name = "bla";
2141 create_tag(&repo, tag_name);
2142 let project = Project::from_exact(&project_root).unwrap().unwrap();
2143 let remote_project_toml = project.toml().into_remote(None).unwrap();
2144 let source = remote_project_toml.source.current_platform();
2145 let source_spec = &source.source_spec;
2146 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
2147 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
2148 let expected_url: RemoteGitUrl =
2149 "https://github.com/lumen-oss/lux.git".parse().unwrap();
2150 assert_eq!(url, &expected_url);
2151 assert_eq!(git_ref, &Some(GitRef::Tag(tag_name.to_string())));
2152 }
2153 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
2154 }
2155
2156 #[test]
2157 fn test_git_project_generate_release_source_tag_with_v_prefix() {
2158 let project_root = assert_fs::TempDir::new().unwrap();
2159 let repo = init_sample_project_repo(&project_root);
2160 let tag_name = "v1.0.0";
2161 create_tag(&repo, "bla");
2162 create_tag(&repo, tag_name);
2163 let project = Project::from_exact(&project_root).unwrap().unwrap();
2164 let remote_project_toml = project.toml().into_remote(None).unwrap();
2165 let source = remote_project_toml.source.current_platform();
2166 let source_spec = &source.source_spec;
2167 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2168 if let RockSourceSpec::Url(url) = source_spec {
2169 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
2170 .parse()
2171 .unwrap();
2172 assert_eq!(url, &expected_url);
2173 }
2174 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2175 }
2176
2177 #[test]
2178 fn test_git_project_generate_release_source_tag_without_v_prefix() {
2179 let project_root = assert_fs::TempDir::new().unwrap();
2180 let repo = init_sample_project_repo(&project_root);
2181 create_tag(&repo, "bla");
2182 let tag_name = "1.0.0";
2183 create_tag(&repo, tag_name);
2184 let project = Project::from_exact(&project_root).unwrap().unwrap();
2185 let remote_project_toml = project.toml().into_remote(None).unwrap();
2186 let source = remote_project_toml.source.current_platform();
2187 let source_spec = &source.source_spec;
2188 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2189 if let RockSourceSpec::Url(url) = source_spec {
2190 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
2191 .parse()
2192 .unwrap();
2193 assert_eq!(url, &expected_url);
2194 }
2195 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2196 }
2197
2198 #[test]
2199 fn test_git_project_in_subdirectory() {
2200 let temp_dir = assert_fs::TempDir::new().unwrap();
2201 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2202 .join("resources/test/sample-projects/source-template/");
2203 let project_dir = temp_dir.child("lux");
2204 project_dir.create_dir_all().unwrap();
2205 project_dir.copy_from(&sample_project, &["**"]).unwrap();
2206 let repo = init_git_repo(temp_dir.path());
2207 create_tag(&repo, "bla");
2208 let tag_name = "1.0.0";
2209 create_tag(&repo, tag_name);
2210 let project = Project::from_exact(&project_dir).unwrap().unwrap();
2211 let remote_project_toml = project.toml().into_remote(None).unwrap();
2212 let source = remote_project_toml.source.current_platform();
2213 let source_spec = &source.source_spec;
2214 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2215 if let RockSourceSpec::Url(url) = source_spec {
2216 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
2217 .parse()
2218 .unwrap();
2219 assert_eq!(url, &expected_url);
2220 }
2221 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2222 }
2223
2224 #[test]
2225 fn test_git_project_in_subdirectory_generate_dev_source() {
2226 let temp_dir = assert_fs::TempDir::new().unwrap();
2227 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2228 .join("resources/test/sample-projects/source-template/");
2229 let project_dir = temp_dir.child("lux");
2230 project_dir.create_dir_all().unwrap();
2231 project_dir.copy_from(&sample_project, &["**"]).unwrap();
2232 init_git_repo(temp_dir.path());
2233
2234 let project = Project::from_exact(&project_dir).unwrap().unwrap();
2235 let remote_project_toml = project.toml().into_remote(None).unwrap();
2236 let RockSourceSpec::Git(GitSource { git_ref, .. }) =
2237 &remote_project_toml.source.current_platform().source_spec
2238 else {
2239 panic!("expected a git source");
2240 };
2241 assert!(git_ref.is_some());
2242 }
2243}