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