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(#[from] LocalProjectTomlValidationError),
110 RockspecError(#[from] LuaRockspecError),
111}
112
113#[derive(Error, Debug, Diagnostic)]
114#[error(transparent)]
115pub enum IntoRemoteRockspecError {
116 RocksTomlValidationError(#[from] RemoteProjectTomlValidationError),
117 RockspecError(#[from] 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 Ok(self.toml().into_local()?.to_lua_rockspec()?)
249 }
250
251 pub fn remote_rockspec(
252 &self,
253 specrev: Option<SpecRev>,
254 ) -> Result<RemoteLuaRockspec, IntoRemoteRockspecError> {
255 Ok(self.toml().into_remote(specrev)?.to_lua_rockspec()?)
256 }
257
258 pub fn extra_rockspec(&self) -> Result<Option<PartialLuaRockspec>, PartialRockspecError> {
259 if self.extra_rockspec_path().exists() {
260 Ok(Some(PartialLuaRockspec::new(&std::fs::read_to_string(
261 self.extra_rockspec_path(),
262 )?)?))
263 } else {
264 Ok(None)
265 }
266 }
267
268 pub fn lua_version(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
269 self.toml().lua_version_matches(config)
270 }
271
272 pub async fn add(
273 &mut self,
274 dependencies: DependencyType<&PackageReq>,
275 package_db: &RemotePackageDB,
276 ) -> Result<(), ProjectEditError> {
277 let mut project_toml =
278 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
279
280 prepare_dependency_tables(&mut project_toml);
281 let table = match dependencies {
282 DependencyType::Regular(_) => &mut project_toml["dependencies"],
283 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
284 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
285 DependencyType::External(_) => &mut project_toml["external_dependencies"],
286 };
287
288 match dependencies {
289 DependencyType::Regular(ref deps)
290 | DependencyType::Build(ref deps)
291 | DependencyType::Test(ref deps) => {
292 for dep in deps {
293 let dep_version_str = if dep.version_req().is_any() {
294 package_db
295 .latest_version(dep.name())
296 .map(|latest_version| latest_version.to_string())
297 .unwrap_or_else(|| dep.version_req().to_string())
298 } else {
299 dep.version_req().to_string()
300 };
301 table[dep.name().to_string()] = toml_edit::value(dep_version_str);
302 }
303 }
304 DependencyType::External(ref deps) => {
305 for (name, dep) in deps {
306 if let Some(path) = &dep.header {
307 table[name]["header"] = toml_edit::value(path.to_slash_lossy().to_string());
308 }
309 if let Some(path) = &dep.library {
310 table[name]["library"] =
311 toml_edit::value(path.to_slash_lossy().to_string());
312 }
313 }
314 }
315 };
316
317 let toml_content = project_toml.to_string();
318 tokio::fs::write(self.toml_path(), &toml_content).await?;
319 self.toml = PartialProjectToml::new(
320 self.toml_path().to_str().unwrap_or("<lux.toml>"),
321 &toml_content,
322 self.root.clone(),
323 )?;
324
325 Ok(())
326 }
327
328 pub async fn add_git(
329 &mut self,
330 dependencies: LuaDependencyType<&RemoteGitUrlShorthand>,
331 ) -> Result<(), ProjectEditError> {
332 let mut project_toml =
333 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
334
335 prepare_dependency_tables(&mut project_toml);
336 let table = match dependencies {
337 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
338 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
339 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
340 };
341
342 match dependencies {
343 LuaDependencyType::Regular(urls)
344 | LuaDependencyType::Build(urls)
345 | LuaDependencyType::Test(urls) => {
346 for url in urls {
347 let git_url: RemoteGitUrl = url.clone().into();
348 let mut dep_entry = toml_edit::table();
349 match git::utils::latest_semver_tag_or_commit_sha(&git_url)? {
350 SemVerTagOrSha::SemVerTag(tag) => {
351 dep_entry["git"] = Item::Value(url.to_string().into());
352 dep_entry["version"] = Item::Value(tag.clone().into());
353 if tag.contains("-") {
354 dep_entry["rev"] = Item::Value(tag.into());
356 }
357 }
358 SemVerTagOrSha::CommitSha(sha) => {
359 dep_entry["git"] = Item::Value(url.to_string().into());
360 dep_entry["version"] = Item::Value(sha.into());
361 }
362 }
363 table[git_url.repo()] = dep_entry;
364 }
365 }
366 }
367
368 let toml_content = project_toml.to_string();
369 tokio::fs::write(self.toml_path(), &toml_content).await?;
370 self.toml = PartialProjectToml::new(
371 self.toml_path().to_str().unwrap_or("<lux.toml>"),
372 &toml_content,
373 self.root.clone(),
374 )?;
375
376 Ok(())
377 }
378
379 pub async fn remove(
380 &mut self,
381 dependencies: DependencyType<&PackageName>,
382 ) -> Result<(), ProjectEditError> {
383 let mut project_toml =
384 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
385
386 prepare_dependency_tables(&mut project_toml);
387 let table = match dependencies {
388 DependencyType::Regular(_) => &mut project_toml["dependencies"],
389 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
390 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
391 DependencyType::External(_) => &mut project_toml["external_dependencies"],
392 };
393
394 match dependencies {
395 DependencyType::Regular(ref deps)
396 | DependencyType::Build(ref deps)
397 | DependencyType::Test(ref deps) => {
398 for dep in deps {
399 table[dep.to_string()] = Item::None;
400 }
401 }
402 DependencyType::External(ref deps) => {
403 for (name, dep) in deps {
404 if dep.header.is_some() {
405 table[name]["header"] = Item::None;
406 }
407 if dep.library.is_some() {
408 table[name]["library"] = Item::None;
409 }
410 }
411 }
412 };
413
414 let toml_content = project_toml.to_string();
415 tokio::fs::write(self.toml_path(), &toml_content).await?;
416 self.toml = PartialProjectToml::new(
417 self.toml_path().to_str().unwrap_or("<lux.toml>"),
418 &toml_content,
419 self.root.clone(),
420 )?;
421
422 Ok(())
423 }
424
425 pub async fn upgrade(
426 &mut self,
427 dependencies: LuaDependencyType<&PackageName>,
428 package_db: &RemotePackageDB,
429 ) -> Result<(), ProjectEditError> {
430 let mut project_toml =
431 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
432
433 prepare_dependency_tables(&mut project_toml);
434 let table = match dependencies {
435 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
436 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
437 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
438 };
439
440 match dependencies {
441 LuaDependencyType::Regular(deps)
442 | LuaDependencyType::Build(deps)
443 | LuaDependencyType::Test(deps) => {
444 let latest_rock_version_str =
445 |dep: &PackageName| -> Result<String, ProjectEditError> {
446 Ok(package_db
447 .latest_version(dep)
448 .ok_or(ProjectEditError::LatestVersionNotFound(dep.clone()))?
449 .to_string())
450 };
451 for dep in deps {
452 let mut dep_item = table[dep.to_string()].clone();
453 match &dep_item {
454 Item::Value(_) => {
455 let dep_version_str = latest_rock_version_str(dep)?;
456 table[dep.to_string()] = toml_edit::value(dep_version_str);
457 }
458 Item::Table(tbl) => {
459 match tbl.get("git") {
460 Some(git_item) => {
461 let git_value =
462 git_item.clone().into_value().map_err(|err| {
463 ProjectEditError::ExpectedValue(Box::new(err))
464 })?;
465 let git_url_str = git_value.as_str().ok_or(
466 ProjectEditError::ExpectedString(Box::new(
467 git_value.clone(),
468 )),
469 )?;
470 let shorthand: RemoteGitUrlShorthand = git_url_str.parse()?;
471 match git::utils::latest_semver_tag_or_commit_sha(
472 &shorthand.into(),
473 )? {
474 SemVerTagOrSha::SemVerTag(latest_tag) => {
475 table[dep.to_string()]["version"] =
476 Item::Value(latest_tag.clone().into());
477 if latest_tag.contains("-") {
478 table[dep.to_string()]["rev"] =
480 Item::Value(latest_tag.into());
481 }
482 }
483 SemVerTagOrSha::CommitSha(latest_sha) => {
484 table[dep.to_string()]["version"] =
485 Item::Value(latest_sha.into());
486 }
487 }
488 table[dep.to_string()] = dep_item;
489 }
490 None => {
491 let dep_version_str = latest_rock_version_str(dep)?;
492 dep_item["version".to_string()] =
493 toml_edit::value(dep_version_str);
494 table[dep.to_string()] = dep_item;
495 }
496 }
497 }
498 _ => {}
499 }
500 }
501 }
502 }
503
504 let toml_content = project_toml.to_string();
505 tokio::fs::write(self.toml_path(), &toml_content).await?;
506 self.toml = PartialProjectToml::new(
507 self.toml_path().to_str().unwrap_or("<lux.toml>"),
508 &toml_content,
509 self.root.clone(),
510 )?;
511
512 Ok(())
513 }
514
515 pub async fn upgrade_all(
516 &mut self,
517 package_db: &RemotePackageDB,
518 ) -> Result<(), ProjectEditError> {
519 if let Some(dependencies) = &self.toml().dependencies {
520 let packages = dependencies
521 .iter()
522 .map(|dep| dep.name())
523 .cloned()
524 .collect_vec();
525 self.upgrade(
526 LuaDependencyType::Regular(packages.iter().collect()),
527 package_db,
528 )
529 .await?;
530 }
531 if let Some(dependencies) = &self.toml().build_dependencies {
532 let packages = dependencies
533 .iter()
534 .map(|dep| dep.name())
535 .cloned()
536 .collect_vec();
537 self.upgrade(
538 LuaDependencyType::Build(packages.iter().collect()),
539 package_db,
540 )
541 .await?;
542 }
543 if let Some(dependencies) = &self.toml().test_dependencies {
544 let packages = dependencies
545 .iter()
546 .map(|dep| dep.name())
547 .cloned()
548 .collect_vec();
549 self.upgrade(
550 LuaDependencyType::Test(packages.iter().collect()),
551 package_db,
552 )
553 .await?;
554 }
555 Ok(())
556 }
557
558 pub async fn set_pinned_state(
559 &mut self,
560 dependencies: LuaDependencyType<&PackageName>,
561 pin: PinnedState,
562 ) -> Result<(), PinError> {
563 let mut project_toml =
564 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
565
566 prepare_dependency_tables(&mut project_toml);
567 let table = match dependencies {
568 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
569 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
570 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
571 };
572
573 match dependencies {
574 LuaDependencyType::Regular(ref _deps) => {
575 self.toml.dependencies = Some(
576 self.toml
577 .dependencies
578 .take()
579 .unwrap_or_default()
580 .into_iter()
581 .map(|dep| LuaDependencySpec { pin, ..dep })
582 .collect(),
583 )
584 }
585 LuaDependencyType::Build(ref _deps) => {
586 self.toml.build_dependencies = Some(
587 self.toml
588 .build_dependencies
589 .take()
590 .unwrap_or_default()
591 .into_iter()
592 .map(|dep| LuaDependencySpec { pin, ..dep })
593 .collect(),
594 )
595 }
596 LuaDependencyType::Test(ref _deps) => {
597 self.toml.test_dependencies = Some(
598 self.toml
599 .test_dependencies
600 .take()
601 .unwrap_or_default()
602 .into_iter()
603 .map(|dep| LuaDependencySpec { pin, ..dep })
604 .collect(),
605 )
606 }
607 }
608
609 match dependencies {
610 LuaDependencyType::Regular(ref deps)
611 | LuaDependencyType::Build(ref deps)
612 | LuaDependencyType::Test(ref deps) => {
613 for dep in deps {
614 let mut dep_item = table[dep.to_string()].clone();
615 match dep_item {
616 version @ Item::Value(_) => match &pin {
617 PinnedState::Unpinned => {}
618 PinnedState::Pinned => {
619 if let Ok(mut dep_entry) = toml_edit::table().into_table() {
620 dep_entry.set_implicit(true);
621 dep_entry["version"] = version;
622 dep_entry["pin"] = toml_edit::value(true);
623 table[dep.to_string()] = toml_edit::Item::Table(dep_entry);
624 }
625 }
626 },
627 Item::Table(_) => {
628 dep_item["pin".to_string()] = toml_edit::value(pin.as_bool());
629 table[dep.to_string()] = dep_item;
630 }
631 _ => {}
632 }
633 }
634 }
635 }
636
637 let toml_content = project_toml.to_string();
638 tokio::fs::write(self.toml_path(), &toml_content).await?;
639 self.toml = PartialProjectToml::new(
640 self.toml_path().to_str().unwrap_or("<lux.toml>"),
641 &toml_content,
642 self.root.clone(),
643 )?;
644
645 Ok(())
646 }
647
648 pub fn project_files(&self) -> Vec<PathBuf> {
649 project_files(&self.root().0)
650 }
651}
652
653pub(crate) fn project_files(src: &Path) -> Vec<PathBuf> {
656 ignore::WalkBuilder::new(src)
657 .add(src.join(".cargo"))
658 .follow_links(false)
659 .build()
660 .filter_map(Result::ok)
661 .filter(|entry| entry.file_type().is_some_and(|ft| ft.is_file()))
662 .map(|entry| entry.into_path())
663 .collect_vec()
664}
665
666fn prepare_dependency_tables(project_toml: &mut DocumentMut) {
667 if !project_toml.contains_table("dependencies") {
668 if let Ok(mut table) = toml_edit::table().into_table() {
669 table.set_implicit(true);
670 project_toml["dependencies"] = toml_edit::Item::Table(table);
671 }
672 }
673 if !project_toml.contains_table("build_dependencies") {
674 if let Ok(mut table) = toml_edit::table().into_table() {
675 table.set_implicit(true);
676 project_toml["build_dependencies"] = toml_edit::Item::Table(table);
677 }
678 }
679 if !project_toml.contains_table("test_dependencies") {
680 if let Ok(mut table) = toml_edit::table().into_table() {
681 table.set_implicit(true);
682 project_toml["test_dependencies"] = toml_edit::Item::Table(table);
683 }
684 }
685 if !project_toml.contains_table("external_dependencies") {
686 if let Ok(mut table) = toml_edit::table().into_table() {
687 table.set_implicit(true);
688 project_toml["external_dependencies"] = toml_edit::Item::Table(table);
689 }
690 }
691}
692
693#[cfg(test)]
695mod tests {
696 use std::collections::HashMap;
697
698 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
699 use url::Url;
700
701 use super::*;
702 use crate::{
703 lua_rockspec::ExternalDependencySpec,
704 manifest::{Manifest, ManifestMetadata},
705 package::PackageReq,
706 rockspec::Rockspec,
707 };
708
709 #[tokio::test]
710 async fn test_add_various_dependencies() {
711 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
712 .join("resources/test/sample-projects/no-build-spec/");
713 let project_root = assert_fs::TempDir::new().unwrap();
714 project_root.copy_from(&sample_project, &["**"]).unwrap();
715 let project_root: PathBuf = project_root.path().into();
716 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
717 let add_dependencies = [PackageReq::new("busted".into(), Some(">= 1.0.0".into())).unwrap()];
718 let expected_dependencies = vec![PackageReq::new("busted".into(), Some(">= 1.0.0".into()))
719 .unwrap()
720 .into()];
721
722 let test_manifest_path =
723 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
724 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
725 let metadata = ManifestMetadata::new(&content).unwrap();
726 let package_db = Manifest::new(Url::parse("https://example.com").unwrap(), metadata).into();
727
728 project
729 .add(
730 DependencyType::Regular(add_dependencies.iter().collect_vec()),
731 &package_db,
732 )
733 .await
734 .unwrap();
735
736 project
737 .add(
738 DependencyType::Build(add_dependencies.iter().collect_vec()),
739 &package_db,
740 )
741 .await
742 .unwrap();
743 project
744 .add(
745 DependencyType::Test(add_dependencies.iter().collect_vec()),
746 &package_db,
747 )
748 .await
749 .unwrap();
750
751 project
752 .add(
753 DependencyType::External(HashMap::from([(
754 "lib".into(),
755 ExternalDependencySpec {
756 library: Some("path.so".into()),
757 header: None,
758 },
759 )])),
760 &package_db,
761 )
762 .await
763 .unwrap();
764
765 let project = Project::from_exact(&project_root).unwrap().unwrap();
768 let validated_toml = project.toml().into_remote(None).unwrap();
769
770 assert_eq!(
771 validated_toml.dependencies().current_platform(),
772 &expected_dependencies
773 );
774 assert_eq!(
775 validated_toml.build_dependencies().current_platform(),
776 &expected_dependencies
777 );
778 assert_eq!(
779 validated_toml.test_dependencies().current_platform(),
780 &expected_dependencies
781 );
782 assert_eq!(
783 validated_toml
784 .external_dependencies()
785 .current_platform()
786 .get("lib")
787 .unwrap(),
788 &ExternalDependencySpec {
789 library: Some("path.so".into()),
790 header: None
791 }
792 );
793 }
794
795 #[tokio::test]
796 async fn test_remove_dependencies() {
797 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
798 .join("resources/test/sample-projects/dependencies/");
799 let project_root = assert_fs::TempDir::new().unwrap();
800 project_root.copy_from(&sample_project, &["**"]).unwrap();
801 let project_root: PathBuf = project_root.path().into();
802 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
803 let lua_cjson = "lua-cjson".into();
804 let plenary_nvim = "plenary.nvim".into();
805 let remove_dependencies = vec![&lua_cjson, &plenary_nvim];
806 project
807 .remove(DependencyType::Regular(remove_dependencies.clone()))
808 .await
809 .unwrap();
810 let check = |project: &Project| {
811 for name in &remove_dependencies {
812 assert!(!project
813 .toml()
814 .dependencies
815 .clone()
816 .unwrap_or_default()
817 .iter()
818 .any(|dep| &dep.name() == name));
819 }
820 };
821 check(&project);
822 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
824 check(&reloaded_project);
825 }
826
827 #[tokio::test]
828 async fn test_extra_rockspec_parsing() {
829 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
830 .join("resources/test/sample-projects/extra-rockspec/");
831 let project_root = assert_fs::TempDir::new().unwrap();
832 project_root.copy_from(&sample_project, &["**"]).unwrap();
833 let project_root: PathBuf = project_root.path().into();
834 let project = Project::from_exact(project_root).unwrap().unwrap();
835
836 let extra_rockspec = project.extra_rockspec().unwrap();
837
838 assert!(extra_rockspec.is_some());
839
840 let rocks = project.toml().into_remote(None).unwrap();
841
842 assert_eq!(rocks.package().to_string(), "custom-package");
843 }
844
845 #[tokio::test]
846 async fn test_pin_dependencies() {
847 test_pin_unpin_dependencies(PinnedState::Pinned).await
848 }
849
850 #[tokio::test]
851 async fn test_unpin_dependencies() {
852 test_pin_unpin_dependencies(PinnedState::Unpinned).await
853 }
854
855 async fn test_pin_unpin_dependencies(pin: PinnedState) {
856 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
857 .join("resources/test/sample-projects/dependencies/");
858 let project_root = assert_fs::TempDir::new().unwrap();
859 project_root.copy_from(&sample_project, &["**"]).unwrap();
860 let project_root: PathBuf = project_root.path().into();
861 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
862 let lua_cjson = "lua-cjson".into();
863 let plenary_nvim = "plenary.nvim".into();
864 let pin_dependencies = vec![&lua_cjson, &plenary_nvim];
865 project
866 .set_pinned_state(LuaDependencyType::Regular(pin_dependencies.clone()), pin)
867 .await
868 .unwrap();
869 let check = |project: &Project| {
870 for name in &pin_dependencies {
871 assert!(project
872 .toml()
873 .dependencies
874 .clone()
875 .unwrap_or_default()
876 .iter()
877 .any(|dep| &dep.name() == name && dep.pin == pin));
878 }
879 };
880 check(&project);
881 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
883 check(&reloaded_project);
884 }
885
886 #[tokio::test]
887 async fn project_files_includes_cargo_directory() {
888 let project_root = assert_fs::TempDir::new().unwrap();
889 let cargo_dir = project_root.child(".cargo");
890 cargo_dir.create_dir_all().unwrap();
891 let cargo_config = cargo_dir.join("config.toml");
892 tokio::fs::write(&cargo_config, "").await.unwrap();
893 let project_files = project_files(&project_root);
894 assert!(project_files.contains(&cargo_config.to_path_buf()));
895 }
896
897 #[tokio::test]
898 async fn from_exact_on_nonexistent_directory() {
899 let empty_dir = assert_fs::TempDir::new().unwrap();
900 assert!(Project::from_exact(&empty_dir).unwrap().is_none());
901 }
902}