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, DisplayLuaKV,
29 ExternalDependencies, ExternalDependencySpec, LuaVersionError, PartialLuaRockspec,
30 PerPlatform, PlatformIdentifier, PlatformSupport, PlatformValidationError,
31 RemoteRockSource, RockDescription, RockSourceError, RockspecFormat, TestSpec,
32 TestSpecDecodeError, 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 pub fn to_local_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
716 Ok(LocalLuaRockspec::from_project_toml(self).into_remote_with_source(self.source.clone()))
717 }
718
719 fn render_rockspec(
720 &self,
721 version: &PackageVersion,
722 source: Option<DisplayLuaKV>,
723 ) -> Result<String, ProjectTomlError> {
724 let starter = format!(
725 r#"
726rockspec_format = "{}"
727package = "{}"
728version = "{}""#,
729 self.rockspec_format
730 .as_ref()
731 .unwrap_or(&RockspecFormat::default()),
732 self.package,
733 version
734 );
735
736 let mut template = Vec::new();
737
738 if self.description != RockDescription::default() {
739 template.push(self.description.display_lua());
740 }
741
742 if self.supported_platforms != PlatformSupport::default() {
743 template.push(self.supported_platforms.display_lua());
744 }
745
746 {
747 let mut dependencies = self.internal.dependencies.clone().unwrap_or_default();
748 dependencies.insert(
749 0,
750 PackageReq {
751 name: "lua".into(),
752 version_req: self.lua.clone(),
753 }
754 .into(),
755 );
756 template.push(Dependencies(&dependencies).display_lua());
757 }
758
759 let mut build_dependencies = self
760 .internal
761 .build_dependencies
762 .as_ref()
763 .cloned()
764 .unwrap_or_default();
765
766 let build_backend_dependency = self
767 .internal
768 .build
769 .build_type
770 .as_ref()
771 .and_then(|build_type| build_type.luarocks_build_backend());
772
773 if let Some(build_backend_dependency) = build_backend_dependency {
774 build_dependencies.push(build_backend_dependency);
775 }
776
777 if !build_dependencies.is_empty() {
778 template.push(BuildDependencies(&build_dependencies).display_lua());
779 }
780
781 match self.internal.external_dependencies {
782 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
783 template.push(ExternalDependencies(external_dependencies).display_lua());
784 }
785 _ => {}
786 }
787
788 if let Some(source) = source {
789 template.push(source);
790 }
791
792 template.push(self.internal.build.display_lua());
793
794 let unformatted_code = std::iter::once(starter)
795 .chain(template.into_iter().map(|kv| kv.to_string()))
796 .join("\n\n");
797 let result = match stylua_lib::format_code(
798 &unformatted_code,
799 stylua_lib::Config::default(),
800 None,
801 stylua_lib::OutputVerification::Full,
802 ) {
803 Ok(formatted_code) => formatted_code,
804 Err(_) => unformatted_code,
805 };
806 validate_generated_lua(&result)?;
807 Ok(result)
808 }
809}
810
811impl Rockspec for LocalProjectToml {
812 type Error = ProjectTomlError;
813
814 fn package(&self) -> &PackageName {
815 &self.package
816 }
817
818 fn version(&self) -> &PackageVersion {
819 &self.version
820 }
821
822 fn description(&self) -> &RockDescription {
823 &self.description
824 }
825
826 fn supported_platforms(&self) -> &PlatformSupport {
827 &self.supported_platforms
828 }
829
830 fn lua(&self) -> &PackageVersionReq {
831 &self.lua
832 }
833
834 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
835 &self.dependencies
836 }
837
838 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
839 &self.build_dependencies
840 }
841
842 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
843 &self.external_dependencies
844 }
845
846 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
847 &self.test_dependencies
848 }
849
850 fn build(&self) -> &PerPlatform<BuildSpec> {
851 &self.build
852 }
853
854 fn test(&self) -> &PerPlatform<TestSpec> {
855 &self.test
856 }
857
858 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
859 &mut self.build
860 }
861
862 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
863 &mut self.test
864 }
865
866 fn format(&self) -> &Option<RockspecFormat> {
867 &self.rockspec_format
868 }
869
870 fn source(&self) -> &PerPlatform<RemoteRockSource> {
871 &self.source
872 }
873
874 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
875 &mut self.source
876 }
877
878 fn deploy(&self) -> &PerPlatform<DeploySpec> {
879 &self.deploy
880 }
881
882 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
883 &mut self.deploy
884 }
885
886 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
887 let lux_toml_dir = &self.internal.lux_toml_dir;
888 let version = self
889 .internal
890 .version_template
891 .try_generate(lux_toml_dir, None)?;
892 let source =
893 self.internal
894 .source_template
895 .try_generate(lux_toml_dir, &self.package, &version)?;
896 self.render_rockspec(&version, Some(source.display_lua()))
897 }
898}
899
900fn validate_generated_lua(lua_str: &str) -> Result<(), ProjectTomlError> {
901 let mut lua = ottavino::Lua::core();
902 lua.try_enter(|ctx| {
903 let closure = ottavino::Closure::load(ctx, None, lua_str.as_bytes())?;
904 let executor = ottavino::Executor::start(ctx, closure.into(), ());
905 if !executor.step(ctx, &mut ottavino::Fuel::with(ROCKSPEC_FUEL_LIMIT))? {
906 return Ok(Err(ProjectTomlError::FuelLimitExceeded));
907 }
908 Ok(Ok(()))
909 })
910 .map_err(|err| ProjectTomlError::GeneratedInvalidLua(err.to_string()))?
911}
912
913#[derive(Error, Debug, Diagnostic)]
914#[non_exhaustive]
915#[error(transparent)]
916pub enum ProjectTomlIntegrityError {
917 LuaRockspecError(#[from] LuaRockspecError),
918 IoError(#[from] io::Error),
919 #[error(transparent)]
920 #[diagnostic(transparent)]
921 Fs(#[from] fs::FsError),
922}
923
924impl HasIntegrity for LocalProjectToml {
925 async fn hash(&self) -> io::Result<Integrity> {
926 match self.to_lua_rockspec() {
927 Ok(lua_rockspec) => lua_rockspec.hash().await,
928 Err(_) => self.internal.hash().await,
929 }
930 }
931}
932
933#[derive(Debug)]
935pub struct RemoteProjectToml {
936 local: LocalProjectToml,
937 source: PerPlatform<RemoteRockSource>,
938}
939
940impl RemoteProjectToml {
941 pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
942 RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
943 }
944}
945
946impl Rockspec for RemoteProjectToml {
947 type Error = ProjectTomlError;
948
949 fn package(&self) -> &PackageName {
950 self.local.package()
951 }
952
953 fn version(&self) -> &PackageVersion {
954 self.local.version()
955 }
956
957 fn description(&self) -> &RockDescription {
958 self.local.description()
959 }
960
961 fn supported_platforms(&self) -> &PlatformSupport {
962 self.local.supported_platforms()
963 }
964
965 fn lua(&self) -> &PackageVersionReq {
966 self.local.lua()
967 }
968
969 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
970 self.local.dependencies()
971 }
972
973 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
974 self.local.build_dependencies()
975 }
976
977 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
978 self.local.external_dependencies()
979 }
980
981 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
982 self.local.test_dependencies()
983 }
984
985 fn build(&self) -> &PerPlatform<BuildSpec> {
986 self.local.build()
987 }
988
989 fn test(&self) -> &PerPlatform<TestSpec> {
990 self.local.test()
991 }
992
993 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
994 self.local.build_mut()
995 }
996
997 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
998 self.local.test_mut()
999 }
1000
1001 fn format(&self) -> &Option<RockspecFormat> {
1002 self.local.format()
1003 }
1004
1005 fn source(&self) -> &PerPlatform<RemoteRockSource> {
1006 &self.source
1007 }
1008
1009 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
1010 &mut self.source
1011 }
1012
1013 fn deploy(&self) -> &PerPlatform<DeploySpec> {
1014 self.local.deploy()
1015 }
1016
1017 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
1018 self.local.deploy_mut()
1019 }
1020
1021 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
1022 let lux_toml_dir = &self.local.internal.lux_toml_dir;
1023 let starter = format!(
1024 r#"
1025rockspec_format = "{}"
1026package = "{}"
1027version = "{}""#,
1028 self.local
1029 .rockspec_format
1030 .as_ref()
1031 .unwrap_or(&RockspecFormat::default()),
1032 self.local.package,
1033 self.version()
1034 );
1035
1036 let mut template = Vec::new();
1037
1038 if self.local.description != RockDescription::default() {
1039 template.push(self.local.description.display_lua());
1040 }
1041
1042 if self.local.supported_platforms != PlatformSupport::default() {
1043 template.push(self.local.supported_platforms.display_lua());
1044 }
1045
1046 {
1047 let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
1048 dependencies.insert(
1049 0,
1050 PackageReq {
1051 name: "lua".into(),
1052 version_req: self.local.lua.clone(),
1053 }
1054 .into(),
1055 );
1056 template.push(Dependencies(&dependencies).display_lua());
1057 }
1058
1059 let mut build_dependencies = self
1060 .local
1061 .internal
1062 .build_dependencies
1063 .as_ref()
1064 .cloned()
1065 .unwrap_or_default();
1066
1067 let build_backend_dependency = self
1068 .local
1069 .internal
1070 .build
1071 .build_type
1072 .as_ref()
1073 .and_then(|build_type| build_type.luarocks_build_backend());
1074
1075 if let Some(build_backend_dependency) = build_backend_dependency {
1076 build_dependencies.push(build_backend_dependency);
1077 }
1078
1079 if !build_dependencies.is_empty() {
1080 template.push(BuildDependencies(&build_dependencies).display_lua());
1081 }
1082
1083 match self.local.internal.external_dependencies {
1084 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
1085 template.push(ExternalDependencies(external_dependencies).display_lua());
1086 }
1087 _ => {}
1088 }
1089
1090 let source = self.local.internal.source_template.try_generate(
1091 lux_toml_dir,
1092 &self.local.internal.package,
1093 self.version(),
1094 )?;
1095 template.push(source.display_lua());
1096
1097 if let Some(ref deploy) = self.local.internal.deploy {
1098 template.push(deploy.display_lua());
1099 }
1100
1101 template.push(self.local.internal.build.display_lua());
1102
1103 let unformatted_code = std::iter::once(starter)
1104 .chain(template.into_iter().map(|kv| kv.to_string()))
1105 .join("\n\n");
1106 let result = match stylua_lib::format_code(
1107 &unformatted_code,
1108 stylua_lib::Config::default(),
1109 None,
1110 stylua_lib::OutputVerification::Full,
1111 ) {
1112 Ok(formatted_code) => formatted_code,
1113 Err(_) => unformatted_code,
1114 };
1115 validate_generated_lua(&result)?;
1116 Ok(result)
1117 }
1118}
1119
1120impl HasIntegrity for RemoteProjectToml {
1121 async fn hash(&self) -> io::Result<Integrity> {
1122 self.to_lua_rockspec()
1123 .map_err(|err| {
1124 io::Error::other(format!(
1125 "unable to convert remote project to Lua rockspec:\n{}",
1126 err
1127 ))
1128 })?
1129 .hash()
1130 .await
1131 }
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136 use std::path::PathBuf;
1137
1138 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1139 use git2::Repository;
1140 use url::Url;
1141
1142 use crate::{
1143 git::{url::RemoteGitUrl, GitRef, GitSource},
1144 lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1145 project::{Project, ProjectRoot, PROJECT_TOML},
1146 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1147 };
1148
1149 use super::PartialProjectToml;
1150
1151 #[test]
1152 fn project_toml_parsing() {
1153 let project_toml = r#"
1154 package = "my-package"
1155 version = "1.0.0"
1156 lua = "5.3"
1157
1158 rockspec_format = "1.0"
1159
1160 [source]
1161 url = "https://example.com"
1162
1163 [dependencies]
1164 foo = "1.0"
1165 bar = ">=2.0"
1166
1167 [run]
1168 args = ["--foo", "--bar"]
1169
1170 [build]
1171 type = "builtin"
1172 "#;
1173
1174 let project =
1175 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1176 let _ = project.into_remote(None).unwrap();
1177
1178 let project_toml = r#"
1179 package = "my-package"
1180 version = "1.0.0"
1181 lua = "5.1"
1182
1183 [description]
1184 summary = "A summary"
1185 detailed = "A detailed description"
1186 license = "MIT"
1187 homepage = "https://example.com"
1188 issues_url = "https://example.com/issues"
1189 maintainer = "John Doe"
1190 labels = ["label1", "label2"]
1191
1192 [supported_platforms]
1193 linux = true
1194 windows = false
1195
1196 [dependencies]
1197 foo = "1.0"
1198 bar = ">=2.0"
1199
1200 [build_dependencies]
1201 baz = "1.0"
1202
1203 [external_dependencies.foo]
1204 header = "foo.h"
1205
1206 [external_dependencies.bar]
1207 library = "libbar.so"
1208
1209 [test_dependencies]
1210 busted = "69.420"
1211
1212 [source]
1213 url = "https://example.com"
1214 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1215 file = "my-package-1.0.0.tar.gz"
1216 dir = "my-package-1.0.0"
1217
1218 [test]
1219 type = "command"
1220 script = "test.lua"
1221 flags = [ "foo", "bar" ]
1222
1223 [run]
1224 command = "my-command"
1225 args = ["--foo", "--bar"]
1226
1227 [build]
1228 type = "builtin"
1229 "#;
1230
1231 let project =
1232 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1233 let _ = project.into_remote(None).unwrap();
1234 }
1235
1236 #[test]
1237 fn compare_project_toml_with_rockspec() {
1238 let project_toml = r#"
1239 package = "my-package"
1240 version = "1.0.0"
1241 lua = "5.1"
1242
1243 # For testing, specify a custom rockspec format
1244 # (defaults to 3.0)
1245 rockspec_format = "1.0"
1246
1247 [description]
1248 summary = "A summary"
1249 detailed = "A detailed description"
1250 license = "MIT"
1251 homepage = "https://example.com"
1252 issues_url = "https://example.com/issues"
1253 maintainer = "John Doe"
1254 labels = ["label1", "label2"]
1255
1256 [supported_platforms]
1257 linux = true
1258 windows = false
1259
1260 [dependencies]
1261 foo = "1.0"
1262 bar = ">=2.0"
1263
1264 [build_dependencies]
1265 baz = "1.0"
1266
1267 [external_dependencies.foo]
1268 header = "foo.h"
1269
1270 [external_dependencies.bar]
1271 library = "libbar.so"
1272
1273 [test_dependencies]
1274 busted = "1.0"
1275
1276 [source]
1277 url = "https://example.com"
1278 file = "my-package-1.0.0.tar.gz"
1279 dir = "my-package-1.0.0"
1280
1281 [test]
1282 type = "command"
1283 script = "test.lua"
1284 flags = [ "foo", "bar" ]
1285
1286 [run]
1287 command = "my-command"
1288 args = ["--foo", "--bar"]
1289
1290 [deploy]
1291 wrap_bin_scripts = false
1292
1293 [build]
1294 type = "builtin"
1295
1296 [build.install.lua]
1297 "foo.bar" = "src/bar.lua"
1298
1299 [build.install.lib]
1300 "foo.baz" = "src/baz.c"
1301
1302 [build.install.bin]
1303 "bla" = "src/bla"
1304
1305 [build.install.conf]
1306 "cfg.conf" = "resources/config.conf"
1307 "#;
1308
1309 let expected_rockspec = r#"
1310 rockspec_format = "1.0"
1311 package = "my-package"
1312 version = "1.0.0"
1313
1314 source = {
1315 url = "https://example.com",
1316 file = "my-package-1.0.0.tar.gz",
1317 dir = "my-package-1.0.0",
1318 }
1319
1320 description = {
1321 summary = "A summary",
1322 detailed = "A detailed description",
1323 license = "MIT",
1324 homepage = "https://example.com",
1325 issues_url = "https://example.com/issues",
1326 maintainer = "John Doe",
1327 labels = {"label1", "label2"},
1328 }
1329
1330 supported_platforms = {"linux", "!windows"}
1331
1332 dependencies = {
1333 "lua ==5.1",
1334 "foo ==1.0",
1335 "bar >=2.0",
1336 }
1337
1338 build_dependencies = {
1339 "baz ==1.0",
1340 }
1341
1342 external_dependencies = {
1343 foo = { header = "foo.h" },
1344 bar = { library = "libbar.so" },
1345 }
1346
1347 source = {
1348 url = "https://example.com",
1349 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1350 file = "my-package-1.0.0.tar.gz",
1351 dir = "my-package-1.0.0",
1352 }
1353
1354 test = {
1355 type = "command",
1356 script = "test.lua",
1357 flags = {"foo", "bar"},
1358 }
1359
1360 deploy = {
1361 wrap_bin_scripts = false,
1362 }
1363
1364 build = {
1365 type = "builtin",
1366 install = {
1367 lua = {
1368 ["foo.bar"] = "src/bar.lua",
1369 },
1370 lib = {
1371 ["foo.baz"] = "src/baz.c",
1372 },
1373 bin = {
1374 bla = "src/bla",
1375 },
1376 conf = {
1377 ["cfg.conf"] = "resources/config.conf",
1378 },
1379 },
1380 }
1381 "#;
1382
1383 let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1384
1385 let project_toml =
1386 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1387 let rockspec = project_toml
1388 .into_remote(None)
1389 .unwrap()
1390 .to_lua_rockspec()
1391 .unwrap();
1392
1393 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1394 let mut v = v.current_platform().clone();
1395 v.sort_by(|a, b| a.name().cmp(b.name()));
1396 v
1397 };
1398
1399 assert_eq!(rockspec.package(), expected_rockspec.package());
1400 assert_eq!(rockspec.version(), expected_rockspec.version());
1401 assert_eq!(rockspec.description(), expected_rockspec.description());
1402 assert_eq!(
1403 rockspec.supported_platforms(),
1404 expected_rockspec.supported_platforms()
1405 );
1406 assert_eq!(
1407 sorted_package_reqs(rockspec.dependencies()),
1408 sorted_package_reqs(expected_rockspec.dependencies())
1409 );
1410 assert_eq!(
1411 sorted_package_reqs(rockspec.build_dependencies()),
1412 sorted_package_reqs(expected_rockspec.build_dependencies())
1413 );
1414 assert_eq!(
1415 rockspec.external_dependencies(),
1416 expected_rockspec.external_dependencies()
1417 );
1418 assert_eq!(rockspec.source(), expected_rockspec.source());
1419 assert_eq!(rockspec.build(), expected_rockspec.build());
1420 assert_eq!(rockspec.format(), expected_rockspec.format());
1421 }
1422
1423 #[test]
1424 fn merge_project_toml_with_partial_rockspec() {
1425 let project_toml = r#"
1426 package = "my-package"
1427 version = "1.0.0"
1428 lua = "5.1"
1429
1430 # For testing, specify a custom rockspec format
1431 # (defaults to 3.0)
1432 rockspec_format = "1.0"
1433
1434 [description]
1435 summary = "A summary"
1436 detailed = "A detailed description"
1437 license = "MIT"
1438 homepage = "https://example.com"
1439 issues_url = "https://example.com/issues"
1440 maintainer = "John Doe"
1441 labels = ["label1", "label2"]
1442
1443 [supported_platforms]
1444 linux = true
1445 windows = false
1446
1447 [dependencies]
1448 foo = "1.0"
1449 bar = ">=2.0"
1450
1451 [build_dependencies]
1452 baz = "1.0"
1453
1454 [external_dependencies.foo]
1455 header = "foo.h"
1456
1457 [external_dependencies.bar]
1458 library = "libbar.so"
1459
1460 [test_dependencies]
1461 busted = "1.0"
1462
1463 [source]
1464 url = "https://example.com"
1465 file = "my-package-1.0.0.tar.gz"
1466 dir = "my-package-1.0.0"
1467
1468 [test]
1469 type = "command"
1470 script = "test.lua"
1471 flags = [ "foo", "bar" ]
1472
1473 [run]
1474 command = "my-command"
1475 args = [ "--foo", "--bar" ]
1476
1477 [build]
1478 type = "builtin"
1479 "#;
1480
1481 let mergable_rockspec_content = r#"
1482 rockspec_format = "1.0"
1483 package = "my-package-overwritten"
1484
1485 description = {
1486 summary = "A summary overwritten",
1487 detailed = "A detailed description overwritten",
1488 license = "GPL-2.0",
1489 homepage = "https://example.com/overwritten",
1490 issues_url = "https://example.com/issues/overwritten",
1491 maintainer = "John Doe Overwritten",
1492 labels = {"over", "written"},
1493 }
1494
1495 -- Inverted supported platforms
1496 supported_platforms = {"!linux", "windows"}
1497
1498 dependencies = {
1499 "lua 5.1",
1500 "foo >1.0",
1501 "bar <=2.0",
1502 }
1503
1504 build_dependencies = {
1505 "baz >1.0",
1506 }
1507
1508 external_dependencies = {
1509 foo = { header = "overwritten.h" },
1510 bar = { library = "overwritten.so" },
1511 }
1512
1513 test = {
1514 type = "command",
1515 script = "overwritten.lua",
1516 flags = {"over", "written"},
1517 }
1518
1519 build = {
1520 type = "builtin",
1521 }
1522 "#;
1523
1524 let remote_rockspec_content = format!(
1525 r#"{}
1526 version = "1.0.0"
1527 source = {{
1528 url = "https://example.com",
1529 file = "my-package-1.0.0.tar.gz",
1530 dir = "my-package-1.0.0",
1531 }}
1532 "#,
1533 mergable_rockspec_content
1534 );
1535
1536 let project_toml =
1537 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default()).unwrap();
1538 let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1539 let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1540
1541 let merged = project_toml
1542 .merge(partial_rockspec)
1543 .into_remote(None)
1544 .unwrap();
1545
1546 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1547 let mut v = v.current_platform().clone();
1548 v.sort_by(|a, b| a.name().cmp(b.name()));
1549 v
1550 };
1551
1552 assert_eq!(merged.package(), expected_rockspec.package());
1553 assert_eq!(merged.version(), expected_rockspec.version());
1554 assert_eq!(merged.description(), expected_rockspec.description());
1555 assert_eq!(
1556 merged.supported_platforms(),
1557 expected_rockspec.supported_platforms()
1558 );
1559 assert_eq!(
1560 sorted_package_reqs(merged.dependencies()),
1561 sorted_package_reqs(expected_rockspec.dependencies())
1562 );
1563 assert_eq!(
1564 sorted_package_reqs(merged.build_dependencies()),
1565 sorted_package_reqs(expected_rockspec.build_dependencies())
1566 );
1567 assert_eq!(
1568 merged.external_dependencies(),
1569 expected_rockspec.external_dependencies()
1570 );
1571 assert_eq!(merged.source(), expected_rockspec.source());
1572 assert_eq!(merged.build(), expected_rockspec.build());
1573 assert_eq!(merged.format(), expected_rockspec.format());
1574 assert!(merged.local.run().is_some());
1576 }
1577
1578 #[test]
1579 fn project_toml_with_lua_in_dependencies() {
1580 let project_toml = r#"
1581 package = "my-package"
1582 version = "1.0.0"
1583 # lua = ">5.1"
1584
1585 [dependencies]
1586 lua = "5.1" # disallowed
1587
1588 [build]
1589 type = "builtin"
1590 "#;
1591
1592 PartialProjectToml::new(PROJECT_TOML, project_toml, ProjectRoot::default())
1593 .unwrap()
1594 .into_local()
1595 .unwrap_err();
1596 }
1597
1598 #[test]
1599 fn project_toml_with_invalid_run_command() {
1600 for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1601 let project_toml = format!(
1602 r#"
1603 package = "my-package"
1604 version = "1.0.0"
1605 lua = "5.1"
1606
1607 [build]
1608 type = "builtin"
1609
1610 [run]
1611 command = "{command}"
1612 "#,
1613 );
1614
1615 PartialProjectToml::new(PROJECT_TOML, &project_toml, ProjectRoot::default())
1616 .unwrap_err();
1617 }
1618 }
1619
1620 #[test]
1621 fn project_root_dir_override_and_rockspec_generation() {
1622 let project_toml = r#"
1623 package = "my-package"
1624 version = "1.0.0"
1625 lua = "5.1"
1626
1627 [project]
1628 root_dir = ".."
1629
1630 [source]
1631 url = "https://example.com"
1632
1633 [build]
1634 type = "builtin"
1635 "#;
1636
1637 let workspace_root = assert_fs::TempDir::new().unwrap();
1638 let lux_toml_dir = workspace_root.child("proj");
1639 lux_toml_dir.create_dir_all().unwrap();
1640 std::fs::write(lux_toml_dir.join(PROJECT_TOML), project_toml).unwrap();
1641
1642 let project = PartialProjectToml::new(
1643 PROJECT_TOML,
1644 project_toml,
1645 ProjectRoot(lux_toml_dir.to_path_buf()),
1646 )
1647 .unwrap();
1648 let local = project.into_local().unwrap();
1649 let RockSourceSpec::File(root_dir) = &local.source().current_platform().source_spec else {
1650 panic!("expected a file source");
1651 };
1652 assert_eq!(
1653 root_dir,
1654 &std::fs::canonicalize(workspace_root.path()).unwrap()
1655 );
1656
1657 let rockspec = local.to_lua_rockspec().unwrap();
1659 assert!(!rockspec
1660 .to_lua_remote_rockspec_string()
1661 .unwrap()
1662 .contains("root_dir"));
1663 }
1664
1665 #[test]
1666 fn generate_non_deterministic_git_source() {
1667 let rockspec_content = r#"
1668 package = "test-package"
1669 version = "1.0.0"
1670 lua = ">=5.1"
1671
1672 [source]
1673 url = "git+https://exaple.com/repo.git"
1674
1675 [build]
1676 type = "builtin"
1677 "#;
1678
1679 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1680 .unwrap()
1681 .into_remote(None)
1682 .unwrap_err();
1683 }
1684
1685 #[test]
1686 fn generate_deterministic_git_source() {
1687 let rockspec_content = r#"
1688 package = "test-package"
1689 version = "1.0.0"
1690 lua = ">=5.1"
1691
1692 [source]
1693 url = "git+https://exaple.com/owner/repo.git"
1694 tag = "v0.1.0"
1695
1696 [build]
1697 type = "builtin"
1698 "#;
1699
1700 PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1701 .unwrap()
1702 .into_remote(None)
1703 .unwrap();
1704 }
1705
1706 #[test]
1707 fn generate_git_source_with_branch() {
1708 let rockspec_content = r#"
1709 package = "test-package"
1710 version = "1.0.0"
1711 lua = ">=5.1"
1712
1713 [source]
1714 url = "git+https://example.com/owner/repo.git"
1715 branch = "main"
1716
1717 [build]
1718 type = "builtin"
1719 "#;
1720
1721 let toml = PartialProjectToml::new(PROJECT_TOML, rockspec_content, ProjectRoot::default())
1722 .unwrap()
1723 .into_remote(None)
1724 .unwrap();
1725
1726 let lua = toml.to_lua_remote_rockspec_string().unwrap();
1727 assert!(lua.contains("branch = \"main\""));
1728 assert!(!lua.contains("tag = "));
1729 }
1730
1731 fn init_git_repo(path: &std::path::Path) -> Repository {
1732 let repo = Repository::init(path).unwrap();
1733 {
1734 let tree_id = repo.index().unwrap().write_tree().unwrap();
1735 let tree = repo.find_tree(tree_id).unwrap();
1736 let sig = git2::Signature::now("name", "email").unwrap();
1737 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
1738 .unwrap();
1739 }
1740 repo
1741 }
1742
1743 fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
1744 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1745 .join("resources/test/sample-projects/source-template/");
1746 temp_dir.copy_from(&sample_project, &["**"]).unwrap();
1747 init_git_repo(temp_dir.path())
1748 }
1749
1750 fn create_tag(repo: &Repository, name: &str) {
1751 let sig = git2::Signature::now("name", "email").unwrap();
1752 let id = repo.head().unwrap().target().unwrap();
1753 let obj = repo.find_object(id, None).unwrap();
1754 repo.tag(name, &obj, &sig, "msg", true).unwrap();
1755 }
1756
1757 #[test]
1758 fn test_git_project_generate_dev_source() {
1759 let project_root = assert_fs::TempDir::new().unwrap();
1760 init_sample_project_repo(&project_root);
1761 let project = Project::from_exact(&project_root).unwrap().unwrap();
1762 let remote_project_toml = project.toml().into_remote(None).unwrap();
1763 let source = remote_project_toml.source.current_platform();
1764 let source_spec = &source.source_spec;
1765 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1766 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
1767 let expected_url: RemoteGitUrl =
1768 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1769 assert_eq!(url, &expected_url);
1770 assert!(git_ref.is_some());
1771 }
1772 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1773 }
1774
1775 #[test]
1776 fn test_git_project_generate_non_semver_tag_source() {
1777 let project_root = assert_fs::TempDir::new().unwrap();
1778 let repo = init_sample_project_repo(&project_root);
1779 let tag_name = "bla";
1780 create_tag(&repo, tag_name);
1781 let project = Project::from_exact(&project_root).unwrap().unwrap();
1782 let remote_project_toml = project.toml().into_remote(None).unwrap();
1783 let source = remote_project_toml.source.current_platform();
1784 let source_spec = &source.source_spec;
1785 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1786 if let RockSourceSpec::Git(GitSource { url, git_ref }) = source_spec {
1787 let expected_url: RemoteGitUrl =
1788 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1789 assert_eq!(url, &expected_url);
1790 assert_eq!(git_ref, &Some(GitRef::Tag(tag_name.to_string())));
1791 }
1792 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1793 }
1794
1795 #[test]
1796 fn test_git_project_generate_release_source_tag_with_v_prefix() {
1797 let project_root = assert_fs::TempDir::new().unwrap();
1798 let repo = init_sample_project_repo(&project_root);
1799 let tag_name = "v1.0.0";
1800 create_tag(&repo, "bla");
1801 create_tag(&repo, tag_name);
1802 let project = Project::from_exact(&project_root).unwrap().unwrap();
1803 let remote_project_toml = project.toml().into_remote(None).unwrap();
1804 let source = remote_project_toml.source.current_platform();
1805 let source_spec = &source.source_spec;
1806 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1807 if let RockSourceSpec::Url(url) = source_spec {
1808 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
1809 .parse()
1810 .unwrap();
1811 assert_eq!(url, &expected_url);
1812 }
1813 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1814 }
1815
1816 #[test]
1817 fn test_git_project_generate_release_source_tag_without_v_prefix() {
1818 let project_root = assert_fs::TempDir::new().unwrap();
1819 let repo = init_sample_project_repo(&project_root);
1820 create_tag(&repo, "bla");
1821 let tag_name = "1.0.0";
1822 create_tag(&repo, tag_name);
1823 let project = Project::from_exact(&project_root).unwrap().unwrap();
1824 let remote_project_toml = project.toml().into_remote(None).unwrap();
1825 let source = remote_project_toml.source.current_platform();
1826 let source_spec = &source.source_spec;
1827 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1828 if let RockSourceSpec::Url(url) = source_spec {
1829 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1830 .parse()
1831 .unwrap();
1832 assert_eq!(url, &expected_url);
1833 }
1834 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1835 }
1836
1837 #[test]
1838 fn test_git_project_in_subdirectory() {
1839 let temp_dir = assert_fs::TempDir::new().unwrap();
1840 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1841 .join("resources/test/sample-projects/source-template/");
1842 let project_dir = temp_dir.child("lux");
1843 project_dir.create_dir_all().unwrap();
1844 project_dir.copy_from(&sample_project, &["**"]).unwrap();
1845 let repo = init_git_repo(temp_dir.path());
1846 create_tag(&repo, "bla");
1847 let tag_name = "1.0.0";
1848 create_tag(&repo, tag_name);
1849 let project = Project::from_exact(&project_dir).unwrap().unwrap();
1850 let remote_project_toml = project.toml().into_remote(None).unwrap();
1851 let source = remote_project_toml.source.current_platform();
1852 let source_spec = &source.source_spec;
1853 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1854 if let RockSourceSpec::Url(url) = source_spec {
1855 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1856 .parse()
1857 .unwrap();
1858 assert_eq!(url, &expected_url);
1859 }
1860 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1861 }
1862
1863 #[test]
1864 fn test_git_project_in_subdirectory_generate_dev_source() {
1865 let temp_dir = assert_fs::TempDir::new().unwrap();
1866 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1867 .join("resources/test/sample-projects/source-template/");
1868 let project_dir = temp_dir.child("lux");
1869 project_dir.create_dir_all().unwrap();
1870 project_dir.copy_from(&sample_project, &["**"]).unwrap();
1871 init_git_repo(temp_dir.path());
1872
1873 let project = Project::from_exact(&project_dir).unwrap().unwrap();
1874 let remote_project_toml = project.toml().into_remote(None).unwrap();
1875 let RockSourceSpec::Git(GitSource { git_ref, .. }) =
1876 &remote_project_toml.source.current_platform().source_spec
1877 else {
1878 panic!("expected a git source");
1879 };
1880 assert!(git_ref.is_some());
1881 }
1882}