Skip to main content

lux_lib/project/
mod.rs

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