1use crate::package;
2use itertools::Itertools;
3use miette::{Diagnostic, NamedSource, SourceSpan};
4use path_slash::PathBufExt;
5use project_toml::{
6 LocalProjectTomlValidationError, PartialProjectToml, RemoteProjectTomlValidationError,
7};
8use std::{
9 io,
10 ops::Deref,
11 path::{Path, PathBuf},
12 str::FromStr,
13};
14use thiserror::Error;
15use toml_edit::{DocumentMut, Item};
16
17use crate::{fs, lockfile::OptState};
18
19use crate::lockfile::PinnedState;
20use crate::{
21 config::Config,
22 git::{self, shorthand::RemoteGitUrlShorthand, url::RemoteGitUrl, utils::SemVerTagOrSha},
23 lua_rockspec::{
24 self, LocalLuaRockspec, LuaVersionError, PartialLuaRockspec, PartialRockspecError,
25 RemoteLuaRockspec,
26 },
27 lua_version::LuaVersion,
28 package_db::PackageDB,
29 rockspec::{
30 lua_dependency::{DependencyType, LuaDependencyType},
31 LuaVersionCompatibility,
32 },
33};
34
35pub(crate) mod gen;
36pub mod project_toml;
37
38pub use project_toml::PROJECT_TOML;
39
40pub const EXTRA_ROCKSPEC: &str = "extra.rockspec";
41
42#[derive(Debug, Error, Diagnostic)]
45#[error("{message}", message = _inner.message())]
46#[diagnostic(
47 code(lux_lib::toml::deserialize),
48 help("check that the file is valid TOML and matches the expected schema"),
49 url("https://lux.lumen-labs.org/reference/lux-toml")
50)]
51pub struct TomlDeError {
52 #[source]
53 _inner: Box<toml::de::Error>,
54 #[source_code]
55 src: NamedSource<String>,
56 #[label("here")]
57 span: SourceSpan,
58}
59
60impl TomlDeError {
61 pub fn new(name: &str, src: &str, inner: toml::de::Error) -> Self {
62 let src = NamedSource::new(name, src.to_string());
63 let span = inner.span().unwrap_or(0..0).into();
64 Self {
65 _inner: Box::new(inner),
66 src,
67 span,
68 }
69 }
70
71 pub fn inner(&self) -> &toml::de::Error {
72 &self._inner
73 }
74}
75
76pub(crate) fn parse_toml<T>(name: &str, src: &str) -> Result<T, TomlDeError>
77where
78 T: serde::de::DeserializeOwned,
79{
80 toml::from_str(src).map_err(|e| TomlDeError::new(name, src, e))
81}
82
83#[derive(Error, Debug, Diagnostic)]
84#[non_exhaustive]
85#[error(transparent)]
86pub enum Error {
87 #[error(transparent)]
88 #[diagnostic(transparent)]
89 Fs(#[from] fs::Error),
90 #[diagnostic(transparent)]
91 Project(#[from] LocalProjectTomlValidationError),
92 #[diagnostic(transparent)]
93 Toml(#[from] TomlDeError),
94 #[error("error when parsing 'extra.rockspec'")]
95 #[diagnostic(forward(0))]
96 Rockspec(#[from] PartialRockspecError),
97}
98
99#[derive(Error, Debug, Diagnostic)]
100#[non_exhaustive]
101#[error(transparent)]
102pub enum IntoLocalRockspecError {
103 LocalProjectTomlValidationError(Box<LocalProjectTomlValidationError>),
104 Rockspec(Box<lua_rockspec::Error>),
105}
106
107#[derive(Error, Debug, Diagnostic)]
108#[non_exhaustive]
109#[error(transparent)]
110pub enum IntoRemoteRockspecError {
111 RocksTomlValidationError(Box<RemoteProjectTomlValidationError>),
112 Rockspec(Box<lua_rockspec::Error>),
113}
114
115#[derive(Error, Debug, Diagnostic)]
116#[non_exhaustive]
117pub enum EditError {
118 #[error(transparent)]
119 Io(#[from] tokio::io::Error),
120 #[error(transparent)]
121 #[diagnostic(transparent)]
122 Fs(#[from] fs::Error),
123 #[error(transparent)]
124 Toml(#[from] toml_edit::TomlError),
125 #[error("error parsing {PROJECT_TOML} after edit. This is probably a bug.")]
126 #[diagnostic(forward(0))]
127 TomlDe(#[from] TomlDeError),
128 #[error(transparent)]
129 #[diagnostic(transparent)]
130 Git(#[from] git::Error),
131 #[error("unable to query latest version for '{0}'")]
132 #[diagnostic(help("check your server config and ensure the package has been published"))]
133 LatestVersionNotFound(package::Name),
134 #[error("expected field to be a value, but got '{0}'")]
135 #[diagnostic(
136 help("ensure your {PROJECT_TOML} matches the specification"),
137 url("https://lux.lumen-labs.org/reference/lux-toml")
138 )]
139 ExpectedValue(Box<toml_edit::Item>),
140 #[error("expected string, but got '{0}'")]
141 #[diagnostic(
142 help("ensure your {PROJECT_TOML} matches the specification"),
143 url("https://lux.lumen-labs.org/reference/lux-toml")
144 )]
145 ExpectedString(Box<toml_edit::Value>),
146 #[error(transparent)]
147 #[diagnostic(transparent)]
148 GitUrlShorthandParse(#[from] git::shorthand::ParseError),
149}
150
151#[derive(Error, Debug, Diagnostic)]
152#[non_exhaustive]
153pub enum PinError {
154 #[error("package '{0}' not found in dependencies")]
155 #[diagnostic(help("to add it, run 'lx add {0}'"))]
156 PackageNotFound(package::Name),
157 #[error("dependency '{dep}' is already {}pinned!", if *.pin_state == PinnedState::Unpinned { "un" } else { "" })]
158 PinStateUnchanged {
159 pin_state: PinnedState,
160 dep: package::Name,
161 },
162 #[error("unable to parse {toml_path} for editing")]
163 #[diagnostic(
164 code(lux_lib::toml_edit::parse),
165 help("check that the file is valid TOML and matches the expected schema"),
166 url("https://lux.lumen-labs.org/reference/lux-toml")
167 )]
168 ParseTomlEdit {
169 toml_path: String,
170 source: toml_edit::TomlError,
171 },
172 #[error("error parsing lux.toml after edit. This is probably a bug.")]
173 #[diagnostic(forward(0))]
174 TomlDe(#[from] TomlDeError),
175 #[error(transparent)]
176 Io(#[from] tokio::io::Error),
177 #[error(transparent)]
178 #[diagnostic(transparent)]
179 Fs(#[from] fs::Error),
180}
181
182#[derive(Clone, Debug)]
185#[cfg_attr(test, derive(Default))]
186pub struct ProjectRoot(PathBuf);
187
188impl ProjectRoot {
189 pub(crate) fn new() -> Self {
190 Self(PathBuf::new())
191 }
192}
193
194impl AsRef<Path> for ProjectRoot {
195 fn as_ref(&self) -> &Path {
196 self.0.as_ref()
197 }
198}
199
200impl Deref for ProjectRoot {
201 type Target = PathBuf;
202
203 fn deref(&self) -> &Self::Target {
204 &self.0
205 }
206}
207
208#[derive(Clone, Debug)]
210pub struct Project {
211 lux_toml_dir: ProjectRoot,
213 root: ProjectRoot,
216 toml: PartialProjectToml,
218}
219
220impl Project {
221 #[tracing::instrument(level = "trace", skip_all)]
223 pub fn from_exact(start: impl AsRef<Path>) -> Result<Option<Self>, Error> {
224 if !start.as_ref().exists() {
225 return Ok(None);
226 }
227
228 if start.as_ref().join(PROJECT_TOML).exists() {
229 let project_toml_path = start.as_ref().join(PROJECT_TOML);
230 let toml_content = fs::sync::read_to_string(&project_toml_path)?;
231 let root = start.as_ref();
232
233 let toml = PartialProjectToml::new(
234 &project_toml_path.to_string_lossy(),
235 &toml_content,
236 ProjectRoot(root.to_path_buf()),
237 )?;
238 let mut project = Project {
239 lux_toml_dir: ProjectRoot(root.to_path_buf()),
240 root: toml.project.root.clone(),
241 toml,
242 };
243
244 if let Some(extra_rockspec) = project.extra_rockspec()? {
245 project.toml = project.toml.merge(extra_rockspec);
246 }
247
248 Ok(Some(project))
249 } else {
250 Ok(None)
251 }
252 }
253
254 pub fn toml_path(&self) -> PathBuf {
256 self.lux_toml_dir.join(PROJECT_TOML)
257 }
258
259 pub fn extra_rockspec_path(&self) -> PathBuf {
261 self.lux_toml_dir.join(EXTRA_ROCKSPEC)
262 }
263
264 pub fn root(&self) -> &ProjectRoot {
265 &self.root
266 }
267
268 pub fn toml(&self) -> &PartialProjectToml {
269 &self.toml
270 }
271
272 pub fn local_rockspec(&self) -> Result<LocalLuaRockspec, IntoLocalRockspecError> {
273 self.toml()
274 .into_local()
275 .map_err(|err| IntoLocalRockspecError::LocalProjectTomlValidationError(Box::new(err)))?
276 .to_lua_rockspec()
277 .map_err(|err| IntoLocalRockspecError::Rockspec(Box::new(err)))
278 }
279
280 pub fn local_remote_rockspec(&self) -> Result<RemoteLuaRockspec, IntoLocalRockspecError> {
283 self.toml()
284 .into_local()
285 .map_err(|err| IntoLocalRockspecError::LocalProjectTomlValidationError(Box::new(err)))?
286 .to_local_rockspec()
287 .map_err(|err| IntoLocalRockspecError::Rockspec(Box::new(err)))
288 }
289
290 pub fn remote_rockspec(
291 &self,
292 specrev: Option<package::version::SpecRev>,
293 ) -> Result<RemoteLuaRockspec, IntoRemoteRockspecError> {
294 self.toml()
295 .into_remote(specrev)
296 .map_err(|err| IntoRemoteRockspecError::RocksTomlValidationError(Box::new(err)))?
297 .to_lua_rockspec()
298 .map_err(|err| IntoRemoteRockspecError::Rockspec(Box::new(err)))
299 }
300
301 pub fn extra_rockspec(&self) -> Result<Option<PartialLuaRockspec>, PartialRockspecError> {
302 if self.extra_rockspec_path().exists() {
303 Ok(Some(PartialLuaRockspec::new(
304 &fs::sync::read_to_string(self.extra_rockspec_path()).map_err(io::Error::other)?,
305 )?))
306 } else {
307 Ok(None)
308 }
309 }
310
311 pub fn lua_version(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
312 self.toml().lua_version_matches(config)
313 }
314
315 async fn write_toml(&mut self, project_toml: DocumentMut) -> Result<(), EditError> {
316 let toml_content = project_toml.to_string();
317 fs::tokio::write(self.toml_path(), &toml_content).await?;
318 self.toml = PartialProjectToml::new(
319 self.toml_path().to_str().unwrap_or("<lux.toml>"),
320 &toml_content,
321 self.lux_toml_dir.clone(),
322 )?;
323 Ok(())
324 }
325
326 pub async fn add(
327 &mut self,
328 dependencies: DependencyType<&package::Req>,
329 package_db: &PackageDB,
330 ) -> Result<(), EditError> {
331 let mut project_toml =
332 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
333
334 prepare_dependency_tables(&mut project_toml);
335 let table = match dependencies {
336 DependencyType::Regular(_) => &mut project_toml["dependencies"],
337 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
338 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
339 DependencyType::External(_) => &mut project_toml["external_dependencies"],
340 };
341
342 match dependencies {
343 DependencyType::Regular(ref deps)
344 | DependencyType::Build(ref deps)
345 | DependencyType::Test(ref deps) => {
346 for dep in deps {
347 let dep_version_str = if dep.version_req().is_any() {
348 let latest_version = package_db
349 .latest_version(dep.name())
350 .await
351 .ok_or_else(|| EditError::LatestVersionNotFound(dep.name().clone()))?;
352 latest_version
353 .to_pessimistic_version_req()
354 .unwrap_or_else(|| latest_version.to_string())
355 } else {
356 dep.version_req().to_string()
357 };
358 table[dep.name().to_string()] = toml_edit::value(dep_version_str);
359 }
360 }
361 DependencyType::External(ref deps) => {
362 for (name, dep) in deps {
363 if let Some(path) = &dep.header {
364 table[name]["header"] = toml_edit::value(path.to_slash_lossy().to_string());
365 }
366 if let Some(path) = &dep.library {
367 table[name]["library"] =
368 toml_edit::value(path.to_slash_lossy().to_string());
369 }
370 }
371 }
372 };
373
374 self.write_toml(project_toml).await?;
375 Ok(())
376 }
377
378 pub async fn add_git(
379 &mut self,
380 dependencies: LuaDependencyType<&RemoteGitUrlShorthand>,
381 config: &Config,
382 ) -> Result<(), EditError> {
383 let mut project_toml =
384 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
385
386 prepare_dependency_tables(&mut project_toml);
387 let table = match dependencies {
388 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
389 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
390 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
391 };
392
393 match dependencies {
394 LuaDependencyType::Regular(urls)
395 | LuaDependencyType::Build(urls)
396 | LuaDependencyType::Test(urls) => {
397 for url in urls {
398 let git_url: RemoteGitUrl = url.clone().into();
399 let mut dep_entry = toml_edit::table();
400 match git::utils::latest_semver_tag_or_commit_sha(&git_url, config)? {
401 SemVerTagOrSha::SemVerTag(tag) => {
402 dep_entry["git"] = Item::Value(url.to_string().into());
403 dep_entry["version"] = Item::Value(tag.clone().into());
404 if tag.contains("-") {
405 dep_entry["rev"] = Item::Value(tag.into());
407 }
408 }
409 SemVerTagOrSha::CommitSha(sha) => {
410 dep_entry["git"] = Item::Value(url.to_string().into());
411 dep_entry["version"] = Item::Value(sha.into());
412 }
413 }
414 table[git_url.repo()] = dep_entry;
415 }
416 }
417 }
418
419 self.write_toml(project_toml).await?;
420
421 Ok(())
422 }
423
424 pub async fn remove(
425 &mut self,
426 dependencies: DependencyType<&package::Name>,
427 ) -> Result<(), EditError> {
428 let mut project_toml =
429 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
430
431 prepare_dependency_tables(&mut project_toml);
432 let table = match dependencies {
433 DependencyType::Regular(_) => &mut project_toml["dependencies"],
434 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
435 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
436 DependencyType::External(_) => &mut project_toml["external_dependencies"],
437 };
438
439 match dependencies {
440 DependencyType::Regular(ref deps)
441 | DependencyType::Build(ref deps)
442 | DependencyType::Test(ref deps) => {
443 for dep in deps {
444 table[dep.to_string()] = Item::None;
445 }
446 }
447 DependencyType::External(ref deps) => {
448 for (name, dep) in deps {
449 if dep.header.is_some() {
450 table[name]["header"] = Item::None;
451 }
452 if dep.library.is_some() {
453 table[name]["library"] = Item::None;
454 }
455 }
456 }
457 };
458
459 self.write_toml(project_toml).await?;
460
461 Ok(())
462 }
463
464 pub async fn upgrade(
465 &mut self,
466 dependencies: LuaDependencyType<&package::Name>,
467 package_db: &PackageDB,
468 config: &Config,
469 ) -> Result<(), EditError> {
470 let mut project_toml =
471 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
472
473 prepare_dependency_tables(&mut project_toml);
474 let table = match dependencies {
475 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
476 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
477 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
478 };
479
480 match dependencies {
481 LuaDependencyType::Regular(deps)
482 | LuaDependencyType::Build(deps)
483 | LuaDependencyType::Test(deps) => {
484 for dep in deps {
485 upgrade_dependency(table, dep, package_db, config).await?;
486 }
487 }
488 }
489
490 self.write_toml(project_toml).await?;
491
492 Ok(())
493 }
494
495 pub async fn upgrade_all(
496 &mut self,
497 package_db: &PackageDB,
498 config: &Config,
499 ) -> Result<(), EditError> {
500 if let Some(dependencies) = &self.toml().dependencies {
501 let packages = dependencies
502 .iter()
503 .map(|dep| dep.name())
504 .cloned()
505 .collect_vec();
506 self.upgrade(
507 LuaDependencyType::Regular(packages.iter().collect()),
508 package_db,
509 config,
510 )
511 .await?;
512 }
513 if let Some(dependencies) = &self.toml().build_dependencies {
514 let packages = dependencies
515 .iter()
516 .map(|dep| dep.name())
517 .cloned()
518 .collect_vec();
519 self.upgrade(
520 LuaDependencyType::Build(packages.iter().collect()),
521 package_db,
522 config,
523 )
524 .await?;
525 }
526 if let Some(dependencies) = &self.toml().test_dependencies {
527 let packages = dependencies
528 .iter()
529 .map(|dep| dep.name())
530 .cloned()
531 .collect_vec();
532 self.upgrade(
533 LuaDependencyType::Test(packages.iter().collect()),
534 package_db,
535 config,
536 )
537 .await?;
538 }
539 Ok(())
540 }
541
542 async fn set_dependency_flag(
543 &mut self,
544 dependencies: LuaDependencyType<&package::Name>,
545 key: &str,
546 enabled: bool,
547 ) -> Result<(), PinError> {
548 let mut project_toml =
549 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)
550 .map_err(|source| PinError::ParseTomlEdit {
551 toml_path: self.toml_path().to_slash_lossy().to_string(),
552 source,
553 })?;
554
555 prepare_dependency_tables(&mut project_toml);
556 let table = match dependencies {
557 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
558 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
559 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
560 };
561
562 match dependencies {
563 LuaDependencyType::Regular(ref deps)
564 | LuaDependencyType::Build(ref deps)
565 | LuaDependencyType::Test(ref deps) => {
566 for dep in deps {
567 let mut dep_item = table[dep.to_string()].clone();
568 match dep_item {
569 version @ Item::Value(_) => {
570 if enabled {
571 if let Ok(mut dep_entry) = toml_edit::table().into_table() {
572 dep_entry.set_implicit(true);
573 dep_entry["version"] = version;
574 dep_entry[key] = toml_edit::value(true);
575 table[dep.to_string()] = toml_edit::Item::Table(dep_entry);
576 }
577 }
578 }
579 Item::Table(_) => {
580 dep_item[key] = toml_edit::value(enabled);
581 table[dep.to_string()] = dep_item;
582 }
583 _ => {}
584 }
585 }
586 }
587 }
588
589 let toml_content = project_toml.to_string();
590 fs::tokio::write(self.toml_path(), &toml_content).await?;
591 self.toml = PartialProjectToml::new(
592 self.toml_path().to_str().unwrap_or("<lux.toml>"),
593 &toml_content,
594 self.lux_toml_dir.clone(),
595 )?;
596
597 Ok(())
598 }
599
600 pub async fn set_pinned_state(
601 &mut self,
602 dependencies: LuaDependencyType<&package::Name>,
603 pin: PinnedState,
604 ) -> Result<(), PinError> {
605 self.set_dependency_flag(dependencies, "pin", pin.as_bool())
606 .await
607 }
608
609 pub async fn set_opt_state(
610 &mut self,
611 dependencies: LuaDependencyType<&package::Name>,
612 opt: OptState,
613 ) -> Result<(), PinError> {
614 self.set_dependency_flag(dependencies, "opt", opt.as_bool())
615 .await
616 }
617
618 pub fn project_files(&self) -> Vec<PathBuf> {
619 project_files(&self.root().0)
620 }
621}
622
623pub(crate) fn project_files(src: &Path) -> Vec<PathBuf> {
626 ignore::WalkBuilder::new(src)
627 .add(src.join(".cargo"))
628 .parents(false)
629 .follow_links(false)
630 .build()
631 .filter_map(Result::ok)
632 .filter(|entry| entry.file_type().is_some_and(|ft| ft.is_file()))
633 .map(|entry| entry.into_path())
634 .collect_vec()
635}
636
637#[tracing::instrument(level = "trace")]
639pub(crate) async fn copy_dir(src: &Path, dest: &Path) -> Result<(), fs::Error> {
640 if src.exists() {
641 for file in project_files(src) {
642 let relative_src_path: PathBuf = pathdiff::diff_paths(src.join(&file), src)
643 .unwrap_or_else(|| unreachable!("diff_path with self"));
644 let target = dest.join(relative_src_path);
645 if let Some(parent) = target.parent() {
646 fs::tokio::create_dir_all(parent).await?;
647 }
648 fs::tokio::copy(&file, &target).await?;
649 fs::make_writable(&target).await?;
650 }
651 }
652 Ok(())
653}
654
655async fn upgraded_version_req(
656 existing: Option<&str>,
657 dep: &package::Name,
658 package_db: &PackageDB,
659) -> Result<Option<String>, EditError> {
660 if let Some(existing) = existing.and_then(|existing| package::VersionReq::parse(existing).ok())
661 {
662 let latest = package_db
663 .latest_version(dep)
664 .await
665 .ok_or(EditError::LatestVersionNotFound(dep.clone()))?;
666 Ok(latest.upgrade_version_req(&existing))
667 } else {
668 Ok(None)
669 }
670}
671
672async fn upgrade_dependency(
673 table: &mut Item,
674 dep: &package::Name,
675 package_db: &PackageDB,
676 config: &Config,
677) -> Result<(), EditError> {
678 let mut dep_item = table[dep.to_string()].clone();
679 match &dep_item {
680 Item::Value(val) => {
681 if let Some(upgraded) = upgraded_version_req(val.as_str(), dep, package_db).await? {
682 table[dep.to_string()] = toml_edit::value(upgraded);
683 }
684 }
685 Item::Table(tbl) => {
686 if let Some(git_item) = tbl.get("git") {
687 let latest = resolve_git_latest(git_url_from_item(git_item)?, config)?;
688 apply_git_latest(table, dep, latest);
689 } else {
690 if let Some(upgraded) = upgraded_version_req(
691 dep_item.get("version").and_then(Item::as_str),
692 dep,
693 package_db,
694 )
695 .await?
696 {
697 dep_item["version"] = toml_edit::value(upgraded);
698 }
699 table[dep.to_string()] = dep_item;
700 }
701 }
702 _ => {}
703 }
704 Ok(())
705}
706
707fn git_url_from_item(git_item: &Item) -> Result<RemoteGitUrlShorthand, EditError> {
708 let git_value = git_item
709 .clone()
710 .into_value()
711 .map_err(|err| EditError::ExpectedValue(Box::new(err)))?;
712 let git_url_str = git_value
713 .as_str()
714 .ok_or(EditError::ExpectedString(Box::new(git_value.clone())))?;
715 Ok(git_url_str.parse()?)
716}
717
718fn resolve_git_latest(
719 url: RemoteGitUrlShorthand,
720 config: &Config,
721) -> Result<SemVerTagOrSha, EditError> {
722 Ok(git::utils::latest_semver_tag_or_commit_sha(
723 &url.into(),
724 config,
725 )?)
726}
727
728fn apply_git_latest(table: &mut Item, dep: &package::Name, latest: SemVerTagOrSha) {
729 match latest {
730 SemVerTagOrSha::SemVerTag(latest_tag) => {
731 table[dep.to_string()]["version"] = Item::Value(latest_tag.clone().into());
732 if latest_tag.contains("-") {
733 table[dep.to_string()]["rev"] = Item::Value(latest_tag.into());
734 }
735 }
736 SemVerTagOrSha::CommitSha(latest_sha) => {
737 table[dep.to_string()]["version"] = Item::Value(latest_sha.into());
738 }
739 }
740}
741
742fn prepare_dependency_tables(project_toml: &mut DocumentMut) {
743 if !project_toml.contains_table("dependencies") {
744 if let Ok(mut table) = toml_edit::table().into_table() {
745 table.set_implicit(true);
746 project_toml["dependencies"] = toml_edit::Item::Table(table);
747 }
748 }
749 if !project_toml.contains_table("build_dependencies") {
750 if let Ok(mut table) = toml_edit::table().into_table() {
751 table.set_implicit(true);
752 project_toml["build_dependencies"] = toml_edit::Item::Table(table);
753 }
754 }
755 if !project_toml.contains_table("test_dependencies") {
756 if let Ok(mut table) = toml_edit::table().into_table() {
757 table.set_implicit(true);
758 project_toml["test_dependencies"] = toml_edit::Item::Table(table);
759 }
760 }
761 if !project_toml.contains_table("external_dependencies") {
762 if let Ok(mut table) = toml_edit::table().into_table() {
763 table.set_implicit(true);
764 project_toml["external_dependencies"] = toml_edit::Item::Table(table);
765 }
766 }
767}
768
769#[cfg(test)]
771mod tests {
772 use std::collections::HashMap;
773
774 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
775 use url::Url;
776
777 use super::*;
778 use crate::{
779 config::ConfigBuilder,
780 fs,
781 lua_rockspec::ExternalDependencySpec,
782 manifest::{Manifest, ManifestMetadata},
783 package_db::PackageDB,
784 rockspec::Rockspec,
785 };
786
787 #[tokio::test]
788 async fn test_add_various_dependencies() {
789 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
790 .join("resources/test/sample-projects/no-build-spec/");
791 let project_root = assert_fs::TempDir::new().unwrap();
792 project_root.copy_from(&sample_project, &["**"]).unwrap();
793 let project_root: PathBuf = project_root.path().into();
794 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
795 let add_dependencies =
796 [package::Req::new("busted".into(), Some(">= 1.0.0".into())).unwrap()];
797 let expected_dependencies =
798 vec![package::Req::new("busted".into(), Some(">= 1.0.0".into()))
799 .unwrap()
800 .into()];
801
802 let test_manifest_path =
803 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
804 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
805 let metadata = ManifestMetadata::new(&content).unwrap();
806 let config = ConfigBuilder::new().unwrap().build().unwrap();
807 let package_db = PackageDB::from_manifest(
808 Manifest::new(
809 Url::parse("https://example.com").unwrap(),
810 metadata,
811 Default::default(),
812 ),
813 &config,
814 );
815
816 project
817 .add(
818 DependencyType::Regular(add_dependencies.iter().collect_vec()),
819 &package_db,
820 )
821 .await
822 .unwrap();
823
824 project
825 .add(
826 DependencyType::Build(add_dependencies.iter().collect_vec()),
827 &package_db,
828 )
829 .await
830 .unwrap();
831 project
832 .add(
833 DependencyType::Test(add_dependencies.iter().collect_vec()),
834 &package_db,
835 )
836 .await
837 .unwrap();
838
839 project
840 .add(
841 DependencyType::External(HashMap::from([(
842 "lib".into(),
843 ExternalDependencySpec {
844 library: Some("path.so".into()),
845 header: None,
846 },
847 )])),
848 &package_db,
849 )
850 .await
851 .unwrap();
852
853 let project = Project::from_exact(&project_root).unwrap().unwrap();
856 let validated_toml = project.toml().into_remote(None).unwrap();
857
858 assert_eq!(
859 validated_toml.dependencies().current_platform(),
860 &expected_dependencies
861 );
862 assert_eq!(
863 validated_toml.build_dependencies().current_platform(),
864 &expected_dependencies
865 );
866 assert_eq!(
867 validated_toml.test_dependencies().current_platform(),
868 &expected_dependencies
869 );
870 assert_eq!(
871 validated_toml
872 .external_dependencies()
873 .current_platform()
874 .get("lib")
875 .unwrap(),
876 &ExternalDependencySpec {
877 library: Some("path.so".into()),
878 header: None
879 }
880 );
881 }
882
883 #[tokio::test]
884 async fn test_add_unpublished_package_does_not_edit_toml() {
885 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
886 .join("resources/test/sample-projects/no-build-spec/");
887 let project_root = assert_fs::TempDir::new().unwrap();
888 project_root.copy_from(&sample_project, &["**"]).unwrap();
889 let project_root: PathBuf = project_root.path().into();
890 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
891 let toml_before = fs::sync::read_to_string(project.toml_path()).unwrap();
892
893 let test_manifest_path =
894 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
895 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
896 let metadata = ManifestMetadata::new(&content).unwrap();
897 let config = ConfigBuilder::new().unwrap().build().unwrap();
898 let package_db = PackageDB::from_manifest(
899 Manifest::new(
900 Url::parse("https://example.com").unwrap(),
901 metadata,
902 Default::default(),
903 ),
904 &config,
905 );
906
907 let unknown_dependencies =
908 [package::Req::from_str("not-a-package-in-any-manifest").unwrap()];
909 let err = project
910 .add(
911 DependencyType::Regular(unknown_dependencies.iter().collect_vec()),
912 &package_db,
913 )
914 .await
915 .unwrap_err();
916
917 assert!(matches!(err, EditError::LatestVersionNotFound(_)));
918 assert_eq!(
919 fs::sync::read_to_string(project.toml_path()).unwrap(),
920 toml_before
921 );
922 }
923
924 #[tokio::test]
925 async fn test_upgrade_preserves_version_req_style() {
926 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
927 .join("resources/test/sample-projects/no-build-spec/");
928 let project_root = assert_fs::TempDir::new().unwrap();
929 project_root.copy_from(&sample_project, &["**"]).unwrap();
930 let project_root: PathBuf = project_root.path().into();
931 let toml_path = project_root.join("lux.toml");
932
933 let test_manifest_path =
934 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
935 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
936 let metadata = ManifestMetadata::new(&content).unwrap();
937 let config = ConfigBuilder::default().build().unwrap();
938 let package_db = PackageDB::from_manifest(
939 Manifest::new(
940 Url::parse("https://example.com").unwrap(),
941 metadata,
942 Default::default(),
943 ),
944 &config,
945 );
946
947 let base = r#"package = "sample-project-no-build-spec"
948version = "0.1.0"
949lua = ">=5.1"
950
951[source]
952url = "https://github.com/lumen-oss/luarocks-stub"
953
954[build]
955type = "builtin"
956"#;
957 for (input, expected) in [
958 ("30log = \"~> 1.0\"", "30log = \"~> 1.3\""),
959 ("30log = \"~> 1\"", "30log = \"~> 1\""),
960 ("30log = \">= 1.0.0\"", "30log = \">= 1.0.0\""),
961 ("30log = \"1.0.0\"", "30log = \"1.3.0\""),
962 ] {
963 std::fs::write(&toml_path, format!("{base}\n[dependencies]\n{input}\n")).unwrap();
964 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
965 let packages = [package::Name::from("30log")];
966 project
967 .upgrade(
968 LuaDependencyType::Regular(packages.iter().collect()),
969 &package_db,
970 &config,
971 )
972 .await
973 .unwrap();
974 let rewritten = std::fs::read_to_string(&toml_path).unwrap();
975 assert!(rewritten.contains(expected), "input {input}: {rewritten}");
976 }
977 }
978
979 #[tokio::test]
980 async fn test_remove_dependencies() {
981 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
982 .join("resources/test/sample-projects/dependencies/");
983 let project_root = assert_fs::TempDir::new().unwrap();
984 project_root.copy_from(&sample_project, &["**"]).unwrap();
985 let project_root: PathBuf = project_root.path().into();
986 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
987 let lua_cjson = "lua-cjson".into();
988 let plenary_nvim = "plenary.nvim".into();
989 let remove_dependencies = vec![&lua_cjson, &plenary_nvim];
990 project
991 .remove(DependencyType::Regular(remove_dependencies.clone()))
992 .await
993 .unwrap();
994 let check = |project: &Project| {
995 for name in &remove_dependencies {
996 assert!(!project
997 .toml()
998 .dependencies
999 .clone()
1000 .unwrap_or_default()
1001 .iter()
1002 .any(|dep| &dep.name() == name));
1003 }
1004 };
1005 check(&project);
1006 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
1008 check(&reloaded_project);
1009 }
1010
1011 #[tokio::test]
1012 async fn test_extra_rockspec_parsing() {
1013 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1014 .join("resources/test/sample-projects/extra-rockspec/");
1015 let project_root = assert_fs::TempDir::new().unwrap();
1016 project_root.copy_from(&sample_project, &["**"]).unwrap();
1017 let project_root: PathBuf = project_root.path().into();
1018 let project = Project::from_exact(project_root).unwrap().unwrap();
1019
1020 let extra_rockspec = project.extra_rockspec().unwrap();
1021
1022 assert!(extra_rockspec.is_some());
1023
1024 let rocks = project.toml().into_remote(None).unwrap();
1025
1026 assert_eq!(rocks.package().to_string(), "custom-package");
1027 }
1028
1029 #[tokio::test]
1030 async fn test_pin_dependencies() {
1031 test_pin_unpin_dependencies(PinnedState::Pinned).await
1032 }
1033
1034 #[tokio::test]
1035 async fn test_unpin_dependencies() {
1036 test_pin_unpin_dependencies(PinnedState::Unpinned).await
1037 }
1038
1039 async fn test_pin_unpin_dependencies(pin: PinnedState) {
1040 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1041 .join("resources/test/sample-projects/dependencies/");
1042 let project_root = assert_fs::TempDir::new().unwrap();
1043 project_root.copy_from(&sample_project, &["**"]).unwrap();
1044 let project_root: PathBuf = project_root.path().into();
1045 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
1046 let lua_cjson = "lua-cjson".into();
1047 let plenary_nvim = "plenary.nvim".into();
1048 let pin_dependencies = vec![&lua_cjson, &plenary_nvim];
1049 project
1050 .set_pinned_state(LuaDependencyType::Regular(pin_dependencies.clone()), pin)
1051 .await
1052 .unwrap();
1053 let check = |project: &Project| {
1054 for name in &pin_dependencies {
1055 assert!(project
1056 .toml()
1057 .dependencies
1058 .clone()
1059 .unwrap_or_default()
1060 .iter()
1061 .any(|dep| &dep.name() == name && dep.pin == pin));
1062 }
1063 };
1064 check(&project);
1065 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
1067 check(&reloaded_project);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_set_opt_state() {
1072 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1073 .join("resources/test/sample-projects/dependencies/");
1074 let project_root = assert_fs::TempDir::new().unwrap();
1075 project_root.copy_from(&sample_project, &["**"]).unwrap();
1076 let project_root: PathBuf = project_root.path().into();
1077 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
1078 let lua_cjson = "lua-cjson".into();
1079 let plenary_nvim = "plenary.nvim".into();
1080
1081 project
1082 .set_opt_state(
1083 LuaDependencyType::Regular(vec![&lua_cjson]),
1084 OptState::Optional,
1085 )
1086 .await
1087 .unwrap();
1088
1089 let check = |project: &Project, opt: OptState| {
1090 let dependencies = project.toml().dependencies.clone().unwrap_or_default();
1091 assert!(dependencies
1092 .iter()
1093 .any(|dep| dep.name() == &lua_cjson && *dep.opt() == opt));
1094 assert!(dependencies
1095 .iter()
1096 .any(|dep| dep.name() == &plenary_nvim && *dep.opt() == OptState::Required));
1097 };
1098 check(&project, OptState::Optional);
1099 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
1100 check(&reloaded_project, OptState::Optional);
1101
1102 project
1103 .set_opt_state(
1104 LuaDependencyType::Regular(vec![&lua_cjson]),
1105 OptState::Required,
1106 )
1107 .await
1108 .unwrap();
1109 check(&project, OptState::Required);
1110 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
1111 check(&reloaded_project, OptState::Required);
1112 }
1113
1114 #[tokio::test]
1115 async fn project_files_includes_cargo_directory() {
1116 let project_root = assert_fs::TempDir::new().unwrap();
1117 let cargo_dir = project_root.child(".cargo");
1118 cargo_dir.create_dir_all().unwrap();
1119 let cargo_config = cargo_dir.join("config.toml");
1120 fs::tokio::write(&cargo_config, "").await.unwrap();
1121 let project_files = project_files(&project_root);
1122 assert!(project_files.contains(&cargo_config.to_path_buf()));
1123 }
1124
1125 #[tokio::test]
1126 async fn project_files_ignores_parent_ignore_files() {
1127 let tmp = assert_fs::TempDir::new().unwrap();
1128 let parent = tmp.child("parent");
1129 parent.create_dir_all().unwrap();
1130 fs::tokio::write(parent.join(".ignore"), "*\n")
1131 .await
1132 .unwrap();
1133 let src = parent.child("etc");
1134 src.create_dir_all().unwrap();
1135 let file = src.join("payload.txt");
1136 fs::tokio::write(&file, "hello").await.unwrap();
1137
1138 let project_files = project_files(&src);
1139 assert!(project_files.contains(&file.to_path_buf()));
1140 }
1141
1142 #[tokio::test]
1143 async fn project_root_dir_override() {
1144 let root_dir = assert_fs::TempDir::new().unwrap();
1145 let lux_toml_dir = root_dir.child("proj");
1146 lux_toml_dir.create_dir_all().unwrap();
1147 fs::tokio::write(
1148 lux_toml_dir.join(PROJECT_TOML),
1149 r#"
1150 package = "my-package"
1151 version = "1.0.0"
1152 lua = "5.1"
1153
1154 [project]
1155 root_dir = ".."
1156
1157 [build]
1158 type = "builtin"
1159 "#,
1160 )
1161 .await
1162 .unwrap();
1163 let project = Project::from_exact(lux_toml_dir.path()).unwrap().unwrap();
1164 assert_eq!(
1165 std::fs::canonicalize(project.root()).unwrap(),
1166 std::fs::canonicalize(root_dir.path()).unwrap()
1167 );
1168 assert_eq!(project.toml_path(), lux_toml_dir.join(PROJECT_TOML));
1170 }
1171
1172 #[tokio::test]
1173 async fn from_exact_on_nonexistent_directory() {
1174 let empty_dir = assert_fs::TempDir::new().unwrap();
1175 assert!(Project::from_exact(&empty_dir).unwrap().is_none());
1176 }
1177}