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