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 pub(crate) package: PackageName,
275 #[serde(default, rename = "version")]
278 #[cfg_attr(feature = "schema", schemars(with = "String"))]
279 pub(crate) version_template: PackageVersionTemplate,
280 #[serde(default)]
282 pub(crate) build: BuildSpecInternal,
283 pub(crate) rockspec_format: Option<RockspecFormat>,
286 #[serde(default)]
288 pub(crate) run: Option<RunSpec>,
289 #[serde(default)]
291 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
292 pub(crate) lua: Option<PackageVersionReq>,
293 #[serde(default)]
295 pub(crate) description: Option<RockDescription>,
296 #[serde(default)]
299 #[cfg_attr(
300 feature = "schema",
301 schemars(with = "Option<std::collections::BTreeMap<String, bool>>")
302 )]
303 pub(crate) supported_platforms: Option<HashMap<PlatformIdentifier, bool>>,
304 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
308 #[cfg_attr(
309 feature = "schema",
310 schemars(
311 with = "Option<std::collections::BTreeMap<String, crate::schema::DependencyEntry>>"
312 )
313 )]
314 pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
315 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
317 #[cfg_attr(
318 feature = "schema",
319 schemars(
320 with = "Option<std::collections::BTreeMap<String, crate::schema::DependencyEntry>>"
321 )
322 )]
323 pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
324 #[serde(default)]
327 pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
328 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
330 #[cfg_attr(
331 feature = "schema",
332 schemars(
333 with = "Option<std::collections::BTreeMap<String, crate::schema::DependencyEntry>>"
334 )
335 )]
336 pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
337 #[serde(default, rename = "source")]
339 pub(crate) source_template: RockSourceTemplate,
340 #[serde(default)]
342 pub(crate) test: Option<TestSpecInternal>,
343 #[serde(default)]
345 pub(crate) deploy: Option<DeploySpec>,
346 #[serde(default)]
348 pub(crate) project: ProjectSpec,
349
350 #[serde(skip, default = "PathBuf::new")]
352 lux_toml_dir: PathBuf,
353}
354
355impl HasIntegrity for PartialProjectToml {
356 async fn hash(&self) -> io::Result<Integrity> {
357 let toml_file = self.lux_toml_dir.join(PROJECT_TOML);
358 let content = fs::sync::read_to_string(&toml_file).map_err(io::Error::other)?;
359 Ok(Integrity::from(&content))
360 }
361}
362
363impl PartialProjectToml {
364 pub(crate) fn new(
365 name: &str,
366 str: &str,
367 lux_toml_dir: ProjectRoot,
368 ) -> Result<Self, TomlDeError> {
369 let mut this: Self = super::parse_toml(name, str)?;
370 this.lux_toml_dir = lux_toml_dir.to_path_buf();
371 this.project.root = match this.project.root_dir {
372 Some(ref root_dir) => {
373 let relative_project_root = lux_toml_dir.join(root_dir);
374 let project_root =
375 std::fs::canonicalize(&relative_project_root).unwrap_or(relative_project_root);
376 ProjectRoot(project_root)
377 }
378 None => lux_toml_dir,
379 };
380 Ok(this)
381 }
382
383 pub fn lua(&self) -> Option<&PackageVersionReq> {
385 self.lua.as_ref()
386 }
387
388 pub fn into_local(&self) -> Result<LocalProjectToml, LocalProjectTomlValidationError> {
391 let project_toml = self.clone();
392
393 if project_toml
395 .dependencies
396 .as_ref()
397 .is_some_and(|deps| deps.iter().any(|dep| dep.name() == &"lua".into()))
398 {
399 return Err(LocalProjectTomlValidationError::DependenciesContainLua);
400 }
401
402 let get_duplicates = |dependencies: &Option<Vec<LuaDependencySpec>>| {
403 dependencies
404 .iter()
405 .flat_map(|deps| {
406 deps.iter()
407 .map(|dep| dep.package_req().name())
408 .duplicates()
409 .cloned()
410 })
411 .collect_vec()
412 };
413 let duplicate_dependencies = get_duplicates(&self.dependencies);
414 if !duplicate_dependencies.is_empty() {
415 return Err(LocalProjectTomlValidationError::DuplicateDependencies(
416 PackageNameList::new(duplicate_dependencies),
417 ));
418 }
419 let duplicate_test_dependencies = get_duplicates(&self.test_dependencies);
420 if !duplicate_test_dependencies.is_empty() {
421 return Err(LocalProjectTomlValidationError::DuplicateTestDependencies(
422 PackageNameList::new(duplicate_test_dependencies),
423 ));
424 }
425 let duplicate_build_dependencies = get_duplicates(&self.build_dependencies);
426 if !duplicate_build_dependencies.is_empty() {
427 return Err(LocalProjectTomlValidationError::DuplicateBuildDependencies(
428 PackageNameList::new(duplicate_build_dependencies),
429 ));
430 }
431
432 let validated = LocalProjectToml {
433 internal: project_toml.clone(),
434
435 package: project_toml.package,
436 version: project_toml
437 .version_template
438 .try_generate(&self.lux_toml_dir, None)
439 .unwrap_or(PackageVersion::default_dev_version()),
440 lua: project_toml
441 .lua
442 .ok_or(LocalProjectTomlValidationError::NoLuaVersion)?,
443 description: project_toml.description.unwrap_or_default(),
444 run: project_toml.run.map(PerPlatform::new),
445 supported_platforms: PlatformSupport::parse(
446 &project_toml
447 .supported_platforms
448 .unwrap_or_default()
449 .into_iter()
450 .map(|(platform, supported)| {
451 if supported {
452 format!("{platform}")
453 } else {
454 format!("!{platform}")
455 }
456 })
457 .collect_vec(),
458 )?,
459 dependencies: PerPlatform::new(
462 project_toml
463 .dependencies
464 .unwrap_or_default()
465 .into_iter()
466 .map(|dep| self.resolve_lua_dependency_spec(dep))
467 .collect_vec(),
468 ),
469 test_dependencies: PerPlatform::new(
470 project_toml
471 .test_dependencies
472 .unwrap_or_default()
473 .into_iter()
474 .map(|dep| self.resolve_lua_dependency_spec(dep))
475 .collect_vec(),
476 ),
477 build_dependencies: PerPlatform::new(
478 project_toml
479 .build_dependencies
480 .unwrap_or_default()
481 .into_iter()
482 .map(|dep| self.resolve_lua_dependency_spec(dep))
483 .collect_vec(),
484 ),
485 external_dependencies: PerPlatform::new(
486 project_toml.external_dependencies.unwrap_or_default(),
487 ),
488 test: PerPlatform::new(TestSpec::try_from(
489 project_toml.test.clone().unwrap_or_default(),
490 )?),
491 build: PerPlatform::new(BuildSpec::from_internal_spec(project_toml.build.clone())?),
492 deploy: PerPlatform::new(project_toml.deploy.clone().unwrap_or_default()),
493 rockspec_format: project_toml.rockspec_format.clone(),
494
495 source: PerPlatform::new(RemoteRockSource {
496 local: LocalRockSource::default(),
497 source_spec: RockSourceSpec::File(self.project.root.to_path_buf()),
498 }),
499 };
500
501 let rockspec_file_name = format!("{}-{}.rockspec", validated.package, validated.version);
502
503 if validated
504 .build
505 .default
506 .copy_directories
507 .contains(&PathBuf::from(&rockspec_file_name))
508 {
509 return Err(LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(None));
510 }
511
512 for (platform, build_override) in &validated.build.per_platform {
513 if build_override
514 .copy_directories
515 .contains(&PathBuf::from(&rockspec_file_name))
516 {
517 return Err(
518 LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(Some(
519 platform.to_string(),
520 )),
521 );
522 }
523 }
524
525 Ok(validated)
526 }
527
528 pub fn into_remote(
532 &self,
533 specrev: Option<SpecRev>,
534 ) -> Result<RemoteProjectToml, RemoteProjectTomlValidationError> {
535 let version = self
536 .version_template
537 .try_generate(&self.lux_toml_dir, specrev)?;
538 let source =
539 self.source_template
540 .try_generate(&self.lux_toml_dir, &self.package, &version)?;
541 let source = PerPlatform::new(RemoteRockSource::try_from(source).map_err(|err| {
542 RemoteProjectTomlValidationError::LocalProjectTomlValidationError(
543 LocalProjectTomlValidationError::RockSource(err),
544 )
545 })?);
546 let mut local = self.into_local()?;
547 local.version = version;
548
549 let validated = RemoteProjectToml { source, local };
550
551 Ok(validated)
552 }
553
554 pub fn package(&self) -> &PackageName {
557 &self.package
558 }
559
560 pub fn version(&self) -> Result<PackageVersion, GenerateVersionError> {
562 self.version_template.try_generate(&self.lux_toml_dir, None)
563 }
564
565 pub fn merge(self, other: PartialLuaRockspec) -> Self {
568 PartialProjectToml {
569 package: other.package.unwrap_or(self.package),
570 version_template: self.version_template,
571 lua: other
572 .dependencies
573 .as_ref()
574 .and_then(|deps| {
575 deps.iter()
576 .find(|dep| dep.name() == &"lua".into())
577 .and_then(|dep| {
578 if dep.version_req().is_any() {
579 None
580 } else {
581 Some(dep.version_req().clone())
582 }
583 })
584 })
585 .or(self.lua),
586 build: other.build.unwrap_or(self.build),
587 run: self.run,
588 description: other.description.or(self.description),
589 supported_platforms: other
590 .supported_platforms
591 .map(|platform_support| platform_support.platforms().clone())
592 .or(self.supported_platforms),
593 dependencies: other
594 .dependencies
595 .map(|deps| {
596 deps.into_iter()
597 .filter(|dep| dep.name() != &"lua".into())
598 .collect()
599 })
600 .or(self.dependencies),
601 build_dependencies: other.build_dependencies.or(self.build_dependencies),
602 test_dependencies: other.test_dependencies.or(self.test_dependencies),
603 external_dependencies: other.external_dependencies.or(self.external_dependencies),
604 source_template: self.source_template,
605 test: other.test.or(self.test),
606 deploy: other.deploy.or(self.deploy),
607 rockspec_format: other.rockspec_format.or(self.rockspec_format),
608
609 project: self.project,
611 lux_toml_dir: self.lux_toml_dir.clone(),
612 }
613 }
614
615 fn resolve_lua_dependency_spec(&self, dep: LuaDependencySpec) -> LuaDependencySpec {
616 match &dep.source {
617 Some(RockSourceSpec::File(path)) if path.is_dir() => dep,
618 Some(RockSourceSpec::File(path)) => LuaDependencySpec {
619 source: Some(RockSourceSpec::File(self.lux_toml_dir.join(path))),
620 ..dep
621 },
622 _ => dep,
623 }
624 }
625}
626
627impl LuaVersionCompatibility for PartialProjectToml {
631 fn validate_lua_version(&self, version: &LuaVersion) -> Result<(), LuaVersionError> {
632 if self.supports_lua_version(version) {
633 Ok(())
634 } else {
635 Err(LuaVersionError::LuaVersionUnsupported(
636 version.clone(),
637 self.package().to_owned(),
638 self.version_template
639 .try_generate(&self.lux_toml_dir, None)
640 .unwrap_or(PackageVersion::default_dev_version()),
641 ))
642 }
643 }
644
645 fn validate_lua_version_from_config(&self, config: &Config) -> Result<(), LuaVersionError> {
646 let _ = self.lua_version_matches(config)?;
647 Ok(())
648 }
649
650 fn lua_version_matches(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
651 let version = LuaVersion::from(config)?.clone();
652 if self.supports_lua_version(&version) {
653 Ok(version)
654 } else {
655 Err(LuaVersionError::LuaVersionUnsupported(
656 version,
657 self.package.clone(),
658 self.version_template
659 .try_generate(&self.lux_toml_dir, None)
660 .unwrap_or(PackageVersion::default_dev_version()),
661 ))
662 }
663 }
664
665 fn supports_lua_version(&self, lua_version: &LuaVersion) -> bool {
666 self.lua.as_ref().is_none_or(|lua| {
667 if lua.is_luau() {
668 return matches!(lua_version, LuaVersion::Luau);
669 }
670 lua.matches(&lua_version.as_version())
671 })
672 }
673
674 fn lua_version(&self) -> Option<LuaVersion> {
675 if self.lua.as_ref().is_some_and(|lua| lua.is_luau()) {
676 return Some(LuaVersion::Luau);
677 }
678 for (possibility, version) in [
679 ("5.5.0", LuaVersion::Lua55),
680 ("5.4.0", LuaVersion::Lua54),
681 ("5.3.0", LuaVersion::Lua53),
682 ("5.2.0", LuaVersion::Lua52),
683 ("5.1.0", LuaVersion::Lua51),
684 ] {
685 let possibility = unsafe { possibility.parse().unwrap_unchecked() };
686 if self
687 .lua
688 .as_ref()
689 .is_none_or(|lua| lua.matches(&possibility))
690 {
691 return Some(version);
692 }
693 }
694 None
695 }
696}
697
698#[derive(Debug, Clone, Deserialize)]
702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
703pub(crate) struct ProjectSpec {
704 root_dir: Option<PathBuf>,
708 #[serde(skip, default = "ProjectRoot::new")]
710 pub(crate) root: ProjectRoot,
711}
712
713impl Default for ProjectSpec {
714 fn default() -> Self {
715 Self {
716 root_dir: None,
717 root: ProjectRoot::new(),
718 }
719 }
720}
721
722#[derive(Debug, Clone, Deserialize)]
724#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
725pub struct RunSpec {
726 pub(crate) command: Option<RunCommand>,
728 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<String>>"))]
730 pub(crate) args: Option<NonEmpty<String>>,
731}
732
733#[derive(Debug)]
737pub struct LocalProjectToml {
738 package: PackageName,
739 version: PackageVersion,
740 lua: PackageVersionReq,
741 rockspec_format: Option<RockspecFormat>,
742 run: Option<PerPlatform<RunSpec>>,
743 description: RockDescription,
744 supported_platforms: PlatformSupport,
745 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
746 build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
747 external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
748 test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
749 test: PerPlatform<TestSpec>,
750 build: PerPlatform<BuildSpec>,
751 deploy: PerPlatform<DeploySpec>,
752
753 internal: PartialProjectToml,
755
756 source: PerPlatform<RemoteRockSource>,
758}
759
760impl LocalProjectToml {
761 pub fn run(&self) -> Option<&PerPlatform<RunSpec>> {
762 self.run.as_ref()
763 }
764
765 pub fn to_lua_rockspec(&self) -> Result<LocalLuaRockspec, LuaRockspecError> {
768 if let Some(dep) = self
769 .dependencies()
770 .per_platform
771 .values()
772 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
773 .collect_vec()
774 .first()
775 {
776 return Err(LuaRockspecError::OffSpecDependency(dep.name().clone()));
777 }
778 if let Some(dep) = self
779 .build_dependencies()
780 .per_platform
781 .values()
782 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
783 .collect_vec()
784 .first()
785 {
786 return Err(LuaRockspecError::OffSpecBuildDependency(dep.name().clone()));
787 }
788 if let Some(dep) = self
789 .test_dependencies()
790 .per_platform
791 .values()
792 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
793 .collect_vec()
794 .first()
795 {
796 return Err(LuaRockspecError::OffSpecTestDependency(dep.name().clone()));
797 }
798 LocalLuaRockspec::new(
799 &self.to_lua_remote_rockspec_string()?,
800 ProjectRoot(self.internal.lux_toml_dir.clone()),
801 )
802 }
803
804 pub fn to_local_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
807 Ok(LocalLuaRockspec::from_project_toml(self).into_remote_with_source(self.source.clone()))
808 }
809
810 fn render_rockspec(
811 &self,
812 version: &PackageVersion,
813 source: Option<DisplayLuaKV>,
814 ) -> Result<String, ProjectTomlError> {
815 let starter = format!(
816 r#"
817rockspec_format = "{}"
818package = "{}"
819version = "{}""#,
820 self.rockspec_format
821 .as_ref()
822 .unwrap_or(&RockspecFormat::default()),
823 self.package,
824 version
825 );
826
827 let mut template = Vec::new();
828
829 if self.description != RockDescription::default() {
830 template.push(self.description.display_lua());
831 }
832
833 if self.supported_platforms != PlatformSupport::default() {
834 template.push(self.supported_platforms.display_lua());
835 }
836
837 {
838 let mut dependencies = self.internal.dependencies.clone().unwrap_or_default();
839 if !self.lua.is_luau() {
840 dependencies.insert(
841 0,
842 PackageReq {
843 name: "lua".into(),
844 version_req: self.lua.clone(),
845 }
846 .into(),
847 );
848 }
849 template.push(Dependencies(&dependencies).display_lua());
850 }
851
852 let mut build_dependencies = self
853 .internal
854 .build_dependencies
855 .as_ref()
856 .cloned()
857 .unwrap_or_default();
858
859 let build_backend_dependency = self
860 .internal
861 .build
862 .build_type
863 .as_ref()
864 .and_then(|build_type| build_type.luarocks_build_backend());
865
866 if let Some(build_backend_dependency) = build_backend_dependency {
867 build_dependencies.push(build_backend_dependency);
868 }
869
870 if !build_dependencies.is_empty() {
871 template.push(BuildDependencies(&build_dependencies).display_lua());
872 }
873
874 match self.internal.external_dependencies {
875 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
876 template.push(ExternalDependencies(external_dependencies).display_lua());
877 }
878 _ => {}
879 }
880
881 if let Some(source) = source {
882 template.push(source);
883 }
884
885 template.push(
886 build_spec_with_autodetected_modules(&self.internal.build, &self.internal.lux_toml_dir)
887 .display_lua(),
888 );
889
890 let unformatted_code = std::iter::once(starter)
891 .chain(template.into_iter().map(|kv| kv.to_string()))
892 .join("\n\n");
893 let result = match stylua_lib::format_code(
894 &unformatted_code,
895 stylua_lib::Config::default(),
896 None,
897 stylua_lib::OutputVerification::Full,
898 ) {
899 Ok(formatted_code) => formatted_code,
900 Err(_) => unformatted_code,
901 };
902 validate_generated_lua(&result)?;
903 Ok(result)
904 }
905}
906
907impl Rockspec for LocalProjectToml {
908 type Error = ProjectTomlError;
909
910 fn package(&self) -> &PackageName {
911 &self.package
912 }
913
914 fn version(&self) -> &PackageVersion {
915 &self.version
916 }
917
918 fn description(&self) -> &RockDescription {
919 &self.description
920 }
921
922 fn supported_platforms(&self) -> &PlatformSupport {
923 &self.supported_platforms
924 }
925
926 fn lua(&self) -> &PackageVersionReq {
927 &self.lua
928 }
929
930 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
931 &self.dependencies
932 }
933
934 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
935 &self.build_dependencies
936 }
937
938 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
939 &self.external_dependencies
940 }
941
942 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
943 &self.test_dependencies
944 }
945
946 fn build(&self) -> &PerPlatform<BuildSpec> {
947 &self.build
948 }
949
950 fn test(&self) -> &PerPlatform<TestSpec> {
951 &self.test
952 }
953
954 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
955 &mut self.build
956 }
957
958 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
959 &mut self.test
960 }
961
962 fn format(&self) -> &Option<RockspecFormat> {
963 &self.rockspec_format
964 }
965
966 fn source(&self) -> &PerPlatform<RemoteRockSource> {
967 &self.source
968 }
969
970 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
971 &mut self.source
972 }
973
974 fn deploy(&self) -> &PerPlatform<DeploySpec> {
975 &self.deploy
976 }
977
978 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
979 &mut self.deploy
980 }
981
982 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
983 let lux_toml_dir = &self.internal.lux_toml_dir;
984 let version = self
985 .internal
986 .version_template
987 .try_generate(lux_toml_dir, None)?;
988 let source =
989 self.internal
990 .source_template
991 .try_generate(lux_toml_dir, &self.package, &version)?;
992 self.render_rockspec(&version, Some(source.display_lua()))
993 }
994}
995
996fn build_spec_with_autodetected_modules(
999 spec: &BuildSpecInternal,
1000 project_root: &Path,
1001) -> BuildSpecInternal {
1002 let builtin_capable = spec
1003 .build_type
1004 .as_ref()
1005 .is_none_or(|build_type| matches!(build_type, BuildType::Builtin | BuildType::None));
1006 if !builtin_capable {
1007 return spec.clone();
1008 }
1009 let detected = match autodetect_modules(project_root, Default::default()) {
1010 Ok(modules) => modules
1011 .into_iter()
1012 .filter_map(|(module, spec)| match spec {
1013 ModuleSpec::SourcePath(path)
1014 if path.extension().is_some_and(|ext| ext == "luau") =>
1015 {
1016 Some((module, path))
1017 }
1018 _ => None,
1019 })
1020 .collect_vec(),
1021 Err(_) => return spec.clone(),
1022 };
1023 if detected.is_empty() {
1024 spec.clone()
1025 } else {
1026 let mut new_spec = spec.clone();
1027 let modules = new_spec.builtin_spec.get_or_insert_with(Default::default);
1028 for (module, path) in detected {
1029 modules
1030 .entry(LuaTableKey::StringKey(module.to_string()))
1031 .or_insert(ModuleSpecInternal::SourcePath(path));
1032 }
1033 new_spec
1034 }
1035}
1036
1037fn validate_generated_lua(lua_str: &str) -> Result<(), ProjectTomlError> {
1038 let mut lua = ottavino::Lua::core();
1039 lua.try_enter(|ctx| {
1040 let closure = ottavino::Closure::load(ctx, None, lua_str.as_bytes())?;
1041 let executor = ottavino::Executor::start(ctx, closure.into(), ());
1042 if !executor.step(ctx, &mut ottavino::Fuel::with(ROCKSPEC_FUEL_LIMIT))? {
1043 return Ok(Err(ProjectTomlError::FuelLimitExceeded));
1044 }
1045 Ok(Ok(()))
1046 })
1047 .map_err(|err| ProjectTomlError::GeneratedInvalidLua(err.to_string()))?
1048}
1049
1050#[derive(Error, Debug, Diagnostic)]
1051#[non_exhaustive]
1052#[error(transparent)]
1053pub enum ProjectTomlIntegrityError {
1054 LuaRockspecError(#[from] LuaRockspecError),
1055 IoError(#[from] io::Error),
1056 #[error(transparent)]
1057 #[diagnostic(transparent)]
1058 Fs(#[from] fs::FsError),
1059}
1060
1061impl HasIntegrity for LocalProjectToml {
1062 async fn hash(&self) -> io::Result<Integrity> {
1063 match self.to_lua_rockspec() {
1064 Ok(lua_rockspec) => lua_rockspec.hash().await,
1065 Err(_) => self.internal.hash().await,
1066 }
1067 }
1068}
1069
1070#[derive(Debug)]
1072pub struct RemoteProjectToml {
1073 local: LocalProjectToml,
1074 source: PerPlatform<RemoteRockSource>,
1075}
1076
1077impl RemoteProjectToml {
1078 pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
1079 RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
1080 }
1081}
1082
1083impl Rockspec for RemoteProjectToml {
1084 type Error = ProjectTomlError;
1085
1086 fn package(&self) -> &PackageName {
1087 self.local.package()
1088 }
1089
1090 fn version(&self) -> &PackageVersion {
1091 self.local.version()
1092 }
1093
1094 fn description(&self) -> &RockDescription {
1095 self.local.description()
1096 }
1097
1098 fn supported_platforms(&self) -> &PlatformSupport {
1099 self.local.supported_platforms()
1100 }
1101
1102 fn lua(&self) -> &PackageVersionReq {
1103 self.local.lua()
1104 }
1105
1106 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1107 self.local.dependencies()
1108 }
1109
1110 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1111 self.local.build_dependencies()
1112 }
1113
1114 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
1115 self.local.external_dependencies()
1116 }
1117
1118 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
1119 self.local.test_dependencies()
1120 }
1121
1122 fn build(&self) -> &PerPlatform<BuildSpec> {
1123 self.local.build()
1124 }
1125
1126 fn test(&self) -> &PerPlatform<TestSpec> {
1127 self.local.test()
1128 }
1129
1130 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
1131 self.local.build_mut()
1132 }
1133
1134 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
1135 self.local.test_mut()
1136 }
1137
1138 fn format(&self) -> &Option<RockspecFormat> {
1139 self.local.format()
1140 }
1141
1142 fn source(&self) -> &PerPlatform<RemoteRockSource> {
1143 &self.source
1144 }
1145
1146 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
1147 &mut self.source
1148 }
1149
1150 fn deploy(&self) -> &PerPlatform<DeploySpec> {
1151 self.local.deploy()
1152 }
1153
1154 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1155 self.local.deploy_mut()
1156 }
1157
1158 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1159 let lux_toml_dir = &self.local.internal.lux_toml_dir;
1160 let starter = format!(
1161 r#"
1162rockspec_format = "{}"
1163package = "{}"
1164version = "{}""#,
1165 self.local
1166 .rockspec_format
1167 .as_ref()
1168 .unwrap_or(&RockspecFormat::default()),
1169 self.local.package,
1170 self.version()
1171 );
1172
1173 let mut template = Vec::new();
1174
1175 if self.local.description != RockDescription::default() {
1176 template.push(self.local.description.display_lua());
1177 }
1178
1179 if self.local.supported_platforms != PlatformSupport::default() {
1180 template.push(self.local.supported_platforms.display_lua());
1181 }
1182
1183 {
1184 let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
1185 if !self.local.lua.is_luau() {
1186 dependencies.insert(
1187 0,
1188 PackageReq {
1189 name: "lua".into(),
1190 version_req: self.local.lua.clone(),
1191 }
1192 .into(),
1193 );
1194 }
1195 template.push(Dependencies(&dependencies).display_lua());
1196 }
1197
1198 let mut build_dependencies = self
1199 .local
1200 .internal
1201 .build_dependencies
1202 .as_ref()
1203 .cloned()
1204 .unwrap_or_default();
1205
1206 let build_backend_dependency = self
1207 .local
1208 .internal
1209 .build
1210 .build_type
1211 .as_ref()
1212 .and_then(|build_type| build_type.luarocks_build_backend());
1213
1214 if let Some(build_backend_dependency) = build_backend_dependency {
1215 build_dependencies.push(build_backend_dependency);
1216 }
1217
1218 if !build_dependencies.is_empty() {
1219 template.push(BuildDependencies(&build_dependencies).display_lua());
1220 }
1221
1222 match self.local.internal.external_dependencies {
1223 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1224 template.push(ExternalDependencies(external_dependencies).display_lua());
1225 }
1226 _ => {}
1227 }
1228
1229 let source = self.local.internal.source_template.try_generate(
1230 lux_toml_dir,
1231 &self.local.internal.package,
1232 self.version(),
1233 )?;
1234 template.push(source.display_lua());
1235
1236 if let Some(ref deploy) = self.local.internal.deploy {
1237 template.push(deploy.display_lua());
1238 }
1239
1240 template.push(
1241 build_spec_with_autodetected_modules(&self.local.internal.build, lux_toml_dir)
1242 .display_lua(),
1243 );
1244
1245 let unformatted_code = std::iter::once(starter)
1246 .chain(template.into_iter().map(|kv| kv.to_string()))
1247 .join("\n\n");
1248 let result = match stylua_lib::format_code(
1249 &unformatted_code,
1250 stylua_lib::Config::default(),
1251 None,
1252 stylua_lib::OutputVerification::Full,
1253 ) {
1254 Ok(formatted_code) => formatted_code,
1255 Err(_) => unformatted_code,
1256 };
1257 validate_generated_lua(&result)?;
1258 Ok(result)
1259 }
1260}
1261
1262impl HasIntegrity for RemoteProjectToml {
1263 async fn hash(&self) -> io::Result<Integrity> {
1264 self.to_lua_rockspec()
1265 .map_err(|err| {
1266 io::Error::other(format!(
1267 "unable to convert remote project to Lua rockspec:\n{}",
1268 err
1269 ))
1270 })?
1271 .hash()
1272 .await
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use std::path::PathBuf;
1279
1280 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1281 use git2::Repository;
1282 use url::Url;
1283
1284 use crate::{
1285 git::{url::RemoteGitUrl, GitRef, GitSource},
1286 lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1287 project::{Project, ProjectRoot, PROJECT_TOML},
1288 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1289 };
1290
1291 use super::PartialProjectToml;
1292
1293 #[test]
1294 fn project_toml_parsing() {
1295 let project_toml = r#"
1296 package = "my-package"
1297 version = "1.0.0"
1298 lua = "5.3"
1299
1300 rockspec_format = "1.0"
1301
1302 [source]
1303 url = "https://example.com"
1304
1305 [dependencies]
1306 foo = "1.0"
1307 bar = ">=2.0"
1308
1309 [run]
1310 args = ["--foo", "--bar"]
1311
1312 [build]
1313 type = "builtin"
1314 "#;
1315
1316 let project =
1317 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1318 let _ = project.into_remote(None).unwrap();
1319
1320 let project_toml = r#"
1321 package = "my-package"
1322 version = "1.0.0"
1323 lua = "5.1"
1324
1325 [description]
1326 summary = "A summary"
1327 detailed = "A detailed description"
1328 license = "MIT"
1329 homepage = "https://example.com"
1330 issues_url = "https://example.com/issues"
1331 maintainer = "John Doe"
1332 labels = ["label1", "label2"]
1333
1334 [supported_platforms]
1335 linux = true
1336 windows = false
1337
1338 [dependencies]
1339 foo = "1.0"
1340 bar = ">=2.0"
1341
1342 [build_dependencies]
1343 baz = "1.0"
1344
1345 [external_dependencies.foo]
1346 header = "foo.h"
1347
1348 [external_dependencies.bar]
1349 library = "libbar.so"
1350
1351 [test_dependencies]
1352 busted = "69.420"
1353
1354 [source]
1355 url = "https://example.com"
1356 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1357 file = "my-package-1.0.0.tar.gz"
1358 dir = "my-package-1.0.0"
1359
1360 [test]
1361 type = "command"
1362 script = "test.lua"
1363 flags = [ "foo", "bar" ]
1364
1365 [run]
1366 command = "my-command"
1367 args = ["--foo", "--bar"]
1368
1369 [build]
1370 type = "builtin"
1371 "#;
1372
1373 let project =
1374 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1375 let _ = project.into_remote(None).unwrap();
1376 }
1377
1378 #[test]
1379 fn compare_project_toml_with_rockspec() {
1380 let project_toml = r#"
1381 package = "my-package"
1382 version = "1.0.0"
1383 lua = "5.1"
1384
1385 # For testing, specify a custom rockspec format
1386 # (defaults to 3.0)
1387 rockspec_format = "1.0"
1388
1389 [description]
1390 summary = "A summary"
1391 detailed = "A detailed description"
1392 license = "MIT"
1393 homepage = "https://example.com"
1394 issues_url = "https://example.com/issues"
1395 maintainer = "John Doe"
1396 labels = ["label1", "label2"]
1397
1398 [supported_platforms]
1399 linux = true
1400 windows = false
1401
1402 [dependencies]
1403 foo = "1.0"
1404 bar = ">=2.0"
1405
1406 [build_dependencies]
1407 baz = "1.0"
1408
1409 [external_dependencies.foo]
1410 header = "foo.h"
1411
1412 [external_dependencies.bar]
1413 library = "libbar.so"
1414
1415 [test_dependencies]
1416 busted = "1.0"
1417
1418 [source]
1419 url = "https://example.com"
1420 file = "my-package-1.0.0.tar.gz"
1421 dir = "my-package-1.0.0"
1422
1423 [test]
1424 type = "command"
1425 script = "test.lua"
1426 flags = [ "foo", "bar" ]
1427
1428 [run]
1429 command = "my-command"
1430 args = ["--foo", "--bar"]
1431
1432 [deploy]
1433 wrap_bin_scripts = false
1434
1435 [build]
1436 type = "builtin"
1437
1438 [build.install.lua]
1439 "foo.bar" = "src/bar.lua"
1440
1441 [build.install.lib]
1442 "foo.baz" = "src/baz.c"
1443
1444 [build.install.bin]
1445 "bla" = "src/bla"
1446
1447 [build.install.conf]
1448 "cfg.conf" = "resources/config.conf"
1449 "#;
1450
1451 let expected_rockspec = r#"
1452 rockspec_format = "1.0"
1453 package = "my-package"
1454 version = "1.0.0"
1455
1456 source = {
1457 url = "https://example.com",
1458 file = "my-package-1.0.0.tar.gz",
1459 dir = "my-package-1.0.0",
1460 }
1461
1462 description = {
1463 summary = "A summary",
1464 detailed = "A detailed description",
1465 license = "MIT",
1466 homepage = "https://example.com",
1467 issues_url = "https://example.com/issues",
1468 maintainer = "John Doe",
1469 labels = {"label1", "label2"},
1470 }
1471
1472 supported_platforms = {"linux", "!windows"}
1473
1474 dependencies = {
1475 "lua ==5.1",
1476 "foo ==1.0",
1477 "bar >=2.0",
1478 }
1479
1480 build_dependencies = {
1481 "baz ==1.0",
1482 }
1483
1484 external_dependencies = {
1485 foo = { header = "foo.h" },
1486 bar = { library = "libbar.so" },
1487 }
1488
1489 source = {
1490 url = "https://example.com",
1491 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1492 file = "my-package-1.0.0.tar.gz",
1493 dir = "my-package-1.0.0",
1494 }
1495
1496 test = {
1497 type = "command",
1498 script = "test.lua",
1499 flags = {"foo", "bar"},
1500 }
1501
1502 deploy = {
1503 wrap_bin_scripts = false,
1504 }
1505
1506 build = {
1507 type = "builtin",
1508 install = {
1509 lua = {
1510 ["foo.bar"] = "src/bar.lua",
1511 },
1512 lib = {
1513 ["foo.baz"] = "src/baz.c",
1514 },
1515 bin = {
1516 bla = "src/bla",
1517 },
1518 conf = {
1519 ["cfg.conf"] = "resources/config.conf",
1520 },
1521 },
1522 }
1523 "#;
1524
1525 let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1526
1527 let project_toml =
1528 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1529 let rockspec = project_toml
1530 .into_remote(None)
1531 .unwrap()
1532 .to_lua_rockspec()
1533 .unwrap();
1534
1535 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1536 let mut v = v.current_platform().clone();
1537 v.sort_by(|a, b| a.name().cmp(b.name()));
1538 v
1539 };
1540
1541 assert_eq!(rockspec.package(), expected_rockspec.package());
1542 assert_eq!(rockspec.version(), expected_rockspec.version());
1543 assert_eq!(rockspec.description(), expected_rockspec.description());
1544 assert_eq!(
1545 rockspec.supported_platforms(),
1546 expected_rockspec.supported_platforms()
1547 );
1548 assert_eq!(
1549 sorted_package_reqs(rockspec.dependencies()),
1550 sorted_package_reqs(expected_rockspec.dependencies())
1551 );
1552 assert_eq!(
1553 sorted_package_reqs(rockspec.build_dependencies()),
1554 sorted_package_reqs(expected_rockspec.build_dependencies())
1555 );
1556 assert_eq!(
1557 rockspec.external_dependencies(),
1558 expected_rockspec.external_dependencies()
1559 );
1560 assert_eq!(rockspec.source(), expected_rockspec.source());
1561 assert_eq!(rockspec.build(), expected_rockspec.build());
1562 assert_eq!(rockspec.format(), expected_rockspec.format());
1563 }
1564
1565 #[test]
1566 fn merge_project_toml_with_partial_rockspec() {
1567 let project_toml = r#"
1568 package = "my-package"
1569 version = "1.0.0"
1570 lua = "5.1"
1571
1572 # For testing, specify a custom rockspec format
1573 # (defaults to 3.0)
1574 rockspec_format = "1.0"
1575
1576 [description]
1577 summary = "A summary"
1578 detailed = "A detailed description"
1579 license = "MIT"
1580 homepage = "https://example.com"
1581 issues_url = "https://example.com/issues"
1582 maintainer = "John Doe"
1583 labels = ["label1", "label2"]
1584
1585 [supported_platforms]
1586 linux = true
1587 windows = false
1588
1589 [dependencies]
1590 foo = "1.0"
1591 bar = ">=2.0"
1592
1593 [build_dependencies]
1594 baz = "1.0"
1595
1596 [external_dependencies.foo]
1597 header = "foo.h"
1598
1599 [external_dependencies.bar]
1600 library = "libbar.so"
1601
1602 [test_dependencies]
1603 busted = "1.0"
1604
1605 [source]
1606 url = "https://example.com"
1607 file = "my-package-1.0.0.tar.gz"
1608 dir = "my-package-1.0.0"
1609
1610 [test]
1611 type = "command"
1612 script = "test.lua"
1613 flags = [ "foo", "bar" ]
1614
1615 [run]
1616 command = "my-command"
1617 args = [ "--foo", "--bar" ]
1618
1619 [build]
1620 type = "builtin"
1621 "#;
1622
1623 let mergable_rockspec_content = r#"
1624 rockspec_format = "1.0"
1625 package = "my-package-overwritten"
1626
1627 description = {
1628 summary = "A summary overwritten",
1629 detailed = "A detailed description overwritten",
1630 license = "GPL-2.0",
1631 homepage = "https://example.com/overwritten",
1632 issues_url = "https://example.com/issues/overwritten",
1633 maintainer = "John Doe Overwritten",
1634 labels = {"over", "written"},
1635 }
1636
1637 -- Inverted supported platforms
1638 supported_platforms = {"!linux", "windows"}
1639
1640 dependencies = {
1641 "lua 5.1",
1642 "foo >1.0",
1643 "bar <=2.0",
1644 }
1645
1646 build_dependencies = {
1647 "baz >1.0",
1648 }
1649
1650 external_dependencies = {
1651 foo = { header = "overwritten.h" },
1652 bar = { library = "overwritten.so" },
1653 }
1654
1655 test = {
1656 type = "command",
1657 script = "overwritten.lua",
1658 flags = {"over", "written"},
1659 }
1660
1661 build = {
1662 type = "builtin",
1663 }
1664 "#;
1665
1666 let remote_rockspec_content = format!(
1667 r#"{}
1668 version = "1.0.0"
1669 source = {{
1670 url = "https://example.com",
1671 file = "my-package-1.0.0.tar.gz",
1672 dir = "my-package-1.0.0",
1673 }}
1674 "#,
1675 mergable_rockspec_content
1676 );
1677
1678 let project_toml =
1679 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1680 let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1681 let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1682
1683 let merged = project_toml
1684 .merge(partial_rockspec)
1685 .into_remote(None)
1686 .unwrap();
1687
1688 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1689 let mut v = v.current_platform().clone();
1690 v.sort_by(|a, b| a.name().cmp(b.name()));
1691 v
1692 };
1693
1694 assert_eq!(merged.package(), expected_rockspec.package());
1695 assert_eq!(merged.version(), expected_rockspec.version());
1696 assert_eq!(merged.description(), expected_rockspec.description());
1697 assert_eq!(
1698 merged.supported_platforms(),
1699 expected_rockspec.supported_platforms()
1700 );
1701 assert_eq!(
1702 sorted_package_reqs(merged.dependencies()),
1703 sorted_package_reqs(expected_rockspec.dependencies())
1704 );
1705 assert_eq!(
1706 sorted_package_reqs(merged.build_dependencies()),
1707 sorted_package_reqs(expected_rockspec.build_dependencies())
1708 );
1709 assert_eq!(
1710 merged.external_dependencies(),
1711 expected_rockspec.external_dependencies()
1712 );
1713 assert_eq!(merged.source(), expected_rockspec.source());
1714 assert_eq!(merged.build(), expected_rockspec.build());
1715 assert_eq!(merged.format(), expected_rockspec.format());
1716 assert!(merged.local.run().is_some());
1718 }
1719
1720 #[test]
1721 fn wally_dependency_parsing() {
1722 let project_toml = r#"
1723 package = "my-package"
1724 version = "1.0.0"
1725 lua = "5.1"
1726
1727 [dependencies.roact]
1728 wally = "jsdotperf/roact@^1.4.0"
1729
1730 [build]
1731 type = "builtin"
1732 "#;
1733
1734 let project =
1735 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1736 let deps = project.dependencies.unwrap();
1737 assert_eq!(deps.len(), 1);
1738 assert_eq!(deps[0].name().to_string(), "roact");
1739 assert!(matches!(
1740 deps[0].source(),
1741 Some(RockSourceSpec::Wally(req)) if req.name().to_string() == "jsdotperf/roact"
1742 ));
1743 }
1744
1745 #[test]
1746 fn wally_dependency_conflicts_with_version() {
1747 let project_toml = r#"
1748 package = "my-package"
1749 version = "1.0.0"
1750 lua = "5.1"
1751
1752 [dependencies.roact]
1753 wally = "jsdotperf/roact@^1.4.0"
1754 version = "1.4.0"
1755
1756 [build]
1757 type = "builtin"
1758 "#;
1759
1760 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap_err();
1761 }
1762
1763 #[test]
1764 fn project_toml_with_lua_in_dependencies() {
1765 let project_toml = r#"
1766 package = "my-package"
1767 version = "1.0.0"
1768 # lua = ">5.1"
1769
1770 [dependencies]
1771 lua = "5.1" # disallowed
1772
1773 [build]
1774 type = "builtin"
1775 "#;
1776
1777 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default())
1778 .unwrap()
1779 .into_local()
1780 .unwrap_err();
1781 }
1782
1783 #[test]
1784 fn project_toml_with_invalid_run_command() {
1785 for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1786 let project_toml = format!(
1787 r#"
1788 package = "my-package"
1789 version = "1.0.0"
1790 lua = "5.1"
1791
1792 [build]
1793 type = "builtin"
1794
1795 [run]
1796 command = "{command}"
1797 "#,
1798 );
1799
1800 PartialProjectToml::new(PROJECT_TOML, &project_toml, ProjectRoot::default())
1801 .unwrap_err();
1802 }
1803 }
1804
1805 #[test]
1806 fn project_root_dir_override_and_rockspec_generation() {
1807 let project_toml = r#"
1808 package = "my-package"
1809 version = "1.0.0"
1810 lua = "5.1"
1811
1812 [project]
1813 root_dir = ".."
1814
1815 [source]
1816 url = "https://example.com"
1817
1818 [build]
1819 type = "builtin"
1820 "#;
1821
1822 let workspace_root = assert_fs::TempDir::new().unwrap();
1823 let lux_toml_dir = workspace_root.child("proj");
1824 lux_toml_dir.create_dir_all().unwrap();
1825 std::fs::write(lux_toml_dir.join(PROJECT_TOML), project_toml).unwrap();
1826
1827 let project = PartialProjectToml::new(
1828 PROJECT_TOML,
1829 project_toml,
1830 ProjectRoot(lux_toml_dir.to_path_buf()),
1831 )
1832 .unwrap();
1833 let local = project.into_local().unwrap();
1834 let RockSourceSpec::File(root_dir) = &local.source().current_platform().source_spec else {
1835 panic!("expected a file source");
1836 };
1837 assert_eq!(
1838 root_dir,
1839 &std::fs::canonicalize(workspace_root.path()).unwrap()
1840 );
1841
1842 let rockspec = local.to_lua_rockspec().unwrap();
1844 assert!(!rockspec
1845 .to_lua_remote_rockspec_string()
1846 .unwrap()
1847 .contains("root_dir"));
1848 }
1849
1850 #[test]
1851 fn generate_non_deterministic_git_source() {
1852 let rockspec_content = r#"
1853 package = "test-package"
1854 version = "1.0.0"
1855 lua = ">=5.1"
1856
1857 [source]
1858 url = "git+https://exaple.com/repo.git"
1859
1860 [build]
1861 type = "builtin"
1862 "#;
1863
1864 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1865 .unwrap()
1866 .into_remote(None)
1867 .unwrap_err();
1868 }
1869
1870 #[test]
1871 fn generate_deterministic_git_source() {
1872 let rockspec_content = r#"
1873 package = "test-package"
1874 version = "1.0.0"
1875 lua = ">=5.1"
1876
1877 [source]
1878 url = "git+https://exaple.com/owner/repo.git"
1879 tag = "v0.1.0"
1880
1881 [build]
1882 type = "builtin"
1883 "#;
1884
1885 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1886 .unwrap()
1887 .into_remote(None)
1888 .unwrap();
1889 }
1890
1891 #[test]
1892 fn generate_git_source_with_branch() {
1893 let rockspec_content = r#"
1894 package = "test-package"
1895 version = "1.0.0"
1896 lua = ">=5.1"
1897
1898 [source]
1899 url = "git+https://example.com/owner/repo.git"
1900 branch = "main"
1901
1902 [build]
1903 type = "builtin"
1904 "#;
1905
1906 let toml = PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1907 .unwrap()
1908 .into_remote(None)
1909 .unwrap();
1910
1911 let lua = toml.to_lua_remote_rockspec_string().unwrap();
1912 assert!(lua.contains("branch = \"main\""));
1913 assert!(!lua.contains("tag = "));
1914 }
1915
1916 fn init_git_repo(path: &std::path::Path) -> Repository {
1917 let repo = Repository::init(path).unwrap();
1918 {
1919 let tree_id = repo.index().unwrap().write_tree().unwrap();
1920 let tree = repo.find_tree(tree_id).unwrap();
1921 let sig = git2::Signature::now("name", "email").unwrap();
1922 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
1923 .unwrap();
1924 }
1925 repo
1926 }
1927
1928 fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
1929 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1930 .join("resources/test/sample-projects/source-template/");
1931 temp_dir.copy_from(&sample_project, &["**"]).unwrap();
1932 init_git_repo(temp_dir.path())
1933 }
1934
1935 fn create_tag(repo: &Repository, name: &str) {
1936 let sig = git2::Signature::now("name", "email").unwrap();
1937 let id = repo.head().unwrap().target().unwrap();
1938 let obj = repo.find_object(id, None).unwrap();
1939 repo.tag(name, &obj, &sig, "msg", true).unwrap();
1940 }
1941
1942 #[test]
1943 fn test_git_project_generate_dev_source() {
1944 let project_root = assert_fs::TempDir::new().unwrap();
1945 init_sample_project_repo(&project_root);
1946 let project = Project::from_exact(&project_root).unwrap().unwrap();
1947 let remote_project_toml = project.toml().into_remote(None).unwrap();
1948 let source = remote_project_toml.source.current_platform();
1949 let source_spec = &source.source_spec;
1950 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1951 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
1952 let expected_url: RemoteGitUrl =
1953 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1954 assert_eq!(url, &expected_url);
1955 assert!(git_ref.is_some());
1956 }
1957 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1958 }
1959
1960 #[test]
1961 fn test_git_project_generate_non_semver_tag_source() {
1962 let project_root = assert_fs::TempDir::new().unwrap();
1963 let repo = init_sample_project_repo(&project_root);
1964 let tag_name = "bla";
1965 create_tag(&repo, tag_name);
1966 let project = Project::from_exact(&project_root).unwrap().unwrap();
1967 let remote_project_toml = project.toml().into_remote(None).unwrap();
1968 let source = remote_project_toml.source.current_platform();
1969 let source_spec = &source.source_spec;
1970 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1971 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
1972 let expected_url: RemoteGitUrl =
1973 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1974 assert_eq!(url, &expected_url);
1975 assert_eq!(git_ref, &Some(GitRef::Tag(tag_name.to_string())));
1976 }
1977 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1978 }
1979
1980 #[test]
1981 fn test_git_project_generate_release_source_tag_with_v_prefix() {
1982 let project_root = assert_fs::TempDir::new().unwrap();
1983 let repo = init_sample_project_repo(&project_root);
1984 let tag_name = "v1.0.0";
1985 create_tag(&repo, "bla");
1986 create_tag(&repo, tag_name);
1987 let project = Project::from_exact(&project_root).unwrap().unwrap();
1988 let remote_project_toml = project.toml().into_remote(None).unwrap();
1989 let source = remote_project_toml.source.current_platform();
1990 let source_spec = &source.source_spec;
1991 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1992 if let RockSourceSpec::Url(url) = source_spec {
1993 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
1994 .parse()
1995 .unwrap();
1996 assert_eq!(url, &expected_url);
1997 }
1998 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1999 }
2000
2001 #[test]
2002 fn test_git_project_generate_release_source_tag_without_v_prefix() {
2003 let project_root = assert_fs::TempDir::new().unwrap();
2004 let repo = init_sample_project_repo(&project_root);
2005 create_tag(&repo, "bla");
2006 let tag_name = "1.0.0";
2007 create_tag(&repo, tag_name);
2008 let project = Project::from_exact(&project_root).unwrap().unwrap();
2009 let remote_project_toml = project.toml().into_remote(None).unwrap();
2010 let source = remote_project_toml.source.current_platform();
2011 let source_spec = &source.source_spec;
2012 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2013 if let RockSourceSpec::Url(url) = source_spec {
2014 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
2015 .parse()
2016 .unwrap();
2017 assert_eq!(url, &expected_url);
2018 }
2019 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2020 }
2021
2022 #[test]
2023 fn test_git_project_in_subdirectory() {
2024 let temp_dir = assert_fs::TempDir::new().unwrap();
2025 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2026 .join("resources/test/sample-projects/source-template/");
2027 let project_dir = temp_dir.child("lux");
2028 project_dir.create_dir_all().unwrap();
2029 project_dir.copy_from(&sample_project, &["**"]).unwrap();
2030 let repo = init_git_repo(temp_dir.path());
2031 create_tag(&repo, "bla");
2032 let tag_name = "1.0.0";
2033 create_tag(&repo, tag_name);
2034 let project = Project::from_exact(&project_dir).unwrap().unwrap();
2035 let remote_project_toml = project.toml().into_remote(None).unwrap();
2036 let source = remote_project_toml.source.current_platform();
2037 let source_spec = &source.source_spec;
2038 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
2039 if let RockSourceSpec::Url(url) = source_spec {
2040 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
2041 .parse()
2042 .unwrap();
2043 assert_eq!(url, &expected_url);
2044 }
2045 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
2046 }
2047
2048 #[test]
2049 fn test_git_project_in_subdirectory_generate_dev_source() {
2050 let temp_dir = assert_fs::TempDir::new().unwrap();
2051 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2052 .join("resources/test/sample-projects/source-template/");
2053 let project_dir = temp_dir.child("lux");
2054 project_dir.create_dir_all().unwrap();
2055 project_dir.copy_from(&sample_project, &["**"]).unwrap();
2056 init_git_repo(temp_dir.path());
2057
2058 let project = Project::from_exact(&project_dir).unwrap().unwrap();
2059 let remote_project_toml = project.toml().into_remote(None).unwrap();
2060 let RockSourceSpec::Git(GitSource { git_ref, .. }) =
2061 &remote_project_toml.source.current_platform().source_spec
2062 else {
2063 panic!("expected a git source");
2064 };
2065 assert!(git_ref.is_some());
2066 }
2067}