Skip to main content

lux_lib/project/
mod.rs

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