lux_lib/project/
mod.rs

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