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