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