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