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::{collections::HashMap, path::PathBuf};
24
25use crate::{
26 config::Config,
27 lua_rockspec::{
28 BuildSpec, BuildSpecInternal, BuildSpecInternalError, DisplayAsLuaKV, ExternalDependencies,
29 ExternalDependencySpec, LuaVersionError, PartialLuaRockspec, PerPlatform,
30 PlatformIdentifier, PlatformSupport, PlatformValidationError, RemoteRockSource,
31 RockDescription, RockSourceError, RockspecFormat, TestSpec, TestSpecDecodeError,
32 TestSpecInternal,
33 },
34 package::{
35 BuildDependencies, Dependencies, PackageName, PackageReq, PackageVersion, PackageVersionReq,
36 },
37 rockspec::{LuaVersionCompatibility, Rockspec},
38};
39use itertools::Itertools;
40use miette::Diagnostic;
41use nonempty::NonEmpty;
42use serde::de;
43use serde::{Deserialize, Deserializer};
44use ssri::Integrity;
45use thiserror::Error;
46
47use super::gen::GenerateSourceError;
48use super::gen::RockSourceTemplate;
49use super::r#gen::GenerateVersionError;
50use super::r#gen::PackageVersionTemplate;
51use super::ProjectRoot;
52
53pub const PROJECT_TOML: &str = "lux.toml";
54
55#[derive(Deserialize)]
56#[serde(untagged)]
57#[allow(clippy::large_enum_variant)] enum DependencyEntry {
59 Simple(PackageVersionReq),
60 Detailed(DependencyTableEntry),
61}
62
63#[derive(Debug, Deserialize)]
64struct DependencyTableEntry {
65 version: PackageVersionReq,
66 #[serde(default)]
67 opt: Option<bool>,
68 #[serde(default)]
69 pin: Option<bool>,
70 #[serde(default)]
71 git: Option<RemoteGitUrlShorthand>,
72 #[serde(default)]
73 path: Option<PathBuf>,
74 #[serde(default)]
75 rev: Option<String>,
76}
77
78fn parse_map_to_dependency_vec_opt<'de, D>(
79 deserializer: D,
80) -> Result<Option<Vec<LuaDependencySpec>>, D::Error>
81where
82 D: Deserializer<'de>,
83{
84 let packages: Option<HashMap<PackageName, DependencyEntry>> =
85 Option::deserialize(deserializer)?;
86
87 match packages {
88 None => Ok(None),
89 Some(packages) => Ok(Some(
90 packages
91 .into_iter()
92 .map(|(name, spec)| match spec {
93 DependencyEntry::Simple(version_req) => {
94 Ok(PackageReq { name, version_req }.into())
95 }
96 DependencyEntry::Detailed(entry) => {
97 let source = match (entry.git, entry.rev, entry.path) {
98 (None, None, None) => Ok(None),
99 (None, Some(_), None) => Err(de::Error::custom(format!(
100 "dependency {} specifies a 'rev', but missing a 'git' field",
101 name
102 ))),
103 (Some(git), Some(rev), None) => Ok(Some(RockSourceSpec::Git(GitSource {
104 url: git.into(),
105 git_ref: Some(GitRef::Tag(rev)),
106 }))),
107 (Some(git), None, None) => Ok(Some(RockSourceSpec::Git(GitSource {
108 url: git.into(),
109 git_ref: Some(GitRef::Tag(
110 entry
111 .version
112 .clone()
113 .to_string()
114 .trim_start_matches("=")
115 .to_string(),
116 )),
117 }))),
118 (None, None, Some(path)) => Ok(Some(RockSourceSpec::File(path))),
119 (_, _, Some(_)) => Err(de::Error::custom(format!(
120 "dependency '{}' specifies a 'path', which cannot be combined with 'git' or 'rev'",
121 name
122 ))),
123 }?;
124 Ok(LuaDependencySpec {
125 package_req: PackageReq {
126 name,
127 version_req: entry.version,
128 },
129 opt: OptState::from(entry.opt.unwrap_or(false)),
130 pin: PinnedState::from(entry.pin.unwrap_or(false)),
131 source,
132 })
133 }
134 })
135 .try_collect()?,
136 )),
137 }
138}
139
140#[derive(Debug, Error, Diagnostic)]
141pub enum ProjectTomlError {
142 #[error("error generating rockspec source")]
143 #[diagnostic(forward(0))]
144 GenerateSource(#[from] GenerateSourceError),
145 #[error("error generating rockspec version")]
146 #[diagnostic(forward(0))]
147 GenerateVersion(#[from] GenerateVersionError),
148 #[error("generated rockspec exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
149 #[diagnostic(help(
150 r#"the rockspec may be too complex or contain too many dependencies.
151 Try reducing the number of dependencies or simplifying build instructions."#
152 ))]
153 FuelLimitExceeded,
154 #[error(
155 r#"generated invalid Lua from TOML:
156{0}
157"#
158 )]
159 #[diagnostic(help("this is a bug in Lux. please report it."))]
160 GeneratedInvalidLua(String),
161}
162
163#[derive(Debug, Error, Diagnostic)]
164pub enum LocalProjectTomlValidationError {
165 #[error("no lua version provided")]
166 #[diagnostic(help(
167 r#"add a `lua` field to your lux.toml, for example:
168
169 lua = ">=5.1""#
170 ))]
171 NoLuaVersion,
172 #[error("could not decode the test spec")]
173 #[diagnostic(forward(0))]
174 TestSpecError(#[from] TestSpecDecodeError),
175 #[error("could not decode the build spec")]
176 #[diagnostic(forward(0))]
177 BuildSpecInternal(#[from] BuildSpecInternalError),
178 #[error(transparent)]
179 #[diagnostic(transparent)]
180 PlatformValidation(#[from] PlatformValidationError),
181 #[error("{}copy_directories cannot contain a rockspec name", ._0.as_ref().map(|p| format!("{p}: ")).unwrap_or_default())]
182 CopyDirectoriesContainRockspecName(Option<String>),
183 #[error("could not decode the source spec")]
184 #[diagnostic(forward(0))]
185 RockSource(#[from] RockSourceError),
186 #[error("duplicate dependencies: {0}")]
187 #[diagnostic(help("remove the duplicate entries from the [dependencies] table."))]
188 DuplicateDependencies(PackageNameList),
189 #[error("duplicate test dependencies: {0}")]
190 #[diagnostic(help("remove the duplicate entries from the [test_dependencies] table."))]
191 DuplicateTestDependencies(PackageNameList),
192 #[error("duplicate build dependencies: {0}")]
193 #[diagnostic(help("remove the duplicate entries from the [build_dependencies] table."))]
194 DuplicateBuildDependencies(PackageNameList),
195 #[error(
196 r#"dependencies field cannot contain 'lua'.
197 Please provide the version in the top-level 'lua' field
198
199 Example:
200
201 ```toml
202 package = "my-package"
203 lua = ">=5.1"
204
205 [dependencies]
206 # do not add Lua here!
207 ```
208 "#
209 )]
210 DependenciesContainLua,
211 #[error("error generating rockspec source")]
212 #[diagnostic(forward(0))]
213 GenerateSource(#[from] GenerateSourceError),
214 #[error("error generating rockspec version")]
215 #[diagnostic(forward(0))]
216 GenerateVersion(#[from] GenerateVersionError),
217}
218
219#[derive(Debug, Error, Diagnostic)]
220pub enum RemoteProjectTomlValidationError {
221 #[error("error generating rockspec source")]
222 #[diagnostic(forward(0))]
223 GenerateSource(#[from] GenerateSourceError),
224 #[error("error generating rockspec version")]
225 #[diagnostic(forward(0))]
226 GenerateVersion(#[from] GenerateVersionError),
227 #[error(transparent)]
228 #[diagnostic(transparent)]
229 LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
230}
231
232#[derive(Clone, Debug, Deserialize)]
236pub struct PartialProjectToml {
237 pub(crate) package: PackageName,
238 #[serde(default, rename = "version")]
239 pub(crate) version_template: PackageVersionTemplate,
240 #[serde(default)]
241 pub(crate) build: BuildSpecInternal,
242 pub(crate) rockspec_format: Option<RockspecFormat>,
243 #[serde(default)]
244 pub(crate) run: Option<RunSpec>,
245 #[serde(default)]
246 pub(crate) lua: Option<PackageVersionReq>,
247 #[serde(default)]
248 pub(crate) description: Option<RockDescription>,
249 #[serde(default)]
250 pub(crate) supported_platforms: Option<HashMap<PlatformIdentifier, bool>>,
251 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
252 pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
253 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
254 pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
255 #[serde(default)]
256 pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
257 #[serde(default, deserialize_with = "parse_map_to_dependency_vec_opt")]
258 pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
259 #[serde(default, rename = "source")]
260 pub(crate) source_template: RockSourceTemplate,
261 #[serde(default)]
262 pub(crate) test: Option<TestSpecInternal>,
263 #[serde(default)]
264 pub(crate) deploy: Option<DeploySpec>,
265 #[serde(default)]
266 pub(crate) project: ProjectSpec,
267
268 #[serde(skip, default = "PathBuf::new")]
270 lux_toml_dir: PathBuf,
271}
272
273impl HasIntegrity for PartialProjectToml {
274 async fn hash(&self) -> io::Result<Integrity> {
275 let toml_file = self.lux_toml_dir.join(PROJECT_TOML);
276 let content = fs::sync::read_to_string(&toml_file).map_err(io::Error::other)?;
277 Ok(Integrity::from(&content))
278 }
279}
280
281impl PartialProjectToml {
282 pub(crate) fn new(
283 name: &str,
284 str: &str,
285 lux_toml_dir: ProjectRoot,
286 ) -> Result<Self, TomlDeError> {
287 let mut this: Self = super::parse_toml(name, str)?;
288 this.lux_toml_dir = lux_toml_dir.to_path_buf();
289 this.project.root = match this.project.root_dir {
290 Some(ref root_dir) => {
291 let relative_project_root = lux_toml_dir.join(root_dir);
292 let project_root =
293 std::fs::canonicalize(&relative_project_root).unwrap_or(relative_project_root);
294 ProjectRoot(project_root)
295 }
296 None => lux_toml_dir,
297 };
298 Ok(this)
299 }
300
301 pub fn lua(&self) -> Option<&PackageVersionReq> {
303 self.lua.as_ref()
304 }
305
306 pub fn into_local(&self) -> Result<LocalProjectToml, LocalProjectTomlValidationError> {
309 let project_toml = self.clone();
310
311 if project_toml
313 .dependencies
314 .as_ref()
315 .is_some_and(|deps| deps.iter().any(|dep| dep.name() == &"lua".into()))
316 {
317 return Err(LocalProjectTomlValidationError::DependenciesContainLua);
318 }
319
320 let get_duplicates = |dependencies: &Option<Vec<LuaDependencySpec>>| {
321 dependencies
322 .iter()
323 .flat_map(|deps| {
324 deps.iter()
325 .map(|dep| dep.package_req().name())
326 .duplicates()
327 .cloned()
328 })
329 .collect_vec()
330 };
331 let duplicate_dependencies = get_duplicates(&self.dependencies);
332 if !duplicate_dependencies.is_empty() {
333 return Err(LocalProjectTomlValidationError::DuplicateDependencies(
334 PackageNameList::new(duplicate_dependencies),
335 ));
336 }
337 let duplicate_test_dependencies = get_duplicates(&self.test_dependencies);
338 if !duplicate_test_dependencies.is_empty() {
339 return Err(LocalProjectTomlValidationError::DuplicateTestDependencies(
340 PackageNameList::new(duplicate_test_dependencies),
341 ));
342 }
343 let duplicate_build_dependencies = get_duplicates(&self.build_dependencies);
344 if !duplicate_build_dependencies.is_empty() {
345 return Err(LocalProjectTomlValidationError::DuplicateBuildDependencies(
346 PackageNameList::new(duplicate_build_dependencies),
347 ));
348 }
349
350 let validated = LocalProjectToml {
351 internal: project_toml.clone(),
352
353 package: project_toml.package,
354 version: project_toml
355 .version_template
356 .try_generate(&self.lux_toml_dir, None)
357 .unwrap_or(PackageVersion::default_dev_version()),
358 lua: project_toml
359 .lua
360 .ok_or(LocalProjectTomlValidationError::NoLuaVersion)?,
361 description: project_toml.description.unwrap_or_default(),
362 run: project_toml.run.map(PerPlatform::new),
363 supported_platforms: PlatformSupport::parse(
364 &project_toml
365 .supported_platforms
366 .unwrap_or_default()
367 .into_iter()
368 .map(|(platform, supported)| {
369 if supported {
370 format!("{platform}")
371 } else {
372 format!("!{platform}")
373 }
374 })
375 .collect_vec(),
376 )?,
377 dependencies: PerPlatform::new(
380 project_toml
381 .dependencies
382 .unwrap_or_default()
383 .into_iter()
384 .map(|dep| self.resolve_lua_dependency_spec(dep))
385 .collect_vec(),
386 ),
387 test_dependencies: PerPlatform::new(
388 project_toml
389 .test_dependencies
390 .unwrap_or_default()
391 .into_iter()
392 .map(|dep| self.resolve_lua_dependency_spec(dep))
393 .collect_vec(),
394 ),
395 build_dependencies: PerPlatform::new(
396 project_toml
397 .build_dependencies
398 .unwrap_or_default()
399 .into_iter()
400 .map(|dep| self.resolve_lua_dependency_spec(dep))
401 .collect_vec(),
402 ),
403 external_dependencies: PerPlatform::new(
404 project_toml.external_dependencies.unwrap_or_default(),
405 ),
406 test: PerPlatform::new(TestSpec::try_from(
407 project_toml.test.clone().unwrap_or_default(),
408 )?),
409 build: PerPlatform::new(BuildSpec::from_internal_spec(project_toml.build.clone())?),
410 deploy: PerPlatform::new(project_toml.deploy.clone().unwrap_or_default()),
411 rockspec_format: project_toml.rockspec_format.clone(),
412
413 source: PerPlatform::new(RemoteRockSource {
414 local: LocalRockSource::default(),
415 source_spec: RockSourceSpec::File(self.project.root.to_path_buf()),
416 }),
417 };
418
419 let rockspec_file_name = format!("{}-{}.rockspec", validated.package, validated.version);
420
421 if validated
422 .build
423 .default
424 .copy_directories
425 .contains(&PathBuf::from(&rockspec_file_name))
426 {
427 return Err(LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(None));
428 }
429
430 for (platform, build_override) in &validated.build.per_platform {
431 if build_override
432 .copy_directories
433 .contains(&PathBuf::from(&rockspec_file_name))
434 {
435 return Err(
436 LocalProjectTomlValidationError::CopyDirectoriesContainRockspecName(Some(
437 platform.to_string(),
438 )),
439 );
440 }
441 }
442
443 Ok(validated)
444 }
445
446 pub fn into_remote(
450 &self,
451 specrev: Option<SpecRev>,
452 ) -> Result<RemoteProjectToml, RemoteProjectTomlValidationError> {
453 let version = self
454 .version_template
455 .try_generate(&self.lux_toml_dir, specrev)?;
456 let source =
457 self.source_template
458 .try_generate(&self.lux_toml_dir, &self.package, &version)?;
459 let source = PerPlatform::new(RemoteRockSource::try_from(source).map_err(|err| {
460 RemoteProjectTomlValidationError::LocalProjectTomlValidationError(
461 LocalProjectTomlValidationError::RockSource(err),
462 )
463 })?);
464 let mut local = self.into_local()?;
465 local.version = version;
466
467 let validated = RemoteProjectToml { source, local };
468
469 Ok(validated)
470 }
471
472 pub fn package(&self) -> &PackageName {
475 &self.package
476 }
477
478 pub fn version(&self) -> Result<PackageVersion, GenerateVersionError> {
480 self.version_template.try_generate(&self.lux_toml_dir, None)
481 }
482
483 pub fn merge(self, other: PartialLuaRockspec) -> Self {
486 PartialProjectToml {
487 package: other.package.unwrap_or(self.package),
488 version_template: self.version_template,
489 lua: other
490 .dependencies
491 .as_ref()
492 .and_then(|deps| {
493 deps.iter()
494 .find(|dep| dep.name() == &"lua".into())
495 .and_then(|dep| {
496 if dep.version_req().is_any() {
497 None
498 } else {
499 Some(dep.version_req().clone())
500 }
501 })
502 })
503 .or(self.lua),
504 build: other.build.unwrap_or(self.build),
505 run: self.run,
506 description: other.description.or(self.description),
507 supported_platforms: other
508 .supported_platforms
509 .map(|platform_support| platform_support.platforms().clone())
510 .or(self.supported_platforms),
511 dependencies: other
512 .dependencies
513 .map(|deps| {
514 deps.into_iter()
515 .filter(|dep| dep.name() != &"lua".into())
516 .collect()
517 })
518 .or(self.dependencies),
519 build_dependencies: other.build_dependencies.or(self.build_dependencies),
520 test_dependencies: other.test_dependencies.or(self.test_dependencies),
521 external_dependencies: other.external_dependencies.or(self.external_dependencies),
522 source_template: self.source_template,
523 test: other.test.or(self.test),
524 deploy: other.deploy.or(self.deploy),
525 rockspec_format: other.rockspec_format.or(self.rockspec_format),
526
527 project: self.project,
529 lux_toml_dir: self.lux_toml_dir.clone(),
530 }
531 }
532
533 fn resolve_lua_dependency_spec(&self, dep: LuaDependencySpec) -> LuaDependencySpec {
534 match &dep.source {
535 Some(RockSourceSpec::File(path)) if path.is_dir() => dep,
536 Some(RockSourceSpec::File(path)) => LuaDependencySpec {
537 source: Some(RockSourceSpec::File(self.lux_toml_dir.join(path))),
538 ..dep
539 },
540 _ => dep,
541 }
542 }
543}
544
545impl LuaVersionCompatibility for PartialProjectToml {
549 fn validate_lua_version(&self, version: &LuaVersion) -> Result<(), LuaVersionError> {
550 if self.supports_lua_version(version) {
551 Ok(())
552 } else {
553 Err(LuaVersionError::LuaVersionUnsupported(
554 version.clone(),
555 self.package().to_owned(),
556 self.version_template
557 .try_generate(&self.lux_toml_dir, None)
558 .unwrap_or(PackageVersion::default_dev_version()),
559 ))
560 }
561 }
562
563 fn validate_lua_version_from_config(&self, config: &Config) -> Result<(), LuaVersionError> {
564 let _ = self.lua_version_matches(config)?;
565 Ok(())
566 }
567
568 fn lua_version_matches(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
569 let version = LuaVersion::from(config)?.clone();
570 if self.supports_lua_version(&version) {
571 Ok(version)
572 } else {
573 Err(LuaVersionError::LuaVersionUnsupported(
574 version,
575 self.package.clone(),
576 self.version_template
577 .try_generate(&self.lux_toml_dir, None)
578 .unwrap_or(PackageVersion::default_dev_version()),
579 ))
580 }
581 }
582
583 fn supports_lua_version(&self, lua_version: &LuaVersion) -> bool {
584 self.lua
585 .as_ref()
586 .is_none_or(|lua| lua.matches(&lua_version.as_version()))
587 }
588
589 fn lua_version(&self) -> Option<LuaVersion> {
590 for (possibility, version) in [
591 ("5.5.0", LuaVersion::Lua55),
592 ("5.4.0", LuaVersion::Lua54),
593 ("5.3.0", LuaVersion::Lua53),
594 ("5.2.0", LuaVersion::Lua52),
595 ("5.1.0", LuaVersion::Lua51),
596 ] {
597 let possibility = unsafe { possibility.parse().unwrap_unchecked() };
598 if self
599 .lua
600 .as_ref()
601 .is_none_or(|lua| lua.matches(&possibility))
602 {
603 return Some(version);
604 }
605 }
606 None
607 }
608}
609
610#[derive(Debug, Clone, Deserialize)]
614pub(crate) struct ProjectSpec {
615 root_dir: Option<PathBuf>,
619 #[serde(skip, default = "ProjectRoot::new")]
621 pub(crate) root: ProjectRoot,
622}
623
624impl Default for ProjectSpec {
625 fn default() -> Self {
626 Self {
627 root_dir: None,
628 root: ProjectRoot::new(),
629 }
630 }
631}
632
633#[derive(Debug, Clone, Deserialize)]
635pub struct RunSpec {
636 pub(crate) command: Option<RunCommand>,
638 pub(crate) args: Option<NonEmpty<String>>,
640}
641
642#[derive(Debug)]
646pub struct LocalProjectToml {
647 package: PackageName,
648 version: PackageVersion,
649 lua: PackageVersionReq,
650 rockspec_format: Option<RockspecFormat>,
651 run: Option<PerPlatform<RunSpec>>,
652 description: RockDescription,
653 supported_platforms: PlatformSupport,
654 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
655 build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
656 external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
657 test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
658 test: PerPlatform<TestSpec>,
659 build: PerPlatform<BuildSpec>,
660 deploy: PerPlatform<DeploySpec>,
661
662 internal: PartialProjectToml,
664
665 source: PerPlatform<RemoteRockSource>,
667}
668
669impl LocalProjectToml {
670 pub fn run(&self) -> Option<&PerPlatform<RunSpec>> {
671 self.run.as_ref()
672 }
673
674 pub fn to_lua_rockspec(&self) -> Result<LocalLuaRockspec, LuaRockspecError> {
677 if let Some(dep) = self
678 .dependencies()
679 .per_platform
680 .values()
681 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
682 .collect_vec()
683 .first()
684 {
685 return Err(LuaRockspecError::OffSpecDependency(dep.name().clone()));
686 }
687 if let Some(dep) = self
688 .build_dependencies()
689 .per_platform
690 .values()
691 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
692 .collect_vec()
693 .first()
694 {
695 return Err(LuaRockspecError::OffSpecBuildDependency(dep.name().clone()));
696 }
697 if let Some(dep) = self
698 .test_dependencies()
699 .per_platform
700 .values()
701 .filter_map(|deps| deps.iter().find(|dep| dep.source().is_some()))
702 .collect_vec()
703 .first()
704 {
705 return Err(LuaRockspecError::OffSpecTestDependency(dep.name().clone()));
706 }
707 LocalLuaRockspec::new(
708 &self.to_lua_remote_rockspec_string()?,
709 ProjectRoot(self.internal.lux_toml_dir.clone()),
710 )
711 }
712}
713
714impl Rockspec for LocalProjectToml {
715 type Error = ProjectTomlError;
716
717 fn package(&self) -> &PackageName {
718 &self.package
719 }
720
721 fn version(&self) -> &PackageVersion {
722 &self.version
723 }
724
725 fn description(&self) -> &RockDescription {
726 &self.description
727 }
728
729 fn supported_platforms(&self) -> &PlatformSupport {
730 &self.supported_platforms
731 }
732
733 fn lua(&self) -> &PackageVersionReq {
734 &self.lua
735 }
736
737 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
738 &self.dependencies
739 }
740
741 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
742 &self.build_dependencies
743 }
744
745 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
746 &self.external_dependencies
747 }
748
749 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
750 &self.test_dependencies
751 }
752
753 fn build(&self) -> &PerPlatform<BuildSpec> {
754 &self.build
755 }
756
757 fn test(&self) -> &PerPlatform<TestSpec> {
758 &self.test
759 }
760
761 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
762 &mut self.build
763 }
764
765 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
766 &mut self.test
767 }
768
769 fn format(&self) -> &Option<RockspecFormat> {
770 &self.rockspec_format
771 }
772
773 fn source(&self) -> &PerPlatform<RemoteRockSource> {
774 &self.source
775 }
776
777 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
778 &mut self.source
779 }
780
781 fn deploy(&self) -> &PerPlatform<DeploySpec> {
782 &self.deploy
783 }
784
785 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
786 &mut self.deploy
787 }
788
789 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
790 let lux_toml_dir = &self.internal.lux_toml_dir;
791 let version = self
792 .internal
793 .version_template
794 .try_generate(lux_toml_dir, None)?;
795 let starter = format!(
796 r#"
797rockspec_format = "{}"
798package = "{}"
799version = "{}""#,
800 self.rockspec_format
801 .as_ref()
802 .unwrap_or(&RockspecFormat::default()),
803 self.package,
804 version
805 );
806
807 let mut template = Vec::new();
808
809 if self.description != RockDescription::default() {
810 template.push(self.description.display_lua());
811 }
812
813 if self.supported_platforms != PlatformSupport::default() {
814 template.push(self.supported_platforms.display_lua());
815 }
816
817 {
818 let mut dependencies = self.internal.dependencies.clone().unwrap_or_default();
819 dependencies.insert(
820 0,
821 PackageReq {
822 name: "lua".into(),
823 version_req: self.lua.clone(),
824 }
825 .into(),
826 );
827 template.push(Dependencies(&dependencies).display_lua());
828 }
829
830 let mut build_dependencies = self
831 .internal
832 .build_dependencies
833 .as_ref()
834 .cloned()
835 .unwrap_or_default();
836
837 let build_backend_dependency = self
838 .internal
839 .build
840 .build_type
841 .as_ref()
842 .and_then(|build_type| build_type.luarocks_build_backend());
843
844 if let Some(build_backend_dependency) = build_backend_dependency {
845 build_dependencies.push(build_backend_dependency);
846 }
847
848 if !build_dependencies.is_empty() {
849 template.push(BuildDependencies(&build_dependencies).display_lua());
850 }
851
852 match self.internal.external_dependencies {
853 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
854 template.push(ExternalDependencies(external_dependencies).display_lua());
855 }
856 _ => {}
857 }
858
859 let source =
860 self.internal
861 .source_template
862 .try_generate(lux_toml_dir, &self.package, &version)?;
863 template.push(source.display_lua());
864
865 template.push(self.internal.build.display_lua());
866
867 let unformatted_code = std::iter::once(starter)
868 .chain(template.into_iter().map(|kv| kv.to_string()))
869 .join("\n\n");
870 let result = match stylua_lib::format_code(
871 &unformatted_code,
872 stylua_lib::Config::default(),
873 None,
874 stylua_lib::OutputVerification::Full,
875 ) {
876 Ok(formatted_code) => formatted_code,
877 Err(_) => unformatted_code,
878 };
879 validate_generated_lua(&result)?;
880 Ok(result)
881 }
882}
883
884fn validate_generated_lua(lua_str: &str) -> Result<(), ProjectTomlError> {
885 let mut lua = ottavino::Lua::core();
886 lua.try_enter(|ctx| {
887 let closure = ottavino::Closure::load(ctx, None, lua_str.as_bytes())?;
888 let executor = ottavino::Executor::start(ctx, closure.into(), ());
889 if !executor.step(ctx, &mut ottavino::Fuel::with(ROCKSPEC_FUEL_LIMIT))? {
890 return Ok(Err(ProjectTomlError::FuelLimitExceeded));
891 }
892 Ok(Ok(()))
893 })
894 .map_err(|err| ProjectTomlError::GeneratedInvalidLua(err.to_string()))?
895}
896
897#[derive(Error, Debug, Diagnostic)]
898#[non_exhaustive]
899#[error(transparent)]
900pub enum ProjectTomlIntegrityError {
901 LuaRockspecError(#[from] LuaRockspecError),
902 IoError(#[from] io::Error),
903 #[error(transparent)]
904 #[diagnostic(transparent)]
905 Fs(#[from] fs::FsError),
906}
907
908impl HasIntegrity for LocalProjectToml {
909 async fn hash(&self) -> io::Result<Integrity> {
910 match self.to_lua_rockspec() {
911 Ok(lua_rockspec) => lua_rockspec.hash().await,
912 Err(_) => self.internal.hash().await,
913 }
914 }
915}
916
917#[derive(Debug)]
919pub struct RemoteProjectToml {
920 local: LocalProjectToml,
921 source: PerPlatform<RemoteRockSource>,
922}
923
924impl RemoteProjectToml {
925 pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
926 RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
927 }
928}
929
930impl Rockspec for RemoteProjectToml {
931 type Error = ProjectTomlError;
932
933 fn package(&self) -> &PackageName {
934 self.local.package()
935 }
936
937 fn version(&self) -> &PackageVersion {
938 self.local.version()
939 }
940
941 fn description(&self) -> &RockDescription {
942 self.local.description()
943 }
944
945 fn supported_platforms(&self) -> &PlatformSupport {
946 self.local.supported_platforms()
947 }
948
949 fn lua(&self) -> &PackageVersionReq {
950 self.local.lua()
951 }
952
953 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
954 self.local.dependencies()
955 }
956
957 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
958 self.local.build_dependencies()
959 }
960
961 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
962 self.local.external_dependencies()
963 }
964
965 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
966 self.local.test_dependencies()
967 }
968
969 fn build(&self) -> &PerPlatform<BuildSpec> {
970 self.local.build()
971 }
972
973 fn test(&self) -> &PerPlatform<TestSpec> {
974 self.local.test()
975 }
976
977 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
978 self.local.build_mut()
979 }
980
981 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
982 self.local.test_mut()
983 }
984
985 fn format(&self) -> &Option<RockspecFormat> {
986 self.local.format()
987 }
988
989 fn source(&self) -> &PerPlatform<RemoteRockSource> {
990 &self.source
991 }
992
993 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
994 &mut self.source
995 }
996
997 fn deploy(&self) -> &PerPlatform<DeploySpec> {
998 self.local.deploy()
999 }
1000
1001 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1002 self.local.deploy_mut()
1003 }
1004
1005 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1006 let lux_toml_dir = &self.local.internal.lux_toml_dir;
1007 let starter = format!(
1008 r#"
1009rockspec_format = "{}"
1010package = "{}"
1011version = "{}""#,
1012 self.local
1013 .rockspec_format
1014 .as_ref()
1015 .unwrap_or(&RockspecFormat::default()),
1016 self.local.package,
1017 self.version()
1018 );
1019
1020 let mut template = Vec::new();
1021
1022 if self.local.description != RockDescription::default() {
1023 template.push(self.local.description.display_lua());
1024 }
1025
1026 if self.local.supported_platforms != PlatformSupport::default() {
1027 template.push(self.local.supported_platforms.display_lua());
1028 }
1029
1030 {
1031 let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
1032 dependencies.insert(
1033 0,
1034 PackageReq {
1035 name: "lua".into(),
1036 version_req: self.local.lua.clone(),
1037 }
1038 .into(),
1039 );
1040 template.push(Dependencies(&dependencies).display_lua());
1041 }
1042
1043 let mut build_dependencies = self
1044 .local
1045 .internal
1046 .build_dependencies
1047 .as_ref()
1048 .cloned()
1049 .unwrap_or_default();
1050
1051 let build_backend_dependency = self
1052 .local
1053 .internal
1054 .build
1055 .build_type
1056 .as_ref()
1057 .and_then(|build_type| build_type.luarocks_build_backend());
1058
1059 if let Some(build_backend_dependency) = build_backend_dependency {
1060 build_dependencies.push(build_backend_dependency);
1061 }
1062
1063 if !build_dependencies.is_empty() {
1064 template.push(BuildDependencies(&build_dependencies).display_lua());
1065 }
1066
1067 match self.local.internal.external_dependencies {
1068 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1069 template.push(ExternalDependencies(external_dependencies).display_lua());
1070 }
1071 _ => {}
1072 }
1073
1074 let source = self.local.internal.source_template.try_generate(
1075 lux_toml_dir,
1076 &self.local.internal.package,
1077 self.version(),
1078 )?;
1079 template.push(source.display_lua());
1080
1081 if let Some(ref deploy) = self.local.internal.deploy {
1082 template.push(deploy.display_lua());
1083 }
1084
1085 template.push(self.local.internal.build.display_lua());
1086
1087 let unformatted_code = std::iter::once(starter)
1088 .chain(template.into_iter().map(|kv| kv.to_string()))
1089 .join("\n\n");
1090 let result = match stylua_lib::format_code(
1091 &unformatted_code,
1092 stylua_lib::Config::default(),
1093 None,
1094 stylua_lib::OutputVerification::Full,
1095 ) {
1096 Ok(formatted_code) => formatted_code,
1097 Err(_) => unformatted_code,
1098 };
1099 validate_generated_lua(&result)?;
1100 Ok(result)
1101 }
1102}
1103
1104impl HasIntegrity for RemoteProjectToml {
1105 async fn hash(&self) -> io::Result<Integrity> {
1106 self.to_lua_rockspec()
1107 .map_err(|err| {
1108 io::Error::other(format!(
1109 "unable to convert remote project to Lua rockspec:\n{}",
1110 err
1111 ))
1112 })?
1113 .hash()
1114 .await
1115 }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120 use std::path::PathBuf;
1121
1122 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1123 use git2::Repository;
1124 use url::Url;
1125
1126 use crate::{
1127 git::{url::RemoteGitUrl, GitRef, GitSource},
1128 lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1129 project::{Project, ProjectRoot, PROJECT_TOML},
1130 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1131 };
1132
1133 use super::PartialProjectToml;
1134
1135 #[test]
1136 fn project_toml_parsing() {
1137 let project_toml = r#"
1138 package = "my-package"
1139 version = "1.0.0"
1140 lua = "5.3"
1141
1142 rockspec_format = "1.0"
1143
1144 [source]
1145 url = "https://example.com"
1146
1147 [dependencies]
1148 foo = "1.0"
1149 bar = ">=2.0"
1150
1151 [run]
1152 args = ["--foo", "--bar"]
1153
1154 [build]
1155 type = "builtin"
1156 "#;
1157
1158 let project =
1159 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1160 let _ = project.into_remote(None).unwrap();
1161
1162 let project_toml = r#"
1163 package = "my-package"
1164 version = "1.0.0"
1165 lua = "5.1"
1166
1167 [description]
1168 summary = "A summary"
1169 detailed = "A detailed description"
1170 license = "MIT"
1171 homepage = "https://example.com"
1172 issues_url = "https://example.com/issues"
1173 maintainer = "John Doe"
1174 labels = ["label1", "label2"]
1175
1176 [supported_platforms]
1177 linux = true
1178 windows = false
1179
1180 [dependencies]
1181 foo = "1.0"
1182 bar = ">=2.0"
1183
1184 [build_dependencies]
1185 baz = "1.0"
1186
1187 [external_dependencies.foo]
1188 header = "foo.h"
1189
1190 [external_dependencies.bar]
1191 library = "libbar.so"
1192
1193 [test_dependencies]
1194 busted = "69.420"
1195
1196 [source]
1197 url = "https://example.com"
1198 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1199 file = "my-package-1.0.0.tar.gz"
1200 dir = "my-package-1.0.0"
1201
1202 [test]
1203 type = "command"
1204 script = "test.lua"
1205 flags = [ "foo", "bar" ]
1206
1207 [run]
1208 command = "my-command"
1209 args = ["--foo", "--bar"]
1210
1211 [build]
1212 type = "builtin"
1213 "#;
1214
1215 let project =
1216 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1217 let _ = project.into_remote(None).unwrap();
1218 }
1219
1220 #[test]
1221 fn compare_project_toml_with_rockspec() {
1222 let project_toml = r#"
1223 package = "my-package"
1224 version = "1.0.0"
1225 lua = "5.1"
1226
1227 # For testing, specify a custom rockspec format
1228 # (defaults to 3.0)
1229 rockspec_format = "1.0"
1230
1231 [description]
1232 summary = "A summary"
1233 detailed = "A detailed description"
1234 license = "MIT"
1235 homepage = "https://example.com"
1236 issues_url = "https://example.com/issues"
1237 maintainer = "John Doe"
1238 labels = ["label1", "label2"]
1239
1240 [supported_platforms]
1241 linux = true
1242 windows = false
1243
1244 [dependencies]
1245 foo = "1.0"
1246 bar = ">=2.0"
1247
1248 [build_dependencies]
1249 baz = "1.0"
1250
1251 [external_dependencies.foo]
1252 header = "foo.h"
1253
1254 [external_dependencies.bar]
1255 library = "libbar.so"
1256
1257 [test_dependencies]
1258 busted = "1.0"
1259
1260 [source]
1261 url = "https://example.com"
1262 file = "my-package-1.0.0.tar.gz"
1263 dir = "my-package-1.0.0"
1264
1265 [test]
1266 type = "command"
1267 script = "test.lua"
1268 flags = [ "foo", "bar" ]
1269
1270 [run]
1271 command = "my-command"
1272 args = ["--foo", "--bar"]
1273
1274 [deploy]
1275 wrap_bin_scripts = false
1276
1277 [build]
1278 type = "builtin"
1279
1280 [build.install.lua]
1281 "foo.bar" = "src/bar.lua"
1282
1283 [build.install.lib]
1284 "foo.baz" = "src/baz.c"
1285
1286 [build.install.bin]
1287 "bla" = "src/bla"
1288
1289 [build.install.conf]
1290 "cfg.conf" = "resources/config.conf"
1291 "#;
1292
1293 let expected_rockspec = r#"
1294 rockspec_format = "1.0"
1295 package = "my-package"
1296 version = "1.0.0"
1297
1298 source = {
1299 url = "https://example.com",
1300 file = "my-package-1.0.0.tar.gz",
1301 dir = "my-package-1.0.0",
1302 }
1303
1304 description = {
1305 summary = "A summary",
1306 detailed = "A detailed description",
1307 license = "MIT",
1308 homepage = "https://example.com",
1309 issues_url = "https://example.com/issues",
1310 maintainer = "John Doe",
1311 labels = {"label1", "label2"},
1312 }
1313
1314 supported_platforms = {"linux", "!windows"}
1315
1316 dependencies = {
1317 "lua ==5.1",
1318 "foo ==1.0",
1319 "bar >=2.0",
1320 }
1321
1322 build_dependencies = {
1323 "baz ==1.0",
1324 }
1325
1326 external_dependencies = {
1327 foo = { header = "foo.h" },
1328 bar = { library = "libbar.so" },
1329 }
1330
1331 source = {
1332 url = "https://example.com",
1333 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1334 file = "my-package-1.0.0.tar.gz",
1335 dir = "my-package-1.0.0",
1336 }
1337
1338 test = {
1339 type = "command",
1340 script = "test.lua",
1341 flags = {"foo", "bar"},
1342 }
1343
1344 deploy = {
1345 wrap_bin_scripts = false,
1346 }
1347
1348 build = {
1349 type = "builtin",
1350 install = {
1351 lua = {
1352 ["foo.bar"] = "src/bar.lua",
1353 },
1354 lib = {
1355 ["foo.baz"] = "src/baz.c",
1356 },
1357 bin = {
1358 bla = "src/bla",
1359 },
1360 conf = {
1361 ["cfg.conf"] = "resources/config.conf",
1362 },
1363 },
1364 }
1365 "#;
1366
1367 let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1368
1369 let project_toml =
1370 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1371 let rockspec = project_toml
1372 .into_remote(None)
1373 .unwrap()
1374 .to_lua_rockspec()
1375 .unwrap();
1376
1377 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1378 let mut v = v.current_platform().clone();
1379 v.sort_by(|a, b| a.name().cmp(b.name()));
1380 v
1381 };
1382
1383 assert_eq!(rockspec.package(), expected_rockspec.package());
1384 assert_eq!(rockspec.version(), expected_rockspec.version());
1385 assert_eq!(rockspec.description(), expected_rockspec.description());
1386 assert_eq!(
1387 rockspec.supported_platforms(),
1388 expected_rockspec.supported_platforms()
1389 );
1390 assert_eq!(
1391 sorted_package_reqs(rockspec.dependencies()),
1392 sorted_package_reqs(expected_rockspec.dependencies())
1393 );
1394 assert_eq!(
1395 sorted_package_reqs(rockspec.build_dependencies()),
1396 sorted_package_reqs(expected_rockspec.build_dependencies())
1397 );
1398 assert_eq!(
1399 rockspec.external_dependencies(),
1400 expected_rockspec.external_dependencies()
1401 );
1402 assert_eq!(rockspec.source(), expected_rockspec.source());
1403 assert_eq!(rockspec.build(), expected_rockspec.build());
1404 assert_eq!(rockspec.format(), expected_rockspec.format());
1405 }
1406
1407 #[test]
1408 fn merge_project_toml_with_partial_rockspec() {
1409 let project_toml = r#"
1410 package = "my-package"
1411 version = "1.0.0"
1412 lua = "5.1"
1413
1414 # For testing, specify a custom rockspec format
1415 # (defaults to 3.0)
1416 rockspec_format = "1.0"
1417
1418 [description]
1419 summary = "A summary"
1420 detailed = "A detailed description"
1421 license = "MIT"
1422 homepage = "https://example.com"
1423 issues_url = "https://example.com/issues"
1424 maintainer = "John Doe"
1425 labels = ["label1", "label2"]
1426
1427 [supported_platforms]
1428 linux = true
1429 windows = false
1430
1431 [dependencies]
1432 foo = "1.0"
1433 bar = ">=2.0"
1434
1435 [build_dependencies]
1436 baz = "1.0"
1437
1438 [external_dependencies.foo]
1439 header = "foo.h"
1440
1441 [external_dependencies.bar]
1442 library = "libbar.so"
1443
1444 [test_dependencies]
1445 busted = "1.0"
1446
1447 [source]
1448 url = "https://example.com"
1449 file = "my-package-1.0.0.tar.gz"
1450 dir = "my-package-1.0.0"
1451
1452 [test]
1453 type = "command"
1454 script = "test.lua"
1455 flags = [ "foo", "bar" ]
1456
1457 [run]
1458 command = "my-command"
1459 args = [ "--foo", "--bar" ]
1460
1461 [build]
1462 type = "builtin"
1463 "#;
1464
1465 let mergable_rockspec_content = r#"
1466 rockspec_format = "1.0"
1467 package = "my-package-overwritten"
1468
1469 description = {
1470 summary = "A summary overwritten",
1471 detailed = "A detailed description overwritten",
1472 license = "GPL-2.0",
1473 homepage = "https://example.com/overwritten",
1474 issues_url = "https://example.com/issues/overwritten",
1475 maintainer = "John Doe Overwritten",
1476 labels = {"over", "written"},
1477 }
1478
1479 -- Inverted supported platforms
1480 supported_platforms = {"!linux", "windows"}
1481
1482 dependencies = {
1483 "lua 5.1",
1484 "foo >1.0",
1485 "bar <=2.0",
1486 }
1487
1488 build_dependencies = {
1489 "baz >1.0",
1490 }
1491
1492 external_dependencies = {
1493 foo = { header = "overwritten.h" },
1494 bar = { library = "overwritten.so" },
1495 }
1496
1497 test = {
1498 type = "command",
1499 script = "overwritten.lua",
1500 flags = {"over", "written"},
1501 }
1502
1503 build = {
1504 type = "builtin",
1505 }
1506 "#;
1507
1508 let remote_rockspec_content = format!(
1509 r#"{}
1510 version = "1.0.0"
1511 source = {{
1512 url = "https://example.com",
1513 file = "my-package-1.0.0.tar.gz",
1514 dir = "my-package-1.0.0",
1515 }}
1516 "#,
1517 mergable_rockspec_content
1518 );
1519
1520 let project_toml =
1521 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1522 let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1523 let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1524
1525 let merged = project_toml
1526 .merge(partial_rockspec)
1527 .into_remote(None)
1528 .unwrap();
1529
1530 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1531 let mut v = v.current_platform().clone();
1532 v.sort_by(|a, b| a.name().cmp(b.name()));
1533 v
1534 };
1535
1536 assert_eq!(merged.package(), expected_rockspec.package());
1537 assert_eq!(merged.version(), expected_rockspec.version());
1538 assert_eq!(merged.description(), expected_rockspec.description());
1539 assert_eq!(
1540 merged.supported_platforms(),
1541 expected_rockspec.supported_platforms()
1542 );
1543 assert_eq!(
1544 sorted_package_reqs(merged.dependencies()),
1545 sorted_package_reqs(expected_rockspec.dependencies())
1546 );
1547 assert_eq!(
1548 sorted_package_reqs(merged.build_dependencies()),
1549 sorted_package_reqs(expected_rockspec.build_dependencies())
1550 );
1551 assert_eq!(
1552 merged.external_dependencies(),
1553 expected_rockspec.external_dependencies()
1554 );
1555 assert_eq!(merged.source(), expected_rockspec.source());
1556 assert_eq!(merged.build(), expected_rockspec.build());
1557 assert_eq!(merged.format(), expected_rockspec.format());
1558 assert!(merged.local.run().is_some());
1560 }
1561
1562 #[test]
1563 fn project_toml_with_lua_in_dependencies() {
1564 let project_toml = r#"
1565 package = "my-package"
1566 version = "1.0.0"
1567 # lua = ">5.1"
1568
1569 [dependencies]
1570 lua = "5.1" # disallowed
1571
1572 [build]
1573 type = "builtin"
1574 "#;
1575
1576 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default())
1577 .unwrap()
1578 .into_local()
1579 .unwrap_err();
1580 }
1581
1582 #[test]
1583 fn project_toml_with_invalid_run_command() {
1584 for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1585 let project_toml = format!(
1586 r#"
1587 package = "my-package"
1588 version = "1.0.0"
1589 lua = "5.1"
1590
1591 [build]
1592 type = "builtin"
1593
1594 [run]
1595 command = "{command}"
1596 "#,
1597 );
1598
1599 PartialProjectToml::new(PROJECT_TOML, &project_toml, ProjectRoot::default())
1600 .unwrap_err();
1601 }
1602 }
1603
1604 #[test]
1605 fn project_root_dir_override_and_rockspec_generation() {
1606 let project_toml = r#"
1607 package = "my-package"
1608 version = "1.0.0"
1609 lua = "5.1"
1610
1611 [project]
1612 root_dir = ".."
1613
1614 [source]
1615 url = "https://example.com"
1616
1617 [build]
1618 type = "builtin"
1619 "#;
1620
1621 let workspace_root = assert_fs::TempDir::new().unwrap();
1622 let lux_toml_dir = workspace_root.child("proj");
1623 lux_toml_dir.create_dir_all().unwrap();
1624 std::fs::write(lux_toml_dir.join(PROJECT_TOML), project_toml).unwrap();
1625
1626 let project = PartialProjectToml::new(
1627 PROJECT_TOML,
1628 project_toml,
1629 ProjectRoot(lux_toml_dir.to_path_buf()),
1630 )
1631 .unwrap();
1632 let local = project.into_local().unwrap();
1633 let RockSourceSpec::File(root_dir) = &local.source().current_platform().source_spec else {
1634 panic!("expected a file source");
1635 };
1636 assert_eq!(
1637 root_dir,
1638 &std::fs::canonicalize(workspace_root.path()).unwrap()
1639 );
1640
1641 let rockspec = local.to_lua_rockspec().unwrap();
1643 assert!(!rockspec
1644 .to_lua_remote_rockspec_string()
1645 .unwrap()
1646 .contains("root_dir"));
1647 }
1648
1649 #[test]
1650 fn generate_non_deterministic_git_source() {
1651 let rockspec_content = r#"
1652 package = "test-package"
1653 version = "1.0.0"
1654 lua = ">=5.1"
1655
1656 [source]
1657 url = "git+https://exaple.com/repo.git"
1658
1659 [build]
1660 type = "builtin"
1661 "#;
1662
1663 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1664 .unwrap()
1665 .into_remote(None)
1666 .unwrap_err();
1667 }
1668
1669 #[test]
1670 fn generate_deterministic_git_source() {
1671 let rockspec_content = r#"
1672 package = "test-package"
1673 version = "1.0.0"
1674 lua = ">=5.1"
1675
1676 [source]
1677 url = "git+https://exaple.com/owner/repo.git"
1678 tag = "v0.1.0"
1679
1680 [build]
1681 type = "builtin"
1682 "#;
1683
1684 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1685 .unwrap()
1686 .into_remote(None)
1687 .unwrap();
1688 }
1689
1690 #[test]
1691 fn generate_git_source_with_branch() {
1692 let rockspec_content = r#"
1693 package = "test-package"
1694 version = "1.0.0"
1695 lua = ">=5.1"
1696
1697 [source]
1698 url = "git+https://example.com/owner/repo.git"
1699 branch = "main"
1700
1701 [build]
1702 type = "builtin"
1703 "#;
1704
1705 let toml = PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1706 .unwrap()
1707 .into_remote(None)
1708 .unwrap();
1709
1710 let lua = toml.to_lua_remote_rockspec_string().unwrap();
1711 assert!(lua.contains("branch = \"main\""));
1712 assert!(!lua.contains("tag = "));
1713 }
1714
1715 fn init_git_repo(path: &std::path::Path) -> Repository {
1716 let repo = Repository::init(path).unwrap();
1717 {
1718 let tree_id = repo.index().unwrap().write_tree().unwrap();
1719 let tree = repo.find_tree(tree_id).unwrap();
1720 let sig = git2::Signature::now("name", "email").unwrap();
1721 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
1722 .unwrap();
1723 }
1724 repo
1725 }
1726
1727 fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
1728 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1729 .join("resources/test/sample-projects/source-template/");
1730 temp_dir.copy_from(&sample_project, &["**"]).unwrap();
1731 init_git_repo(temp_dir.path())
1732 }
1733
1734 fn create_tag(repo: &Repository, name: &str) {
1735 let sig = git2::Signature::now("name", "email").unwrap();
1736 let id = repo.head().unwrap().target().unwrap();
1737 let obj = repo.find_object(id, None).unwrap();
1738 repo.tag(name, &obj, &sig, "msg", true).unwrap();
1739 }
1740
1741 #[test]
1742 fn test_git_project_generate_dev_source() {
1743 let project_root = assert_fs::TempDir::new().unwrap();
1744 init_sample_project_repo(&project_root);
1745 let project = Project::from_exact(&project_root).unwrap().unwrap();
1746 let remote_project_toml = project.toml().into_remote(None).unwrap();
1747 let source = remote_project_toml.source.current_platform();
1748 let source_spec = &source.source_spec;
1749 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1750 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
1751 let expected_url: RemoteGitUrl =
1752 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1753 assert_eq!(url, &expected_url);
1754 assert!(git_ref.is_some());
1755 }
1756 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1757 }
1758
1759 #[test]
1760 fn test_git_project_generate_non_semver_tag_source() {
1761 let project_root = assert_fs::TempDir::new().unwrap();
1762 let repo = init_sample_project_repo(&project_root);
1763 let tag_name = "bla";
1764 create_tag(&repo, tag_name);
1765 let project = Project::from_exact(&project_root).unwrap().unwrap();
1766 let remote_project_toml = project.toml().into_remote(None).unwrap();
1767 let source = remote_project_toml.source.current_platform();
1768 let source_spec = &source.source_spec;
1769 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1770 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
1771 let expected_url: RemoteGitUrl =
1772 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1773 assert_eq!(url, &expected_url);
1774 assert_eq!(git_ref, &Some(GitRef::Tag(tag_name.to_string())));
1775 }
1776 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1777 }
1778
1779 #[test]
1780 fn test_git_project_generate_release_source_tag_with_v_prefix() {
1781 let project_root = assert_fs::TempDir::new().unwrap();
1782 let repo = init_sample_project_repo(&project_root);
1783 let tag_name = "v1.0.0";
1784 create_tag(&repo, "bla");
1785 create_tag(&repo, tag_name);
1786 let project = Project::from_exact(&project_root).unwrap().unwrap();
1787 let remote_project_toml = project.toml().into_remote(None).unwrap();
1788 let source = remote_project_toml.source.current_platform();
1789 let source_spec = &source.source_spec;
1790 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1791 if let RockSourceSpec::Url(url) = source_spec {
1792 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
1793 .parse()
1794 .unwrap();
1795 assert_eq!(url, &expected_url);
1796 }
1797 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1798 }
1799
1800 #[test]
1801 fn test_git_project_generate_release_source_tag_without_v_prefix() {
1802 let project_root = assert_fs::TempDir::new().unwrap();
1803 let repo = init_sample_project_repo(&project_root);
1804 create_tag(&repo, "bla");
1805 let tag_name = "1.0.0";
1806 create_tag(&repo, tag_name);
1807 let project = Project::from_exact(&project_root).unwrap().unwrap();
1808 let remote_project_toml = project.toml().into_remote(None).unwrap();
1809 let source = remote_project_toml.source.current_platform();
1810 let source_spec = &source.source_spec;
1811 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1812 if let RockSourceSpec::Url(url) = source_spec {
1813 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1814 .parse()
1815 .unwrap();
1816 assert_eq!(url, &expected_url);
1817 }
1818 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1819 }
1820
1821 #[test]
1822 fn test_git_project_in_subdirectory() {
1823 let temp_dir = assert_fs::TempDir::new().unwrap();
1824 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1825 .join("resources/test/sample-projects/source-template/");
1826 let project_dir = temp_dir.child("lux");
1827 project_dir.create_dir_all().unwrap();
1828 project_dir.copy_from(&sample_project, &["**"]).unwrap();
1829 let repo = init_git_repo(temp_dir.path());
1830 create_tag(&repo, "bla");
1831 let tag_name = "1.0.0";
1832 create_tag(&repo, tag_name);
1833 let project = Project::from_exact(&project_dir).unwrap().unwrap();
1834 let remote_project_toml = project.toml().into_remote(None).unwrap();
1835 let source = remote_project_toml.source.current_platform();
1836 let source_spec = &source.source_spec;
1837 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1838 if let RockSourceSpec::Url(url) = source_spec {
1839 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1840 .parse()
1841 .unwrap();
1842 assert_eq!(url, &expected_url);
1843 }
1844 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1845 }
1846
1847 #[test]
1848 fn test_git_project_in_subdirectory_generate_dev_source() {
1849 let temp_dir = assert_fs::TempDir::new().unwrap();
1850 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1851 .join("resources/test/sample-projects/source-template/");
1852 let project_dir = temp_dir.child("lux");
1853 project_dir.create_dir_all().unwrap();
1854 project_dir.copy_from(&sample_project, &["**"]).unwrap();
1855 init_git_repo(temp_dir.path());
1856
1857 let project = Project::from_exact(&project_dir).unwrap().unwrap();
1858 let remote_project_toml = project.toml().into_remote(None).unwrap();
1859 let RockSourceSpec::Git(GitSource { git_ref, .. }) =
1860 &remote_project_toml.source.current_platform().source_spec
1861 else {
1862 panic!("expected a git source");
1863 };
1864 assert!(git_ref.is_some());
1865 }
1866}