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