1use itertools::Itertools;
2use miette::{Diagnostic, NamedSource, SourceSpan};
3use path_slash::PathBufExt;
4use project_toml::{
5 LocalProjectTomlValidationError, PartialProjectToml, RemoteProjectTomlValidationError,
6};
7use std::{
8 io,
9 ops::Deref,
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13use thiserror::Error;
14use toml_edit::{DocumentMut, Item};
15
16use crate::{
17 config::Config,
18 git::{
19 self,
20 shorthand::RemoteGitUrlShorthand,
21 url::RemoteGitUrl,
22 utils::{GitError, SemVerTagOrSha},
23 },
24 lua_rockspec::{
25 LocalLuaRockspec, LuaRockspecError, LuaVersionError, PartialLuaRockspec,
26 PartialRockspecError, RemoteLuaRockspec,
27 },
28 lua_version::LuaVersion,
29 package::SpecRev,
30 remote_package_db::RemotePackageDB,
31 rockspec::{
32 lua_dependency::{DependencyType, LuaDependencySpec, LuaDependencyType},
33 LuaVersionCompatibility,
34 },
35};
36use crate::{
37 lockfile::PinnedState,
38 package::{PackageName, PackageReq},
39};
40
41pub(crate) mod gen;
42pub mod project_toml;
43
44pub use project_toml::PROJECT_TOML;
45
46pub const EXTRA_ROCKSPEC: &str = "extra.rockspec";
47
48#[derive(Debug, Error, Diagnostic)]
51#[error("{message}", message = _inner.message())]
52#[diagnostic(
53 code(lux_lib::toml::deserialize),
54 help("check that the file is valid TOML and matches the expected schema")
55)]
56pub struct TomlDeError {
57 #[source]
58 _inner: Box<toml::de::Error>,
59 #[source_code]
60 src: NamedSource<String>,
61 #[label("here")]
62 span: SourceSpan,
63}
64
65impl TomlDeError {
66 pub fn new(name: &str, src: &str, inner: toml::de::Error) -> Self {
67 let src = NamedSource::new(name, src.to_string());
68 let span = inner.span().unwrap_or(0..0).into();
69 Self {
70 _inner: Box::new(inner),
71 src,
72 span,
73 }
74 }
75
76 pub fn inner(&self) -> &toml::de::Error {
77 &self._inner
78 }
79}
80
81pub(crate) fn parse_toml<T>(name: &str, src: &str) -> Result<T, TomlDeError>
82where
83 T: serde::de::DeserializeOwned,
84{
85 toml::from_str(src).map_err(|e| TomlDeError::new(name, src, e))
86}
87
88#[derive(Error, Debug, Diagnostic)]
89#[non_exhaustive]
90#[error(transparent)]
91pub enum ProjectError {
92 #[error("error reading project TOML at {0}:\n{1}")]
93 #[diagnostic(help("ensure the file exists and is readable."))]
94 ReadProjectTOML(String, io::Error),
95 #[error("error creating project root at {0}:\n{1}")]
96 #[diagnostic(help("check that the parent directory exists and you have write permissions."))]
97 CreateProjectRoot(String, io::Error),
98 #[diagnostic(transparent)]
99 Project(#[from] LocalProjectTomlValidationError),
100 #[diagnostic(transparent)]
101 Toml(#[from] TomlDeError),
102 #[error("error when parsing `extra.rockspec`: {0}")]
103 #[diagnostic(forward(0))]
104 Rockspec(#[from] PartialRockspecError),
105}
106
107#[derive(Error, Debug, Diagnostic)]
108#[non_exhaustive]
109#[error(transparent)]
110pub enum IntoLocalRockspecError {
111 LocalProjectTomlValidationError(Box<LocalProjectTomlValidationError>),
112 RockspecError(Box<LuaRockspecError>),
113}
114
115#[derive(Error, Debug, Diagnostic)]
116#[non_exhaustive]
117#[error(transparent)]
118pub enum IntoRemoteRockspecError {
119 RocksTomlValidationError(Box<RemoteProjectTomlValidationError>),
120 RockspecError(Box<LuaRockspecError>),
121}
122
123#[derive(Error, Debug, Diagnostic)]
124#[non_exhaustive]
125pub enum ProjectEditError {
126 #[error(transparent)]
127 Io(#[from] tokio::io::Error),
128 #[error(transparent)]
129 Toml(#[from] toml_edit::TomlError),
130 #[error("error parsing {PROJECT_TOML} after edit. This is probably a bug.")]
131 #[diagnostic(forward(0))]
132 TomlDe(#[from] TomlDeError),
133 #[error(transparent)]
134 #[diagnostic(transparent)]
135 Git(#[from] GitError),
136 #[error("unable to query latest version for {0}")]
137 LatestVersionNotFound(PackageName),
138 #[error("expected field to be a value, but got {0}")]
139 ExpectedValue(Box<toml_edit::Item>),
140 #[error("expected string, but got {0}")]
141 ExpectedString(Box<toml_edit::Value>),
142 #[error(transparent)]
143 #[diagnostic(transparent)]
144 GitUrlShorthandParse(#[from] git::shorthand::ParseError),
145}
146
147#[derive(Error, Debug, Diagnostic)]
148#[non_exhaustive]
149pub enum PinError {
150 #[error("package {0} not found in dependencies")]
151 PackageNotFound(PackageName),
152 #[error("dependency {dep} is already {}pinned!", if *.pin_state == PinnedState::Unpinned { "un" } else { "" })]
153 PinStateUnchanged {
154 pin_state: PinnedState,
155 dep: PackageName,
156 },
157 #[error(transparent)]
158 Toml(#[from] toml_edit::TomlError),
159 #[error("error parsing lux.toml after edit. This is probably a bug.")]
160 #[diagnostic(forward(0))]
161 TomlDe(#[from] TomlDeError),
162 #[error(transparent)]
163 Io(#[from] tokio::io::Error),
164}
165
166#[derive(Clone, Debug)]
169#[cfg_attr(test, derive(Default))]
170pub struct ProjectRoot(PathBuf);
171
172impl ProjectRoot {
173 pub(crate) fn new() -> Self {
174 Self(PathBuf::new())
175 }
176}
177
178impl AsRef<Path> for ProjectRoot {
179 fn as_ref(&self) -> &Path {
180 self.0.as_ref()
181 }
182}
183
184impl Deref for ProjectRoot {
185 type Target = PathBuf;
186
187 fn deref(&self) -> &Self::Target {
188 &self.0
189 }
190}
191
192#[derive(Clone, Debug)]
194pub struct Project {
195 root: ProjectRoot,
197 toml: PartialProjectToml,
199}
200
201impl Project {
202 #[tracing::instrument(level = "trace", skip_all)]
204 pub fn from_exact(start: impl AsRef<Path>) -> Result<Option<Self>, ProjectError> {
205 if !start.as_ref().exists() {
206 return Ok(None);
207 }
208
209 if start.as_ref().join(PROJECT_TOML).exists() {
210 let project_toml_path = start.as_ref().join(PROJECT_TOML);
211 let toml_content = std::fs::read_to_string(&project_toml_path).map_err(|err| {
212 ProjectError::ReadProjectTOML(project_toml_path.to_string_lossy().to_string(), err)
213 })?;
214 let root = start.as_ref();
215
216 let mut project = Project {
217 root: ProjectRoot(root.to_path_buf()),
218 toml: PartialProjectToml::new(
219 &project_toml_path.to_string_lossy(),
220 &toml_content,
221 ProjectRoot(root.to_path_buf()),
222 )?,
223 };
224
225 if let Some(extra_rockspec) = project.extra_rockspec()? {
226 project.toml = project.toml.merge(extra_rockspec);
227 }
228
229 Ok(Some(project))
230 } else {
231 Ok(None)
232 }
233 }
234
235 pub fn toml_path(&self) -> PathBuf {
237 self.root.join(PROJECT_TOML)
238 }
239
240 pub fn extra_rockspec_path(&self) -> PathBuf {
242 self.root.join(EXTRA_ROCKSPEC)
243 }
244
245 pub fn root(&self) -> &ProjectRoot {
246 &self.root
247 }
248
249 pub fn toml(&self) -> &PartialProjectToml {
250 &self.toml
251 }
252
253 pub fn local_rockspec(&self) -> Result<LocalLuaRockspec, IntoLocalRockspecError> {
254 self.toml()
255 .into_local()
256 .map_err(|err| IntoLocalRockspecError::LocalProjectTomlValidationError(Box::new(err)))?
257 .to_lua_rockspec()
258 .map_err(|err| IntoLocalRockspecError::RockspecError(Box::new(err)))
259 }
260
261 pub fn remote_rockspec(
262 &self,
263 specrev: Option<SpecRev>,
264 ) -> Result<RemoteLuaRockspec, IntoRemoteRockspecError> {
265 self.toml()
266 .into_remote(specrev)
267 .map_err(|err| IntoRemoteRockspecError::RocksTomlValidationError(Box::new(err)))?
268 .to_lua_rockspec()
269 .map_err(|err| IntoRemoteRockspecError::RockspecError(Box::new(err)))
270 }
271
272 pub fn extra_rockspec(&self) -> Result<Option<PartialLuaRockspec>, PartialRockspecError> {
273 if self.extra_rockspec_path().exists() {
274 Ok(Some(PartialLuaRockspec::new(&std::fs::read_to_string(
275 self.extra_rockspec_path(),
276 )?)?))
277 } else {
278 Ok(None)
279 }
280 }
281
282 pub fn lua_version(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
283 self.toml().lua_version_matches(config)
284 }
285
286 pub async fn add(
287 &mut self,
288 dependencies: DependencyType<&PackageReq>,
289 package_db: &RemotePackageDB,
290 ) -> Result<(), ProjectEditError> {
291 let mut project_toml =
292 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
293
294 prepare_dependency_tables(&mut project_toml);
295 let table = match dependencies {
296 DependencyType::Regular(_) => &mut project_toml["dependencies"],
297 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
298 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
299 DependencyType::External(_) => &mut project_toml["external_dependencies"],
300 };
301
302 match dependencies {
303 DependencyType::Regular(ref deps)
304 | DependencyType::Build(ref deps)
305 | DependencyType::Test(ref deps) => {
306 for dep in deps {
307 let dep_version_str = if dep.version_req().is_any() {
308 package_db
309 .latest_version(dep.name())
310 .map(|latest_version| latest_version.to_string())
311 .unwrap_or_else(|| dep.version_req().to_string())
312 } else {
313 dep.version_req().to_string()
314 };
315 table[dep.name().to_string()] = toml_edit::value(dep_version_str);
316 }
317 }
318 DependencyType::External(ref deps) => {
319 for (name, dep) in deps {
320 if let Some(path) = &dep.header {
321 table[name]["header"] = toml_edit::value(path.to_slash_lossy().to_string());
322 }
323 if let Some(path) = &dep.library {
324 table[name]["library"] =
325 toml_edit::value(path.to_slash_lossy().to_string());
326 }
327 }
328 }
329 };
330
331 let toml_content = project_toml.to_string();
332 tokio::fs::write(self.toml_path(), &toml_content).await?;
333 self.toml = PartialProjectToml::new(
334 self.toml_path().to_str().unwrap_or("<lux.toml>"),
335 &toml_content,
336 self.root.clone(),
337 )?;
338
339 Ok(())
340 }
341
342 pub async fn add_git(
343 &mut self,
344 dependencies: LuaDependencyType<&RemoteGitUrlShorthand>,
345 ) -> Result<(), ProjectEditError> {
346 let mut project_toml =
347 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
348
349 prepare_dependency_tables(&mut project_toml);
350 let table = match dependencies {
351 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
352 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
353 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
354 };
355
356 match dependencies {
357 LuaDependencyType::Regular(urls)
358 | LuaDependencyType::Build(urls)
359 | LuaDependencyType::Test(urls) => {
360 for url in urls {
361 let git_url: RemoteGitUrl = url.clone().into();
362 let mut dep_entry = toml_edit::table();
363 match git::utils::latest_semver_tag_or_commit_sha(&git_url)? {
364 SemVerTagOrSha::SemVerTag(tag) => {
365 dep_entry["git"] = Item::Value(url.to_string().into());
366 dep_entry["version"] = Item::Value(tag.clone().into());
367 if tag.contains("-") {
368 dep_entry["rev"] = Item::Value(tag.into());
370 }
371 }
372 SemVerTagOrSha::CommitSha(sha) => {
373 dep_entry["git"] = Item::Value(url.to_string().into());
374 dep_entry["version"] = Item::Value(sha.into());
375 }
376 }
377 table[git_url.repo()] = dep_entry;
378 }
379 }
380 }
381
382 let toml_content = project_toml.to_string();
383 tokio::fs::write(self.toml_path(), &toml_content).await?;
384 self.toml = PartialProjectToml::new(
385 self.toml_path().to_str().unwrap_or("<lux.toml>"),
386 &toml_content,
387 self.root.clone(),
388 )?;
389
390 Ok(())
391 }
392
393 pub async fn remove(
394 &mut self,
395 dependencies: DependencyType<&PackageName>,
396 ) -> Result<(), ProjectEditError> {
397 let mut project_toml =
398 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
399
400 prepare_dependency_tables(&mut project_toml);
401 let table = match dependencies {
402 DependencyType::Regular(_) => &mut project_toml["dependencies"],
403 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
404 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
405 DependencyType::External(_) => &mut project_toml["external_dependencies"],
406 };
407
408 match dependencies {
409 DependencyType::Regular(ref deps)
410 | DependencyType::Build(ref deps)
411 | DependencyType::Test(ref deps) => {
412 for dep in deps {
413 table[dep.to_string()] = Item::None;
414 }
415 }
416 DependencyType::External(ref deps) => {
417 for (name, dep) in deps {
418 if dep.header.is_some() {
419 table[name]["header"] = Item::None;
420 }
421 if dep.library.is_some() {
422 table[name]["library"] = Item::None;
423 }
424 }
425 }
426 };
427
428 let toml_content = project_toml.to_string();
429 tokio::fs::write(self.toml_path(), &toml_content).await?;
430 self.toml = PartialProjectToml::new(
431 self.toml_path().to_str().unwrap_or("<lux.toml>"),
432 &toml_content,
433 self.root.clone(),
434 )?;
435
436 Ok(())
437 }
438
439 pub async fn upgrade(
440 &mut self,
441 dependencies: LuaDependencyType<&PackageName>,
442 package_db: &RemotePackageDB,
443 ) -> Result<(), ProjectEditError> {
444 let mut project_toml =
445 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
446
447 prepare_dependency_tables(&mut project_toml);
448 let table = match dependencies {
449 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
450 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
451 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
452 };
453
454 match dependencies {
455 LuaDependencyType::Regular(deps)
456 | LuaDependencyType::Build(deps)
457 | LuaDependencyType::Test(deps) => {
458 let latest_rock_version_str =
459 |dep: &PackageName| -> Result<String, ProjectEditError> {
460 Ok(package_db
461 .latest_version(dep)
462 .ok_or(ProjectEditError::LatestVersionNotFound(dep.clone()))?
463 .to_string())
464 };
465 for dep in deps {
466 let mut dep_item = table[dep.to_string()].clone();
467 match &dep_item {
468 Item::Value(_) => {
469 let dep_version_str = latest_rock_version_str(dep)?;
470 table[dep.to_string()] = toml_edit::value(dep_version_str);
471 }
472 Item::Table(tbl) => {
473 match tbl.get("git") {
474 Some(git_item) => {
475 let git_value =
476 git_item.clone().into_value().map_err(|err| {
477 ProjectEditError::ExpectedValue(Box::new(err))
478 })?;
479 let git_url_str = git_value.as_str().ok_or(
480 ProjectEditError::ExpectedString(Box::new(
481 git_value.clone(),
482 )),
483 )?;
484 let shorthand: RemoteGitUrlShorthand = git_url_str.parse()?;
485 match git::utils::latest_semver_tag_or_commit_sha(
486 &shorthand.into(),
487 )? {
488 SemVerTagOrSha::SemVerTag(latest_tag) => {
489 table[dep.to_string()]["version"] =
490 Item::Value(latest_tag.clone().into());
491 if latest_tag.contains("-") {
492 table[dep.to_string()]["rev"] =
494 Item::Value(latest_tag.into());
495 }
496 }
497 SemVerTagOrSha::CommitSha(latest_sha) => {
498 table[dep.to_string()]["version"] =
499 Item::Value(latest_sha.into());
500 }
501 }
502 table[dep.to_string()] = dep_item;
503 }
504 None => {
505 let dep_version_str = latest_rock_version_str(dep)?;
506 dep_item["version".to_string()] =
507 toml_edit::value(dep_version_str);
508 table[dep.to_string()] = dep_item;
509 }
510 }
511 }
512 _ => {}
513 }
514 }
515 }
516 }
517
518 let toml_content = project_toml.to_string();
519 tokio::fs::write(self.toml_path(), &toml_content).await?;
520 self.toml = PartialProjectToml::new(
521 self.toml_path().to_str().unwrap_or("<lux.toml>"),
522 &toml_content,
523 self.root.clone(),
524 )?;
525
526 Ok(())
527 }
528
529 pub async fn upgrade_all(
530 &mut self,
531 package_db: &RemotePackageDB,
532 ) -> Result<(), ProjectEditError> {
533 if let Some(dependencies) = &self.toml().dependencies {
534 let packages = dependencies
535 .iter()
536 .map(|dep| dep.name())
537 .cloned()
538 .collect_vec();
539 self.upgrade(
540 LuaDependencyType::Regular(packages.iter().collect()),
541 package_db,
542 )
543 .await?;
544 }
545 if let Some(dependencies) = &self.toml().build_dependencies {
546 let packages = dependencies
547 .iter()
548 .map(|dep| dep.name())
549 .cloned()
550 .collect_vec();
551 self.upgrade(
552 LuaDependencyType::Build(packages.iter().collect()),
553 package_db,
554 )
555 .await?;
556 }
557 if let Some(dependencies) = &self.toml().test_dependencies {
558 let packages = dependencies
559 .iter()
560 .map(|dep| dep.name())
561 .cloned()
562 .collect_vec();
563 self.upgrade(
564 LuaDependencyType::Test(packages.iter().collect()),
565 package_db,
566 )
567 .await?;
568 }
569 Ok(())
570 }
571
572 pub async fn set_pinned_state(
573 &mut self,
574 dependencies: LuaDependencyType<&PackageName>,
575 pin: PinnedState,
576 ) -> Result<(), PinError> {
577 let mut project_toml =
578 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
579
580 prepare_dependency_tables(&mut project_toml);
581 let table = match dependencies {
582 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
583 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
584 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
585 };
586
587 match dependencies {
588 LuaDependencyType::Regular(ref _deps) => {
589 self.toml.dependencies = Some(
590 self.toml
591 .dependencies
592 .take()
593 .unwrap_or_default()
594 .into_iter()
595 .map(|dep| LuaDependencySpec { pin, ..dep })
596 .collect(),
597 )
598 }
599 LuaDependencyType::Build(ref _deps) => {
600 self.toml.build_dependencies = Some(
601 self.toml
602 .build_dependencies
603 .take()
604 .unwrap_or_default()
605 .into_iter()
606 .map(|dep| LuaDependencySpec { pin, ..dep })
607 .collect(),
608 )
609 }
610 LuaDependencyType::Test(ref _deps) => {
611 self.toml.test_dependencies = Some(
612 self.toml
613 .test_dependencies
614 .take()
615 .unwrap_or_default()
616 .into_iter()
617 .map(|dep| LuaDependencySpec { pin, ..dep })
618 .collect(),
619 )
620 }
621 }
622
623 match dependencies {
624 LuaDependencyType::Regular(ref deps)
625 | LuaDependencyType::Build(ref deps)
626 | LuaDependencyType::Test(ref deps) => {
627 for dep in deps {
628 let mut dep_item = table[dep.to_string()].clone();
629 match dep_item {
630 version @ Item::Value(_) => match &pin {
631 PinnedState::Unpinned => {}
632 PinnedState::Pinned => {
633 if let Ok(mut dep_entry) = toml_edit::table().into_table() {
634 dep_entry.set_implicit(true);
635 dep_entry["version"] = version;
636 dep_entry["pin"] = toml_edit::value(true);
637 table[dep.to_string()] = toml_edit::Item::Table(dep_entry);
638 }
639 }
640 },
641 Item::Table(_) => {
642 dep_item["pin".to_string()] = toml_edit::value(pin.as_bool());
643 table[dep.to_string()] = dep_item;
644 }
645 _ => {}
646 }
647 }
648 }
649 }
650
651 let toml_content = project_toml.to_string();
652 tokio::fs::write(self.toml_path(), &toml_content).await?;
653 self.toml = PartialProjectToml::new(
654 self.toml_path().to_str().unwrap_or("<lux.toml>"),
655 &toml_content,
656 self.root.clone(),
657 )?;
658
659 Ok(())
660 }
661
662 pub fn project_files(&self) -> Vec<PathBuf> {
663 project_files(&self.root().0)
664 }
665}
666
667pub(crate) fn project_files(src: &Path) -> Vec<PathBuf> {
670 ignore::WalkBuilder::new(src)
671 .add(src.join(".cargo"))
672 .follow_links(false)
673 .build()
674 .filter_map(Result::ok)
675 .filter(|entry| entry.file_type().is_some_and(|ft| ft.is_file()))
676 .map(|entry| entry.into_path())
677 .collect_vec()
678}
679
680fn prepare_dependency_tables(project_toml: &mut DocumentMut) {
681 if !project_toml.contains_table("dependencies") {
682 if let Ok(mut table) = toml_edit::table().into_table() {
683 table.set_implicit(true);
684 project_toml["dependencies"] = toml_edit::Item::Table(table);
685 }
686 }
687 if !project_toml.contains_table("build_dependencies") {
688 if let Ok(mut table) = toml_edit::table().into_table() {
689 table.set_implicit(true);
690 project_toml["build_dependencies"] = toml_edit::Item::Table(table);
691 }
692 }
693 if !project_toml.contains_table("test_dependencies") {
694 if let Ok(mut table) = toml_edit::table().into_table() {
695 table.set_implicit(true);
696 project_toml["test_dependencies"] = toml_edit::Item::Table(table);
697 }
698 }
699 if !project_toml.contains_table("external_dependencies") {
700 if let Ok(mut table) = toml_edit::table().into_table() {
701 table.set_implicit(true);
702 project_toml["external_dependencies"] = toml_edit::Item::Table(table);
703 }
704 }
705}
706
707#[cfg(test)]
709mod tests {
710 use std::collections::HashMap;
711
712 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
713 use url::Url;
714
715 use super::*;
716 use crate::{
717 lua_rockspec::ExternalDependencySpec,
718 manifest::{Manifest, ManifestMetadata},
719 package::PackageReq,
720 rockspec::Rockspec,
721 };
722
723 #[tokio::test]
724 async fn test_add_various_dependencies() {
725 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
726 .join("resources/test/sample-projects/no-build-spec/");
727 let project_root = assert_fs::TempDir::new().unwrap();
728 project_root.copy_from(&sample_project, &["**"]).unwrap();
729 let project_root: PathBuf = project_root.path().into();
730 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
731 let add_dependencies = [PackageReq::new("busted".into(), Some(">= 1.0.0".into())).unwrap()];
732 let expected_dependencies = vec![PackageReq::new("busted".into(), Some(">= 1.0.0".into()))
733 .unwrap()
734 .into()];
735
736 let test_manifest_path =
737 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
738 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
739 let metadata = ManifestMetadata::new(&content).unwrap();
740 let package_db = Manifest::new(Url::parse("https://example.com").unwrap(), metadata).into();
741
742 project
743 .add(
744 DependencyType::Regular(add_dependencies.iter().collect_vec()),
745 &package_db,
746 )
747 .await
748 .unwrap();
749
750 project
751 .add(
752 DependencyType::Build(add_dependencies.iter().collect_vec()),
753 &package_db,
754 )
755 .await
756 .unwrap();
757 project
758 .add(
759 DependencyType::Test(add_dependencies.iter().collect_vec()),
760 &package_db,
761 )
762 .await
763 .unwrap();
764
765 project
766 .add(
767 DependencyType::External(HashMap::from([(
768 "lib".into(),
769 ExternalDependencySpec {
770 library: Some("path.so".into()),
771 header: None,
772 },
773 )])),
774 &package_db,
775 )
776 .await
777 .unwrap();
778
779 let project = Project::from_exact(&project_root).unwrap().unwrap();
782 let validated_toml = project.toml().into_remote(None).unwrap();
783
784 assert_eq!(
785 validated_toml.dependencies().current_platform(),
786 &expected_dependencies
787 );
788 assert_eq!(
789 validated_toml.build_dependencies().current_platform(),
790 &expected_dependencies
791 );
792 assert_eq!(
793 validated_toml.test_dependencies().current_platform(),
794 &expected_dependencies
795 );
796 assert_eq!(
797 validated_toml
798 .external_dependencies()
799 .current_platform()
800 .get("lib")
801 .unwrap(),
802 &ExternalDependencySpec {
803 library: Some("path.so".into()),
804 header: None
805 }
806 );
807 }
808
809 #[tokio::test]
810 async fn test_remove_dependencies() {
811 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
812 .join("resources/test/sample-projects/dependencies/");
813 let project_root = assert_fs::TempDir::new().unwrap();
814 project_root.copy_from(&sample_project, &["**"]).unwrap();
815 let project_root: PathBuf = project_root.path().into();
816 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
817 let lua_cjson = "lua-cjson".into();
818 let plenary_nvim = "plenary.nvim".into();
819 let remove_dependencies = vec![&lua_cjson, &plenary_nvim];
820 project
821 .remove(DependencyType::Regular(remove_dependencies.clone()))
822 .await
823 .unwrap();
824 let check = |project: &Project| {
825 for name in &remove_dependencies {
826 assert!(!project
827 .toml()
828 .dependencies
829 .clone()
830 .unwrap_or_default()
831 .iter()
832 .any(|dep| &dep.name() == name));
833 }
834 };
835 check(&project);
836 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
838 check(&reloaded_project);
839 }
840
841 #[tokio::test]
842 async fn test_extra_rockspec_parsing() {
843 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
844 .join("resources/test/sample-projects/extra-rockspec/");
845 let project_root = assert_fs::TempDir::new().unwrap();
846 project_root.copy_from(&sample_project, &["**"]).unwrap();
847 let project_root: PathBuf = project_root.path().into();
848 let project = Project::from_exact(project_root).unwrap().unwrap();
849
850 let extra_rockspec = project.extra_rockspec().unwrap();
851
852 assert!(extra_rockspec.is_some());
853
854 let rocks = project.toml().into_remote(None).unwrap();
855
856 assert_eq!(rocks.package().to_string(), "custom-package");
857 }
858
859 #[tokio::test]
860 async fn test_pin_dependencies() {
861 test_pin_unpin_dependencies(PinnedState::Pinned).await
862 }
863
864 #[tokio::test]
865 async fn test_unpin_dependencies() {
866 test_pin_unpin_dependencies(PinnedState::Unpinned).await
867 }
868
869 async fn test_pin_unpin_dependencies(pin: PinnedState) {
870 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
871 .join("resources/test/sample-projects/dependencies/");
872 let project_root = assert_fs::TempDir::new().unwrap();
873 project_root.copy_from(&sample_project, &["**"]).unwrap();
874 let project_root: PathBuf = project_root.path().into();
875 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
876 let lua_cjson = "lua-cjson".into();
877 let plenary_nvim = "plenary.nvim".into();
878 let pin_dependencies = vec![&lua_cjson, &plenary_nvim];
879 project
880 .set_pinned_state(LuaDependencyType::Regular(pin_dependencies.clone()), pin)
881 .await
882 .unwrap();
883 let check = |project: &Project| {
884 for name in &pin_dependencies {
885 assert!(project
886 .toml()
887 .dependencies
888 .clone()
889 .unwrap_or_default()
890 .iter()
891 .any(|dep| &dep.name() == name && dep.pin == pin));
892 }
893 };
894 check(&project);
895 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
897 check(&reloaded_project);
898 }
899
900 #[tokio::test]
901 async fn project_files_includes_cargo_directory() {
902 let project_root = assert_fs::TempDir::new().unwrap();
903 let cargo_dir = project_root.child(".cargo");
904 cargo_dir.create_dir_all().unwrap();
905 let cargo_config = cargo_dir.join("config.toml");
906 tokio::fs::write(&cargo_config, "").await.unwrap();
907 let project_files = project_files(&project_root);
908 assert!(project_files.contains(&cargo_config.to_path_buf()));
909 }
910
911 #[tokio::test]
912 async fn from_exact_on_nonexistent_directory() {
913 let empty_dir = assert_fs::TempDir::new().unwrap();
914 assert!(Project::from_exact(&empty_dir).unwrap().is_none());
915 }
916}