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 package_db::PackageDB,
33 rockspec::{
34 lua_dependency::{DependencyType, LuaDependencySpec, LuaDependencyType},
35 LuaVersionCompatibility,
36 },
37};
38use crate::{
39 lockfile::PinnedState,
40 package::{PackageName, PackageReq, PackageVersionReq},
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 local_remote_rockspec(&self) -> Result<RemoteLuaRockspec, IntoLocalRockspecError> {
291 self.toml()
292 .into_local()
293 .map_err(|err| IntoLocalRockspecError::LocalProjectTomlValidationError(Box::new(err)))?
294 .to_local_rockspec()
295 .map_err(|err| IntoLocalRockspecError::RockspecError(Box::new(err)))
296 }
297
298 pub fn remote_rockspec(
299 &self,
300 specrev: Option<SpecRev>,
301 ) -> Result<RemoteLuaRockspec, IntoRemoteRockspecError> {
302 self.toml()
303 .into_remote(specrev)
304 .map_err(|err| IntoRemoteRockspecError::RocksTomlValidationError(Box::new(err)))?
305 .to_lua_rockspec()
306 .map_err(|err| IntoRemoteRockspecError::RockspecError(Box::new(err)))
307 }
308
309 pub fn extra_rockspec(&self) -> Result<Option<PartialLuaRockspec>, PartialRockspecError> {
310 if self.extra_rockspec_path().exists() {
311 Ok(Some(PartialLuaRockspec::new(
312 &fs::sync::read_to_string(self.extra_rockspec_path()).map_err(io::Error::other)?,
313 )?))
314 } else {
315 Ok(None)
316 }
317 }
318
319 pub fn lua_version(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
320 self.toml().lua_version_matches(config)
321 }
322
323 pub async fn add(
324 &mut self,
325 dependencies: DependencyType<&PackageReq>,
326 package_db: &PackageDB,
327 ) -> Result<(), ProjectEditError> {
328 let mut project_toml =
329 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
330
331 prepare_dependency_tables(&mut project_toml);
332 let table = match dependencies {
333 DependencyType::Regular(_) => &mut project_toml["dependencies"],
334 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
335 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
336 DependencyType::External(_) => &mut project_toml["external_dependencies"],
337 };
338
339 match dependencies {
340 DependencyType::Regular(ref deps)
341 | DependencyType::Build(ref deps)
342 | DependencyType::Test(ref deps) => {
343 for dep in deps {
344 let dep_version_str = if dep.version_req().is_any() {
345 let latest_version =
346 package_db.latest_version(dep.name()).await.ok_or_else(|| {
347 ProjectEditError::LatestVersionNotFound(dep.name().clone())
348 })?;
349 latest_version
350 .to_pessimistic_version_req()
351 .unwrap_or_else(|| latest_version.to_string())
352 } else {
353 dep.version_req().to_string()
354 };
355 table[dep.name().to_string()] = toml_edit::value(dep_version_str);
356 }
357 }
358 DependencyType::External(ref deps) => {
359 for (name, dep) in deps {
360 if let Some(path) = &dep.header {
361 table[name]["header"] = toml_edit::value(path.to_slash_lossy().to_string());
362 }
363 if let Some(path) = &dep.library {
364 table[name]["library"] =
365 toml_edit::value(path.to_slash_lossy().to_string());
366 }
367 }
368 }
369 };
370
371 let toml_content = project_toml.to_string();
372 fs::tokio::write(self.toml_path(), &toml_content).await?;
373 self.toml = PartialProjectToml::new(
374 self.toml_path().to_str().unwrap_or("<lux.toml>"),
375 &toml_content,
376 self.lux_toml_dir.clone(),
377 )?;
378
379 Ok(())
380 }
381
382 pub async fn add_git(
383 &mut self,
384 dependencies: LuaDependencyType<&RemoteGitUrlShorthand>,
385 config: &Config,
386 ) -> Result<(), ProjectEditError> {
387 let mut project_toml =
388 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
389
390 prepare_dependency_tables(&mut project_toml);
391 let table = match dependencies {
392 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
393 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
394 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
395 };
396
397 match dependencies {
398 LuaDependencyType::Regular(urls)
399 | LuaDependencyType::Build(urls)
400 | LuaDependencyType::Test(urls) => {
401 for url in urls {
402 let git_url: RemoteGitUrl = url.clone().into();
403 let mut dep_entry = toml_edit::table();
404 match git::utils::latest_semver_tag_or_commit_sha(&git_url, config)? {
405 SemVerTagOrSha::SemVerTag(tag) => {
406 dep_entry["git"] = Item::Value(url.to_string().into());
407 dep_entry["version"] = Item::Value(tag.clone().into());
408 if tag.contains("-") {
409 dep_entry["rev"] = Item::Value(tag.into());
411 }
412 }
413 SemVerTagOrSha::CommitSha(sha) => {
414 dep_entry["git"] = Item::Value(url.to_string().into());
415 dep_entry["version"] = Item::Value(sha.into());
416 }
417 }
418 table[git_url.repo()] = dep_entry;
419 }
420 }
421 }
422
423 let toml_content = project_toml.to_string();
424 fs::tokio::write(self.toml_path(), &toml_content).await?;
425 self.toml = PartialProjectToml::new(
426 self.toml_path().to_str().unwrap_or("<lux.toml>"),
427 &toml_content,
428 self.lux_toml_dir.clone(),
429 )?;
430
431 Ok(())
432 }
433
434 pub async fn remove(
435 &mut self,
436 dependencies: DependencyType<&PackageName>,
437 ) -> Result<(), ProjectEditError> {
438 let mut project_toml =
439 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
440
441 prepare_dependency_tables(&mut project_toml);
442 let table = match dependencies {
443 DependencyType::Regular(_) => &mut project_toml["dependencies"],
444 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
445 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
446 DependencyType::External(_) => &mut project_toml["external_dependencies"],
447 };
448
449 match dependencies {
450 DependencyType::Regular(ref deps)
451 | DependencyType::Build(ref deps)
452 | DependencyType::Test(ref deps) => {
453 for dep in deps {
454 table[dep.to_string()] = Item::None;
455 }
456 }
457 DependencyType::External(ref deps) => {
458 for (name, dep) in deps {
459 if dep.header.is_some() {
460 table[name]["header"] = Item::None;
461 }
462 if dep.library.is_some() {
463 table[name]["library"] = Item::None;
464 }
465 }
466 }
467 };
468
469 let toml_content = project_toml.to_string();
470 fs::tokio::write(self.toml_path(), &toml_content).await?;
471 self.toml = PartialProjectToml::new(
472 self.toml_path().to_str().unwrap_or("<lux.toml>"),
473 &toml_content,
474 self.lux_toml_dir.clone(),
475 )?;
476
477 Ok(())
478 }
479
480 pub async fn upgrade(
481 &mut self,
482 dependencies: LuaDependencyType<&PackageName>,
483 package_db: &PackageDB,
484 config: &Config,
485 ) -> Result<(), ProjectEditError> {
486 let mut project_toml =
487 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)?;
488
489 prepare_dependency_tables(&mut project_toml);
490 let table = match dependencies {
491 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
492 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
493 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
494 };
495
496 match dependencies {
497 LuaDependencyType::Regular(deps)
498 | LuaDependencyType::Build(deps)
499 | LuaDependencyType::Test(deps) => {
500 for dep in deps {
501 upgrade_dependency(table, dep, package_db, config).await?;
502 }
503 }
504 }
505
506 let toml_content = project_toml.to_string();
507 fs::tokio::write(self.toml_path(), &toml_content).await?;
508 self.toml = PartialProjectToml::new(
509 self.toml_path().to_str().unwrap_or("<lux.toml>"),
510 &toml_content,
511 self.lux_toml_dir.clone(),
512 )?;
513
514 Ok(())
515 }
516
517 pub async fn upgrade_all(
518 &mut self,
519 package_db: &PackageDB,
520 config: &Config,
521 ) -> Result<(), ProjectEditError> {
522 if let Some(dependencies) = &self.toml().dependencies {
523 let packages = dependencies
524 .iter()
525 .map(|dep| dep.name())
526 .cloned()
527 .collect_vec();
528 self.upgrade(
529 LuaDependencyType::Regular(packages.iter().collect()),
530 package_db,
531 config,
532 )
533 .await?;
534 }
535 if let Some(dependencies) = &self.toml().build_dependencies {
536 let packages = dependencies
537 .iter()
538 .map(|dep| dep.name())
539 .cloned()
540 .collect_vec();
541 self.upgrade(
542 LuaDependencyType::Build(packages.iter().collect()),
543 package_db,
544 config,
545 )
546 .await?;
547 }
548 if let Some(dependencies) = &self.toml().test_dependencies {
549 let packages = dependencies
550 .iter()
551 .map(|dep| dep.name())
552 .cloned()
553 .collect_vec();
554 self.upgrade(
555 LuaDependencyType::Test(packages.iter().collect()),
556 package_db,
557 config,
558 )
559 .await?;
560 }
561 Ok(())
562 }
563
564 pub async fn set_pinned_state(
565 &mut self,
566 dependencies: LuaDependencyType<&PackageName>,
567 pin: PinnedState,
568 ) -> Result<(), PinError> {
569 let mut project_toml =
570 toml_edit::DocumentMut::from_str(&fs::tokio::read_to_string(self.toml_path()).await?)
571 .map_err(|source| PinError::ParseTomlEdit {
572 toml_path: self.toml_path().to_slash_lossy().to_string(),
573 source,
574 })?;
575
576 prepare_dependency_tables(&mut project_toml);
577 let table = match dependencies {
578 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
579 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
580 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
581 };
582
583 match dependencies {
584 LuaDependencyType::Regular(ref _deps) => {
585 self.toml.dependencies = Some(
586 self.toml
587 .dependencies
588 .take()
589 .unwrap_or_default()
590 .into_iter()
591 .map(|dep| LuaDependencySpec { pin, ..dep })
592 .collect(),
593 )
594 }
595 LuaDependencyType::Build(ref _deps) => {
596 self.toml.build_dependencies = Some(
597 self.toml
598 .build_dependencies
599 .take()
600 .unwrap_or_default()
601 .into_iter()
602 .map(|dep| LuaDependencySpec { pin, ..dep })
603 .collect(),
604 )
605 }
606 LuaDependencyType::Test(ref _deps) => {
607 self.toml.test_dependencies = Some(
608 self.toml
609 .test_dependencies
610 .take()
611 .unwrap_or_default()
612 .into_iter()
613 .map(|dep| LuaDependencySpec { pin, ..dep })
614 .collect(),
615 )
616 }
617 }
618
619 match dependencies {
620 LuaDependencyType::Regular(ref deps)
621 | LuaDependencyType::Build(ref deps)
622 | LuaDependencyType::Test(ref deps) => {
623 for dep in deps {
624 let mut dep_item = table[dep.to_string()].clone();
625 match dep_item {
626 version @ Item::Value(_) => match &pin {
627 PinnedState::Unpinned => {}
628 PinnedState::Pinned => {
629 if let Ok(mut dep_entry) = toml_edit::table().into_table() {
630 dep_entry.set_implicit(true);
631 dep_entry["version"] = version;
632 dep_entry["pin"] = toml_edit::value(true);
633 table[dep.to_string()] = toml_edit::Item::Table(dep_entry);
634 }
635 }
636 },
637 Item::Table(_) => {
638 dep_item["pin".to_string()] = toml_edit::value(pin.as_bool());
639 table[dep.to_string()] = dep_item;
640 }
641 _ => {}
642 }
643 }
644 }
645 }
646
647 let toml_content = project_toml.to_string();
648 fs::tokio::write(self.toml_path(), &toml_content).await?;
649 self.toml = PartialProjectToml::new(
650 self.toml_path().to_str().unwrap_or("<lux.toml>"),
651 &toml_content,
652 self.lux_toml_dir.clone(),
653 )?;
654
655 Ok(())
656 }
657
658 pub fn project_files(&self) -> Vec<PathBuf> {
659 project_files(&self.root().0)
660 }
661}
662
663pub(crate) fn project_files(src: &Path) -> Vec<PathBuf> {
666 ignore::WalkBuilder::new(src)
667 .add(src.join(".cargo"))
668 .parents(false)
669 .follow_links(false)
670 .build()
671 .filter_map(Result::ok)
672 .filter(|entry| entry.file_type().is_some_and(|ft| ft.is_file()))
673 .map(|entry| entry.into_path())
674 .collect_vec()
675}
676
677async fn upgraded_version_req(
678 existing: Option<&str>,
679 dep: &PackageName,
680 package_db: &PackageDB,
681) -> Result<Option<String>, ProjectEditError> {
682 if let Some(existing) = existing.and_then(|existing| PackageVersionReq::parse(existing).ok()) {
683 let latest = package_db
684 .latest_version(dep)
685 .await
686 .ok_or(ProjectEditError::LatestVersionNotFound(dep.clone()))?;
687 Ok(latest.upgrade_version_req(&existing))
688 } else {
689 Ok(None)
690 }
691}
692
693async fn upgrade_dependency(
694 table: &mut Item,
695 dep: &PackageName,
696 package_db: &PackageDB,
697 config: &Config,
698) -> Result<(), ProjectEditError> {
699 let mut dep_item = table[dep.to_string()].clone();
700 match &dep_item {
701 Item::Value(val) => {
702 if let Some(upgraded) = upgraded_version_req(val.as_str(), dep, package_db).await? {
703 table[dep.to_string()] = toml_edit::value(upgraded);
704 }
705 }
706 Item::Table(tbl) => {
707 if let Some(git_item) = tbl.get("git") {
708 let latest = resolve_git_latest(git_url_from_item(git_item)?, config)?;
709 apply_git_latest(table, dep, latest);
710 } else {
711 if let Some(upgraded) = upgraded_version_req(
712 dep_item.get("version").and_then(Item::as_str),
713 dep,
714 package_db,
715 )
716 .await?
717 {
718 dep_item["version"] = toml_edit::value(upgraded);
719 }
720 table[dep.to_string()] = dep_item;
721 }
722 }
723 _ => {}
724 }
725 Ok(())
726}
727
728fn git_url_from_item(git_item: &Item) -> Result<RemoteGitUrlShorthand, ProjectEditError> {
729 let git_value = git_item
730 .clone()
731 .into_value()
732 .map_err(|err| ProjectEditError::ExpectedValue(Box::new(err)))?;
733 let git_url_str = git_value
734 .as_str()
735 .ok_or(ProjectEditError::ExpectedString(Box::new(
736 git_value.clone(),
737 )))?;
738 Ok(git_url_str.parse()?)
739}
740
741fn resolve_git_latest(
742 url: RemoteGitUrlShorthand,
743 config: &Config,
744) -> Result<SemVerTagOrSha, ProjectEditError> {
745 Ok(git::utils::latest_semver_tag_or_commit_sha(
746 &url.into(),
747 config,
748 )?)
749}
750
751fn apply_git_latest(table: &mut Item, dep: &PackageName, latest: SemVerTagOrSha) {
752 match latest {
753 SemVerTagOrSha::SemVerTag(latest_tag) => {
754 table[dep.to_string()]["version"] = Item::Value(latest_tag.clone().into());
755 if latest_tag.contains("-") {
756 table[dep.to_string()]["rev"] = Item::Value(latest_tag.into());
757 }
758 }
759 SemVerTagOrSha::CommitSha(latest_sha) => {
760 table[dep.to_string()]["version"] = Item::Value(latest_sha.into());
761 }
762 }
763}
764
765fn prepare_dependency_tables(project_toml: &mut DocumentMut) {
766 if !project_toml.contains_table("dependencies") {
767 if let Ok(mut table) = toml_edit::table().into_table() {
768 table.set_implicit(true);
769 project_toml["dependencies"] = toml_edit::Item::Table(table);
770 }
771 }
772 if !project_toml.contains_table("build_dependencies") {
773 if let Ok(mut table) = toml_edit::table().into_table() {
774 table.set_implicit(true);
775 project_toml["build_dependencies"] = toml_edit::Item::Table(table);
776 }
777 }
778 if !project_toml.contains_table("test_dependencies") {
779 if let Ok(mut table) = toml_edit::table().into_table() {
780 table.set_implicit(true);
781 project_toml["test_dependencies"] = toml_edit::Item::Table(table);
782 }
783 }
784 if !project_toml.contains_table("external_dependencies") {
785 if let Ok(mut table) = toml_edit::table().into_table() {
786 table.set_implicit(true);
787 project_toml["external_dependencies"] = toml_edit::Item::Table(table);
788 }
789 }
790}
791
792#[cfg(test)]
794mod tests {
795 use std::collections::HashMap;
796
797 use assert_fs::prelude::{PathChild, PathCopy, PathCreateDir};
798 use url::Url;
799
800 use super::*;
801 use crate::{
802 config::ConfigBuilder,
803 fs,
804 lua_rockspec::ExternalDependencySpec,
805 manifest::{Manifest, ManifestMetadata},
806 package::PackageReq,
807 package_db::PackageDB,
808 rockspec::Rockspec,
809 };
810
811 #[tokio::test]
812 async fn test_add_various_dependencies() {
813 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
814 .join("resources/test/sample-projects/no-build-spec/");
815 let project_root = assert_fs::TempDir::new().unwrap();
816 project_root.copy_from(&sample_project, &["**"]).unwrap();
817 let project_root: PathBuf = project_root.path().into();
818 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
819 let add_dependencies = [PackageReq::new("busted".into(), Some(">= 1.0.0".into())).unwrap()];
820 let expected_dependencies = vec![PackageReq::new("busted".into(), Some(">= 1.0.0".into()))
821 .unwrap()
822 .into()];
823
824 let test_manifest_path =
825 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
826 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
827 let metadata = ManifestMetadata::new(&content).unwrap();
828 let config = ConfigBuilder::new().unwrap().build().unwrap();
829 let package_db = PackageDB::from_manifest(
830 Manifest::new(
831 Url::parse("https://example.com").unwrap(),
832 metadata,
833 Default::default(),
834 ),
835 &config,
836 );
837
838 project
839 .add(
840 DependencyType::Regular(add_dependencies.iter().collect_vec()),
841 &package_db,
842 )
843 .await
844 .unwrap();
845
846 project
847 .add(
848 DependencyType::Build(add_dependencies.iter().collect_vec()),
849 &package_db,
850 )
851 .await
852 .unwrap();
853 project
854 .add(
855 DependencyType::Test(add_dependencies.iter().collect_vec()),
856 &package_db,
857 )
858 .await
859 .unwrap();
860
861 project
862 .add(
863 DependencyType::External(HashMap::from([(
864 "lib".into(),
865 ExternalDependencySpec {
866 library: Some("path.so".into()),
867 header: None,
868 },
869 )])),
870 &package_db,
871 )
872 .await
873 .unwrap();
874
875 let project = Project::from_exact(&project_root).unwrap().unwrap();
878 let validated_toml = project.toml().into_remote(None).unwrap();
879
880 assert_eq!(
881 validated_toml.dependencies().current_platform(),
882 &expected_dependencies
883 );
884 assert_eq!(
885 validated_toml.build_dependencies().current_platform(),
886 &expected_dependencies
887 );
888 assert_eq!(
889 validated_toml.test_dependencies().current_platform(),
890 &expected_dependencies
891 );
892 assert_eq!(
893 validated_toml
894 .external_dependencies()
895 .current_platform()
896 .get("lib")
897 .unwrap(),
898 &ExternalDependencySpec {
899 library: Some("path.so".into()),
900 header: None
901 }
902 );
903 }
904
905 #[tokio::test]
906 async fn test_add_unpublished_package_does_not_edit_toml() {
907 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
908 .join("resources/test/sample-projects/no-build-spec/");
909 let project_root = assert_fs::TempDir::new().unwrap();
910 project_root.copy_from(&sample_project, &["**"]).unwrap();
911 let project_root: PathBuf = project_root.path().into();
912 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
913 let toml_before = fs::sync::read_to_string(project.toml_path()).unwrap();
914
915 let test_manifest_path =
916 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
917 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
918 let metadata = ManifestMetadata::new(&content).unwrap();
919 let config = ConfigBuilder::new().unwrap().build().unwrap();
920 let package_db = PackageDB::from_manifest(
921 Manifest::new(
922 Url::parse("https://example.com").unwrap(),
923 metadata,
924 Default::default(),
925 ),
926 &config,
927 );
928
929 let unknown_dependencies = [PackageReq::from_str("not-a-package-in-any-manifest").unwrap()];
930 let err = project
931 .add(
932 DependencyType::Regular(unknown_dependencies.iter().collect_vec()),
933 &package_db,
934 )
935 .await
936 .unwrap_err();
937
938 assert!(matches!(err, ProjectEditError::LatestVersionNotFound(_)));
939 assert_eq!(
940 fs::sync::read_to_string(project.toml_path()).unwrap(),
941 toml_before
942 );
943 }
944
945 #[tokio::test]
946 async fn test_upgrade_preserves_version_req_style() {
947 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
948 .join("resources/test/sample-projects/no-build-spec/");
949 let project_root = assert_fs::TempDir::new().unwrap();
950 project_root.copy_from(&sample_project, &["**"]).unwrap();
951 let project_root: PathBuf = project_root.path().into();
952 let toml_path = project_root.join("lux.toml");
953
954 let test_manifest_path =
955 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
956 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
957 let metadata = ManifestMetadata::new(&content).unwrap();
958 let config = ConfigBuilder::default().build().unwrap();
959 let package_db = PackageDB::from_manifest(
960 Manifest::new(
961 Url::parse("https://example.com").unwrap(),
962 metadata,
963 Default::default(),
964 ),
965 &config,
966 );
967
968 let base = r#"package = "sample-project-no-build-spec"
969version = "0.1.0"
970lua = ">=5.1"
971
972[source]
973url = "https://github.com/lumen-oss/luarocks-stub"
974
975[build]
976type = "builtin"
977"#;
978 for (input, expected) in [
979 ("30log = \"~> 1.0\"", "30log = \"~> 1.3\""),
980 ("30log = \"~> 1\"", "30log = \"~> 1\""),
981 ("30log = \">= 1.0.0\"", "30log = \">= 1.0.0\""),
982 ("30log = \"1.0.0\"", "30log = \"1.3.0\""),
983 ] {
984 std::fs::write(&toml_path, format!("{base}\n[dependencies]\n{input}\n")).unwrap();
985 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
986 let packages = [PackageName::from("30log")];
987 project
988 .upgrade(
989 LuaDependencyType::Regular(packages.iter().collect()),
990 &package_db,
991 &config,
992 )
993 .await
994 .unwrap();
995 let rewritten = std::fs::read_to_string(&toml_path).unwrap();
996 assert!(rewritten.contains(expected), "input {input}: {rewritten}");
997 }
998 }
999
1000 #[tokio::test]
1001 async fn test_remove_dependencies() {
1002 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1003 .join("resources/test/sample-projects/dependencies/");
1004 let project_root = assert_fs::TempDir::new().unwrap();
1005 project_root.copy_from(&sample_project, &["**"]).unwrap();
1006 let project_root: PathBuf = project_root.path().into();
1007 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
1008 let lua_cjson = "lua-cjson".into();
1009 let plenary_nvim = "plenary.nvim".into();
1010 let remove_dependencies = vec![&lua_cjson, &plenary_nvim];
1011 project
1012 .remove(DependencyType::Regular(remove_dependencies.clone()))
1013 .await
1014 .unwrap();
1015 let check = |project: &Project| {
1016 for name in &remove_dependencies {
1017 assert!(!project
1018 .toml()
1019 .dependencies
1020 .clone()
1021 .unwrap_or_default()
1022 .iter()
1023 .any(|dep| &dep.name() == name));
1024 }
1025 };
1026 check(&project);
1027 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
1029 check(&reloaded_project);
1030 }
1031
1032 #[tokio::test]
1033 async fn test_extra_rockspec_parsing() {
1034 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1035 .join("resources/test/sample-projects/extra-rockspec/");
1036 let project_root = assert_fs::TempDir::new().unwrap();
1037 project_root.copy_from(&sample_project, &["**"]).unwrap();
1038 let project_root: PathBuf = project_root.path().into();
1039 let project = Project::from_exact(project_root).unwrap().unwrap();
1040
1041 let extra_rockspec = project.extra_rockspec().unwrap();
1042
1043 assert!(extra_rockspec.is_some());
1044
1045 let rocks = project.toml().into_remote(None).unwrap();
1046
1047 assert_eq!(rocks.package().to_string(), "custom-package");
1048 }
1049
1050 #[tokio::test]
1051 async fn test_pin_dependencies() {
1052 test_pin_unpin_dependencies(PinnedState::Pinned).await
1053 }
1054
1055 #[tokio::test]
1056 async fn test_unpin_dependencies() {
1057 test_pin_unpin_dependencies(PinnedState::Unpinned).await
1058 }
1059
1060 async fn test_pin_unpin_dependencies(pin: PinnedState) {
1061 let sample_project = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1062 .join("resources/test/sample-projects/dependencies/");
1063 let project_root = assert_fs::TempDir::new().unwrap();
1064 project_root.copy_from(&sample_project, &["**"]).unwrap();
1065 let project_root: PathBuf = project_root.path().into();
1066 let mut project = Project::from_exact(&project_root).unwrap().unwrap();
1067 let lua_cjson = "lua-cjson".into();
1068 let plenary_nvim = "plenary.nvim".into();
1069 let pin_dependencies = vec![&lua_cjson, &plenary_nvim];
1070 project
1071 .set_pinned_state(LuaDependencyType::Regular(pin_dependencies.clone()), pin)
1072 .await
1073 .unwrap();
1074 let check = |project: &Project| {
1075 for name in &pin_dependencies {
1076 assert!(project
1077 .toml()
1078 .dependencies
1079 .clone()
1080 .unwrap_or_default()
1081 .iter()
1082 .any(|dep| &dep.name() == name && dep.pin == pin));
1083 }
1084 };
1085 check(&project);
1086 let reloaded_project = Project::from_exact(&project_root).unwrap().unwrap();
1088 check(&reloaded_project);
1089 }
1090
1091 #[tokio::test]
1092 async fn project_files_includes_cargo_directory() {
1093 let project_root = assert_fs::TempDir::new().unwrap();
1094 let cargo_dir = project_root.child(".cargo");
1095 cargo_dir.create_dir_all().unwrap();
1096 let cargo_config = cargo_dir.join("config.toml");
1097 fs::tokio::write(&cargo_config, "").await.unwrap();
1098 let project_files = project_files(&project_root);
1099 assert!(project_files.contains(&cargo_config.to_path_buf()));
1100 }
1101
1102 #[tokio::test]
1103 async fn project_files_ignores_parent_ignore_files() {
1104 let tmp = assert_fs::TempDir::new().unwrap();
1105 let parent = tmp.child("parent");
1106 parent.create_dir_all().unwrap();
1107 fs::tokio::write(parent.join(".ignore"), "*\n")
1108 .await
1109 .unwrap();
1110 let src = parent.child("etc");
1111 src.create_dir_all().unwrap();
1112 let file = src.join("payload.txt");
1113 fs::tokio::write(&file, "hello").await.unwrap();
1114
1115 let project_files = project_files(&src);
1116 assert!(project_files.contains(&file.to_path_buf()));
1117 }
1118
1119 #[tokio::test]
1120 async fn project_root_dir_override() {
1121 let root_dir = assert_fs::TempDir::new().unwrap();
1122 let lux_toml_dir = root_dir.child("proj");
1123 lux_toml_dir.create_dir_all().unwrap();
1124 fs::tokio::write(
1125 lux_toml_dir.join(PROJECT_TOML),
1126 r#"
1127 package = "my-package"
1128 version = "1.0.0"
1129 lua = "5.1"
1130
1131 [project]
1132 root_dir = ".."
1133
1134 [build]
1135 type = "builtin"
1136 "#,
1137 )
1138 .await
1139 .unwrap();
1140 let project = Project::from_exact(lux_toml_dir.path()).unwrap().unwrap();
1141 assert_eq!(
1142 std::fs::canonicalize(project.root()).unwrap(),
1143 std::fs::canonicalize(root_dir.path()).unwrap()
1144 );
1145 assert_eq!(project.toml_path(), lux_toml_dir.join(PROJECT_TOML));
1147 }
1148
1149 #[tokio::test]
1150 async fn from_exact_on_nonexistent_directory() {
1151 let empty_dir = assert_fs::TempDir::new().unwrap();
1152 assert!(Project::from_exact(&empty_dir).unwrap().is_none());
1153 }
1154}