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