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