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