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