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