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