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