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