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