Skip to main content

lux_lib/project/
mod.rs

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