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)]
846pub struct RemoteProjectToml {
847 local: LocalProjectToml,
848 source: PerPlatform<RemoteRockSource>,
849}
850
851impl RemoteProjectToml {
852 pub fn to_lua_rockspec(&self) -> Result<RemoteLuaRockspec, LuaRockspecError> {
853 RemoteLuaRockspec::new(&self.to_lua_remote_rockspec_string()?)
854 }
855}
856
857impl Rockspec for RemoteProjectToml {
858 type Error = ProjectTomlError;
859
860 fn package(&self) -> &PackageName {
861 self.local.package()
862 }
863
864 fn version(&self) -> &PackageVersion {
865 self.local.version()
866 }
867
868 fn description(&self) -> &RockDescription {
869 self.local.description()
870 }
871
872 fn supported_platforms(&self) -> &PlatformSupport {
873 self.local.supported_platforms()
874 }
875
876 fn lua(&self) -> &PackageVersionReq {
877 self.local.lua()
878 }
879
880 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
881 self.local.dependencies()
882 }
883
884 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
885 self.local.build_dependencies()
886 }
887
888 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
889 self.local.external_dependencies()
890 }
891
892 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
893 self.local.test_dependencies()
894 }
895
896 fn build(&self) -> &PerPlatform<BuildSpec> {
897 self.local.build()
898 }
899
900 fn test(&self) -> &PerPlatform<TestSpec> {
901 self.local.test()
902 }
903
904 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
905 self.local.build_mut()
906 }
907
908 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
909 self.local.test_mut()
910 }
911
912 fn format(&self) -> &Option<RockspecFormat> {
913 self.local.format()
914 }
915
916 fn source(&self) -> &PerPlatform<RemoteRockSource> {
917 &self.source
918 }
919
920 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
921 &mut self.source
922 }
923
924 fn deploy(&self) -> &PerPlatform<DeploySpec> {
925 self.local.deploy()
926 }
927
928 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
929 self.local.deploy_mut()
930 }
931
932 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
933 let project_root = &self.local.internal.project_root;
934 let starter = format!(
935 r#"
936rockspec_format = "{}"
937package = "{}"
938version = "{}""#,
939 self.local
940 .rockspec_format
941 .as_ref()
942 .unwrap_or(&RockspecFormat::default()),
943 self.local.package,
944 self.version()
945 );
946
947 let mut template = Vec::new();
948
949 if self.local.description != RockDescription::default() {
950 template.push(self.local.description.display_lua());
951 }
952
953 if self.local.supported_platforms != PlatformSupport::default() {
954 template.push(self.local.supported_platforms.display_lua());
955 }
956
957 {
958 let mut dependencies = self.local.internal.dependencies.clone().unwrap_or_default();
959 dependencies.insert(
960 0,
961 PackageReq {
962 name: "lua".into(),
963 version_req: self.local.lua.clone(),
964 }
965 .into(),
966 );
967 template.push(Dependencies(&dependencies).display_lua());
968 }
969
970 let mut build_dependencies = self
971 .local
972 .internal
973 .build_dependencies
974 .as_ref()
975 .cloned()
976 .unwrap_or_default();
977
978 let build_backend_dependency = self
979 .local
980 .internal
981 .build
982 .build_type
983 .as_ref()
984 .and_then(|build_type| build_type.luarocks_build_backend());
985
986 if let Some(build_backend_dependency) = build_backend_dependency {
987 build_dependencies.push(build_backend_dependency);
988 }
989
990 if !build_dependencies.is_empty() {
991 template.push(BuildDependencies(&build_dependencies).display_lua());
992 }
993
994 match self.local.internal.external_dependencies {
995 Some(ref external_dependencies) if !external_dependencies.is_empty() => {
996 template.push(ExternalDependencies(external_dependencies).display_lua());
997 }
998 _ => {}
999 }
1000
1001 let source = self.local.internal.source_template.try_generate(
1002 project_root,
1003 &self.local.internal.package,
1004 self.version(),
1005 )?;
1006 template.push(source.display_lua());
1007
1008 if let Some(ref deploy) = self.local.internal.deploy {
1009 template.push(deploy.display_lua());
1010 }
1011
1012 template.push(self.local.internal.build.display_lua());
1013
1014 let unformatted_code = std::iter::once(starter)
1015 .chain(template.into_iter().map(|kv| kv.to_string()))
1016 .join("\n\n");
1017 let result = match stylua_lib::format_code(
1018 &unformatted_code,
1019 stylua_lib::Config::default(),
1020 None,
1021 stylua_lib::OutputVerification::Full,
1022 ) {
1023 Ok(formatted_code) => formatted_code,
1024 Err(_) => unformatted_code,
1025 };
1026 validate_generated_lua(&result)?;
1027 Ok(result)
1028 }
1029}
1030
1031impl HasIntegrity for RemoteProjectToml {
1032 fn hash(&self) -> io::Result<Integrity> {
1033 self.to_lua_rockspec()
1034 .map_err(|err| {
1035 io::Error::other(format!(
1036 "unable to convert remote project to Lua rockspec:\n{}",
1037 err
1038 ))
1039 })?
1040 .hash()
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use std::path::PathBuf;
1047
1048 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
1049 use git2::{Repository, RepositoryInitOptions};
1050 use url::Url;
1051
1052 use crate::{
1053 git::{url::RemoteGitUrl, GitSource},
1054 lua_rockspec::{PartialLuaRockspec, PerPlatform, RemoteLuaRockspec, RockSourceSpec},
1055 project::{Project, ProjectRoot},
1056 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
1057 };
1058
1059 use super::PartialProjectToml;
1060
1061 #[test]
1062 fn project_toml_parsing() {
1063 let project_toml = r#"
1064 package = "my-package"
1065 version = "1.0.0"
1066 lua = "5.3"
1067
1068 rockspec_format = "1.0"
1069
1070 [source]
1071 url = "https://example.com"
1072
1073 [dependencies]
1074 foo = "1.0"
1075 bar = ">=2.0"
1076
1077 [run]
1078 args = ["--foo", "--bar"]
1079
1080 [build]
1081 type = "builtin"
1082 "#;
1083
1084 let project = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1085 let _ = project.into_remote(None).unwrap();
1086
1087 let project_toml = r#"
1088 package = "my-package"
1089 version = "1.0.0"
1090 lua = "5.1"
1091
1092 [description]
1093 summary = "A summary"
1094 detailed = "A detailed description"
1095 license = "MIT"
1096 homepage = "https://example.com"
1097 issues_url = "https://example.com/issues"
1098 maintainer = "John Doe"
1099 labels = ["label1", "label2"]
1100
1101 [supported_platforms]
1102 linux = true
1103 windows = false
1104
1105 [dependencies]
1106 foo = "1.0"
1107 bar = ">=2.0"
1108
1109 [build_dependencies]
1110 baz = "1.0"
1111
1112 [external_dependencies.foo]
1113 header = "foo.h"
1114
1115 [external_dependencies.bar]
1116 library = "libbar.so"
1117
1118 [test_dependencies]
1119 busted = "69.420"
1120
1121 [source]
1122 url = "https://example.com"
1123 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU="
1124 file = "my-package-1.0.0.tar.gz"
1125 dir = "my-package-1.0.0"
1126
1127 [test]
1128 type = "command"
1129 script = "test.lua"
1130 flags = [ "foo", "bar" ]
1131
1132 [run]
1133 command = "my-command"
1134 args = ["--foo", "--bar"]
1135
1136 [build]
1137 type = "builtin"
1138 "#;
1139
1140 let project = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1141 let _ = project.into_remote(None).unwrap();
1142 }
1143
1144 #[test]
1145 fn compare_project_toml_with_rockspec() {
1146 let project_toml = r#"
1147 package = "my-package"
1148 version = "1.0.0"
1149 lua = "5.1"
1150
1151 # For testing, specify a custom rockspec format
1152 # (defaults to 3.0)
1153 rockspec_format = "1.0"
1154
1155 [description]
1156 summary = "A summary"
1157 detailed = "A detailed description"
1158 license = "MIT"
1159 homepage = "https://example.com"
1160 issues_url = "https://example.com/issues"
1161 maintainer = "John Doe"
1162 labels = ["label1", "label2"]
1163
1164 [supported_platforms]
1165 linux = true
1166 windows = false
1167
1168 [dependencies]
1169 foo = "1.0"
1170 bar = ">=2.0"
1171
1172 [build_dependencies]
1173 baz = "1.0"
1174
1175 [external_dependencies.foo]
1176 header = "foo.h"
1177
1178 [external_dependencies.bar]
1179 library = "libbar.so"
1180
1181 [test_dependencies]
1182 busted = "1.0"
1183
1184 [source]
1185 url = "https://example.com"
1186 file = "my-package-1.0.0.tar.gz"
1187 dir = "my-package-1.0.0"
1188
1189 [test]
1190 type = "command"
1191 script = "test.lua"
1192 flags = [ "foo", "bar" ]
1193
1194 [run]
1195 command = "my-command"
1196 args = ["--foo", "--bar"]
1197
1198 [deploy]
1199 wrap_bin_scripts = false
1200
1201 [build]
1202 type = "builtin"
1203
1204 [build.install.lua]
1205 "foo.bar" = "src/bar.lua"
1206
1207 [build.install.lib]
1208 "foo.baz" = "src/baz.c"
1209
1210 [build.install.bin]
1211 "bla" = "src/bla"
1212
1213 [build.install.conf]
1214 "cfg.conf" = "resources/config.conf"
1215 "#;
1216
1217 let expected_rockspec = r#"
1218 rockspec_format = "1.0"
1219 package = "my-package"
1220 version = "1.0.0"
1221
1222 source = {
1223 url = "https://example.com",
1224 file = "my-package-1.0.0.tar.gz",
1225 dir = "my-package-1.0.0",
1226 }
1227
1228 description = {
1229 summary = "A summary",
1230 detailed = "A detailed description",
1231 license = "MIT",
1232 homepage = "https://example.com",
1233 issues_url = "https://example.com/issues",
1234 maintainer = "John Doe",
1235 labels = {"label1", "label2"},
1236 }
1237
1238 supported_platforms = {"linux", "!windows"}
1239
1240 dependencies = {
1241 "lua ==5.1",
1242 "foo ==1.0",
1243 "bar >=2.0",
1244 }
1245
1246 build_dependencies = {
1247 "baz ==1.0",
1248 }
1249
1250 external_dependencies = {
1251 foo = { header = "foo.h" },
1252 bar = { library = "libbar.so" },
1253 }
1254
1255 source = {
1256 url = "https://example.com",
1257 hash = "sha256-di00mD8txN7rjaVpvxzNbnQsAh6H16zUtJZapH7U4HU=",
1258 file = "my-package-1.0.0.tar.gz",
1259 dir = "my-package-1.0.0",
1260 }
1261
1262 test = {
1263 type = "command",
1264 script = "test.lua",
1265 flags = {"foo", "bar"},
1266 }
1267
1268 deploy = {
1269 wrap_bin_scripts = false,
1270 }
1271
1272 build = {
1273 type = "builtin",
1274 install = {
1275 lua = {
1276 ["foo.bar"] = "src/bar.lua",
1277 },
1278 lib = {
1279 ["foo.baz"] = "src/baz.c",
1280 },
1281 bin = {
1282 bla = "src/bla",
1283 },
1284 conf = {
1285 ["cfg.conf"] = "resources/config.conf",
1286 },
1287 },
1288 }
1289 "#;
1290
1291 let expected_rockspec = RemoteLuaRockspec::new(expected_rockspec).unwrap();
1292
1293 let project_toml = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1294 let rockspec = project_toml
1295 .into_remote(None)
1296 .unwrap()
1297 .to_lua_rockspec()
1298 .unwrap();
1299
1300 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1301 let mut v = v.current_platform().clone();
1302 v.sort_by(|a, b| a.name().cmp(b.name()));
1303 v
1304 };
1305
1306 assert_eq!(rockspec.package(), expected_rockspec.package());
1307 assert_eq!(rockspec.version(), expected_rockspec.version());
1308 assert_eq!(rockspec.description(), expected_rockspec.description());
1309 assert_eq!(
1310 rockspec.supported_platforms(),
1311 expected_rockspec.supported_platforms()
1312 );
1313 assert_eq!(
1314 sorted_package_reqs(rockspec.dependencies()),
1315 sorted_package_reqs(expected_rockspec.dependencies())
1316 );
1317 assert_eq!(
1318 sorted_package_reqs(rockspec.build_dependencies()),
1319 sorted_package_reqs(expected_rockspec.build_dependencies())
1320 );
1321 assert_eq!(
1322 rockspec.external_dependencies(),
1323 expected_rockspec.external_dependencies()
1324 );
1325 assert_eq!(rockspec.source(), expected_rockspec.source());
1326 assert_eq!(rockspec.build(), expected_rockspec.build());
1327 assert_eq!(rockspec.format(), expected_rockspec.format());
1328 }
1329
1330 #[test]
1331 fn merge_project_toml_with_partial_rockspec() {
1332 let project_toml = r#"
1333 package = "my-package"
1334 version = "1.0.0"
1335 lua = "5.1"
1336
1337 # For testing, specify a custom rockspec format
1338 # (defaults to 3.0)
1339 rockspec_format = "1.0"
1340
1341 [description]
1342 summary = "A summary"
1343 detailed = "A detailed description"
1344 license = "MIT"
1345 homepage = "https://example.com"
1346 issues_url = "https://example.com/issues"
1347 maintainer = "John Doe"
1348 labels = ["label1", "label2"]
1349
1350 [supported_platforms]
1351 linux = true
1352 windows = false
1353
1354 [dependencies]
1355 foo = "1.0"
1356 bar = ">=2.0"
1357
1358 [build_dependencies]
1359 baz = "1.0"
1360
1361 [external_dependencies.foo]
1362 header = "foo.h"
1363
1364 [external_dependencies.bar]
1365 library = "libbar.so"
1366
1367 [test_dependencies]
1368 busted = "1.0"
1369
1370 [source]
1371 url = "https://example.com"
1372 file = "my-package-1.0.0.tar.gz"
1373 dir = "my-package-1.0.0"
1374
1375 [test]
1376 type = "command"
1377 script = "test.lua"
1378 flags = [ "foo", "bar" ]
1379
1380 [run]
1381 command = "my-command"
1382 args = [ "--foo", "--bar" ]
1383
1384 [build]
1385 type = "builtin"
1386 "#;
1387
1388 let mergable_rockspec_content = r#"
1389 rockspec_format = "1.0"
1390 package = "my-package-overwritten"
1391
1392 description = {
1393 summary = "A summary overwritten",
1394 detailed = "A detailed description overwritten",
1395 license = "GPL-2.0",
1396 homepage = "https://example.com/overwritten",
1397 issues_url = "https://example.com/issues/overwritten",
1398 maintainer = "John Doe Overwritten",
1399 labels = {"over", "written"},
1400 }
1401
1402 -- Inverted supported platforms
1403 supported_platforms = {"!linux", "windows"}
1404
1405 dependencies = {
1406 "lua 5.1",
1407 "foo >1.0",
1408 "bar <=2.0",
1409 }
1410
1411 build_dependencies = {
1412 "baz >1.0",
1413 }
1414
1415 external_dependencies = {
1416 foo = { header = "overwritten.h" },
1417 bar = { library = "overwritten.so" },
1418 }
1419
1420 test = {
1421 type = "command",
1422 script = "overwritten.lua",
1423 flags = {"over", "written"},
1424 }
1425
1426 build = {
1427 type = "builtin",
1428 }
1429 "#;
1430
1431 let remote_rockspec_content = format!(
1432 r#"{}
1433 version = "1.0.0"
1434 source = {{
1435 url = "https://example.com",
1436 file = "my-package-1.0.0.tar.gz",
1437 dir = "my-package-1.0.0",
1438 }}
1439 "#,
1440 &mergable_rockspec_content
1441 );
1442
1443 let project_toml = PartialProjectToml::new(project_toml, ProjectRoot::default()).unwrap();
1444 let partial_rockspec = PartialLuaRockspec::new(mergable_rockspec_content).unwrap();
1445 let expected_rockspec = RemoteLuaRockspec::new(&remote_rockspec_content).unwrap();
1446
1447 let merged = project_toml
1448 .merge(partial_rockspec)
1449 .into_remote(None)
1450 .unwrap();
1451
1452 let sorted_package_reqs = |v: &PerPlatform<Vec<LuaDependencySpec>>| {
1453 let mut v = v.current_platform().clone();
1454 v.sort_by(|a, b| a.name().cmp(b.name()));
1455 v
1456 };
1457
1458 assert_eq!(merged.package(), expected_rockspec.package());
1459 assert_eq!(merged.version(), expected_rockspec.version());
1460 assert_eq!(merged.description(), expected_rockspec.description());
1461 assert_eq!(
1462 merged.supported_platforms(),
1463 expected_rockspec.supported_platforms()
1464 );
1465 assert_eq!(
1466 sorted_package_reqs(merged.dependencies()),
1467 sorted_package_reqs(expected_rockspec.dependencies())
1468 );
1469 assert_eq!(
1470 sorted_package_reqs(merged.build_dependencies()),
1471 sorted_package_reqs(expected_rockspec.build_dependencies())
1472 );
1473 assert_eq!(
1474 merged.external_dependencies(),
1475 expected_rockspec.external_dependencies()
1476 );
1477 assert_eq!(merged.source(), expected_rockspec.source());
1478 assert_eq!(merged.build(), expected_rockspec.build());
1479 assert_eq!(merged.format(), expected_rockspec.format());
1480 assert!(merged.local.run().is_some());
1482 }
1483
1484 #[test]
1485 fn project_toml_with_lua_in_dependencies() {
1486 let project_toml = r#"
1487 package = "my-package"
1488 version = "1.0.0"
1489 # lua = ">5.1"
1490
1491 [dependencies]
1492 lua = "5.1" # disallowed
1493
1494 [build]
1495 type = "builtin"
1496 "#;
1497
1498 PartialProjectToml::new(project_toml, ProjectRoot::default())
1499 .unwrap()
1500 .into_local()
1501 .unwrap_err();
1502 }
1503
1504 #[test]
1505 fn project_toml_with_invalid_run_command() {
1506 for command in ["lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4", "luajit"] {
1507 let project_toml = format!(
1508 r#"
1509 package = "my-package"
1510 version = "1.0.0"
1511 lua = "5.1"
1512
1513 [build]
1514 type = "builtin"
1515
1516 [run]
1517 command = "{command}"
1518 "#,
1519 );
1520
1521 PartialProjectToml::new(&project_toml, ProjectRoot::default()).unwrap_err();
1522 }
1523 }
1524
1525 #[test]
1526 fn generate_non_deterministic_git_source() {
1527 let rockspec_content = r#"
1528 package = "test-package"
1529 version = "1.0.0"
1530 lua = ">=5.1"
1531
1532 [source]
1533 url = "git+https://exaple.com/repo.git"
1534
1535 [build]
1536 type = "builtin"
1537 "#;
1538
1539 PartialProjectToml::new(rockspec_content, ProjectRoot::default())
1540 .unwrap()
1541 .into_remote(None)
1542 .unwrap_err();
1543 }
1544
1545 #[test]
1546 fn generate_deterministic_git_source() {
1547 let rockspec_content = r#"
1548 package = "test-package"
1549 version = "1.0.0"
1550 lua = ">=5.1"
1551
1552 [source]
1553 url = "git+https://exaple.com/owner/repo.git"
1554 tag = "v0.1.0"
1555
1556 [build]
1557 type = "builtin"
1558 "#;
1559
1560 PartialProjectToml::new(rockspec_content, ProjectRoot::default())
1561 .unwrap()
1562 .into_remote(None)
1563 .unwrap();
1564 }
1565
1566 fn init_sample_project_repo(temp_dir: &assert_fs::TempDir) -> Repository {
1567 let sample_project: PathBuf = "resources/test/sample-projects/source-template/".into();
1568 temp_dir.copy_from(&sample_project, &["**"]).unwrap();
1569 let repo = Repository::init(temp_dir).unwrap();
1570 let mut opts = RepositoryInitOptions::new();
1571 opts.initial_head("main");
1572 {
1573 let mut config = repo.config().unwrap();
1574 config.set_str("user.name", "name").unwrap();
1575 config.set_str("user.email", "email").unwrap();
1576 let mut index = repo.index().unwrap();
1577 let id = index.write_tree().unwrap();
1578
1579 let tree = repo.find_tree(id).unwrap();
1580 let sig = repo.signature().unwrap();
1581 repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
1582 .unwrap();
1583 }
1584 repo
1585 }
1586
1587 fn create_tag(repo: &Repository, name: &str) {
1588 let sig = repo.signature().unwrap();
1589 let id = repo.head().unwrap().target().unwrap();
1590 let obj = repo.find_object(id, None).unwrap();
1591 repo.tag(name, &obj, &sig, "msg", true).unwrap();
1592 }
1593
1594 #[test]
1595 fn test_git_project_generate_dev_source() {
1596 let project_root = assert_fs::TempDir::new().unwrap();
1597 init_sample_project_repo(&project_root);
1598 let project = Project::from_exact(&project_root).unwrap().unwrap();
1599 let remote_project_toml = project.toml().into_remote(None).unwrap();
1600 let source = remote_project_toml.source.current_platform();
1601 let source_spec = &source.source_spec;
1602 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1603 if let RockSourceSpec::Git(GitSource { url, checkout_ref }) = source_spec {
1604 let expected_url: RemoteGitUrl =
1605 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1606 assert_eq!(url, &expected_url);
1607 assert!(checkout_ref.is_some());
1608 }
1609 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1610 }
1611
1612 #[test]
1613 fn test_git_project_generate_non_semver_tag_source() {
1614 let project_root = assert_fs::TempDir::new().unwrap();
1615 let repo = init_sample_project_repo(&project_root);
1616 let tag_name = "bla";
1617 create_tag(&repo, tag_name);
1618 let project = Project::from_exact(&project_root).unwrap().unwrap();
1619 let remote_project_toml = project.toml().into_remote(None).unwrap();
1620 let source = remote_project_toml.source.current_platform();
1621 let source_spec = &source.source_spec;
1622 assert!(matches!(source_spec, &RockSourceSpec::Git { .. }));
1623 if let RockSourceSpec::Git(GitSource { url, checkout_ref }) = source_spec {
1624 let expected_url: RemoteGitUrl =
1625 "https://github.com/lumen-oss/lux.git".parse().unwrap();
1626 assert_eq!(url, &expected_url);
1627 assert_eq!(checkout_ref, &Some(tag_name.to_string()));
1628 }
1629 assert_eq!(source.unpack_dir, Some("lux-dev".into()));
1630 }
1631
1632 #[test]
1633 fn test_git_project_generate_release_source_tag_with_v_prefix() {
1634 let project_root = assert_fs::TempDir::new().unwrap();
1635 let repo = init_sample_project_repo(&project_root);
1636 let tag_name = "v1.0.0";
1637 create_tag(&repo, "bla");
1638 create_tag(&repo, tag_name);
1639 let project = Project::from_exact(&project_root).unwrap().unwrap();
1640 let remote_project_toml = project.toml().into_remote(None).unwrap();
1641 let source = remote_project_toml.source.current_platform();
1642 let source_spec = &source.source_spec;
1643 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1644 if let RockSourceSpec::Url(url) = source_spec {
1645 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/v1.0.0.zip"
1646 .parse()
1647 .unwrap();
1648 assert_eq!(url, &expected_url);
1649 }
1650 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1651 }
1652
1653 #[test]
1654 fn test_git_project_generate_release_source_tag_without_v_prefix() {
1655 let project_root = assert_fs::TempDir::new().unwrap();
1656 let repo = init_sample_project_repo(&project_root);
1657 create_tag(&repo, "bla");
1658 let tag_name = "1.0.0";
1659 create_tag(&repo, tag_name);
1660 let project = Project::from_exact(&project_root).unwrap().unwrap();
1661 let remote_project_toml = project.toml().into_remote(None).unwrap();
1662 let source = remote_project_toml.source.current_platform();
1663 let source_spec = &source.source_spec;
1664 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1665 if let RockSourceSpec::Url(url) = source_spec {
1666 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1667 .parse()
1668 .unwrap();
1669 assert_eq!(url, &expected_url);
1670 }
1671 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1672 }
1673
1674 #[test]
1675 fn test_git_project_in_subdirectory() {
1676 let temp_dir = assert_fs::TempDir::new().unwrap();
1677 let sample_project: PathBuf = "resources/test/sample-projects/source-template/".into();
1678 let project_dir = temp_dir.child("lux");
1679 project_dir.create_dir_all().unwrap();
1680 project_dir.copy_from(&sample_project, &["**"]).unwrap();
1681 let repo = Repository::init(&temp_dir).unwrap();
1682 let mut opts = RepositoryInitOptions::new();
1683 opts.initial_head("main");
1684 {
1685 let mut config = repo.config().unwrap();
1686 config.set_str("user.name", "name").unwrap();
1687 config.set_str("user.email", "email").unwrap();
1688 let mut index = repo.index().unwrap();
1689 let id = index.write_tree().unwrap();
1690
1691 let tree = repo.find_tree(id).unwrap();
1692 let sig = repo.signature().unwrap();
1693 repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
1694 .unwrap();
1695 }
1696 create_tag(&repo, "bla");
1697 let tag_name = "1.0.0";
1698 create_tag(&repo, tag_name);
1699 let project = Project::from_exact(&project_dir).unwrap().unwrap();
1700 let remote_project_toml = project.toml().into_remote(None).unwrap();
1701 let source = remote_project_toml.source.current_platform();
1702 let source_spec = &source.source_spec;
1703 assert!(matches!(source_spec, &RockSourceSpec::Url { .. }));
1704 if let RockSourceSpec::Url(url) = source_spec {
1705 let expected_url: Url = "https://github.com/lumen-oss/lux/archive/refs/tags/1.0.0.zip"
1706 .parse()
1707 .unwrap();
1708 assert_eq!(url, &expected_url);
1709 }
1710 assert_eq!(source.unpack_dir, Some("lux-1.0.0".into()));
1711 }
1712}