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