Skip to main content

mlua_pkg/
project.rs

1//! Project context for the package-manager operations in [`crate::ops`].
2//!
3//! Two small value types tie an operation to the filesystem:
4//!
5//! | Type | Owns | Decided by |
6//! |------|------|------------|
7//! | [`PkgDir`] | the base directory that holds `cache/` and `vendored/` | the caller (CLI flag / env / SDK constructor) |
8//! | [`Project`] | manifest path, lockfile path, and a [`PkgDir`] | the caller |
9//!
10//! The library never inspects the current working directory or environment
11//! to pick these paths.  The CLI resolves its `--mlua-pkgs-dir` / `MLUA_PKG_DIR`
12//! / `target/` precedence and hands the result in as a single [`PkgDir`];
13//! an embedding application does the same with whatever policy it prefers.
14
15use std::path::{Path, PathBuf};
16
17/// Default base-directory name used when nothing else is configured.
18pub const DEFAULT_PKG_DIR_NAME: &str = ".mlua-pkgs";
19
20/// Manifest filename inside a project root.
21pub const MANIFEST_FILE_NAME: &str = "mlua-pkg.toml";
22
23/// Lockfile filename inside a project root.
24pub const LOCKFILE_FILE_NAME: &str = "mlua-pkg.lock";
25
26/// Base directory for cache + vendored output.
27///
28/// This is the single place that knows the internal layout:
29///
30/// ```text
31/// <base>/
32///   cache/      git clones, keyed by host/org/repo/sha  (GitFetcher root)
33///   vendored/   <name> -> ../cache/…/<entry> symlinks    (VendoredResolver root)
34/// ```
35///
36/// Callers pass one path; [`cache`](Self::cache) and
37/// [`vendored`](Self::vendored) derive the rest so no consumer has to
38/// replicate the `join("cache")` / `join("vendored")` convention.
39///
40/// # Example
41///
42/// ```rust
43/// use mlua_pkg::PkgDir;
44/// use std::path::Path;
45///
46/// let dir = PkgDir::new(".mlua-pkgs");
47/// assert_eq!(dir.cache(), Path::new(".mlua-pkgs/cache"));
48/// assert_eq!(dir.vendored(), Path::new(".mlua-pkgs/vendored"));
49/// ```
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct PkgDir {
52    base: PathBuf,
53}
54
55impl PkgDir {
56    /// Wrap an explicit base directory.  No filesystem access.
57    pub fn new(base: impl Into<PathBuf>) -> Self {
58        Self { base: base.into() }
59    }
60
61    /// `<root>/.mlua-pkgs` — the conventional location under a project root.
62    pub fn default_in(root: impl AsRef<Path>) -> Self {
63        Self::new(root.as_ref().join(DEFAULT_PKG_DIR_NAME))
64    }
65
66    /// The base directory as given.
67    pub fn base(&self) -> &Path {
68        &self.base
69    }
70
71    /// `<base>/cache` — root handed to [`crate::fetcher::GitFetcher`].
72    pub fn cache(&self) -> PathBuf {
73        self.base.join("cache")
74    }
75
76    /// `<base>/vendored` — root handed to
77    /// [`crate::resolvers::VendoredResolver`].
78    pub fn vendored(&self) -> PathBuf {
79        self.base.join("vendored")
80    }
81
82    /// `<base>/vendored/<name>` — the symlink `install` creates for a
83    /// package without `target_dir`.  It points at the **package root**
84    /// (upstream cache checkout or `patch_dir`); the `require` root inside
85    /// it is `<root>/<entry>` with `entry` taken from the lockfile
86    /// ([`LockedPkg::require_dir`](crate::lockfile::LockedPkg::require_dir)).
87    pub fn vendored_root(&self, name: &str) -> PathBuf {
88        self.vendored().join(name)
89    }
90}
91
92impl<T: Into<PathBuf>> From<T> for PkgDir {
93    fn from(p: T) -> Self {
94        Self::new(p)
95    }
96}
97
98/// Everything an operation in [`crate::ops`] needs to locate files.
99///
100/// Built once by the caller and passed by reference to `install` / `add` /
101/// `update` / `clean`.  Paths are used as given (relative paths resolve
102/// against the process working directory, as with any `std::fs` call).
103///
104/// # Example
105///
106/// ```rust
107/// use mlua_pkg::{PkgDir, Project};
108/// use std::path::Path;
109///
110/// // Conventional layout: <root>/mlua-pkg.toml, <root>/mlua-pkg.lock,
111/// // <root>/.mlua-pkgs/{cache,vendored}
112/// let p = Project::in_dir("/srv/app", PkgDir::default_in("/srv/app"));
113/// assert_eq!(p.manifest_path(), Path::new("/srv/app/mlua-pkg.toml"));
114/// assert_eq!(p.lock_path(), Path::new("/srv/app/mlua-pkg.lock"));
115/// assert_eq!(p.pkg_dir().vendored(), Path::new("/srv/app/.mlua-pkgs/vendored"));
116/// ```
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Project {
119    manifest_path: PathBuf,
120    lock_path: PathBuf,
121    pkg_dir: PkgDir,
122}
123
124impl Project {
125    /// Fully explicit constructor.
126    pub fn new(
127        manifest_path: impl Into<PathBuf>,
128        lock_path: impl Into<PathBuf>,
129        pkg_dir: impl Into<PkgDir>,
130    ) -> Self {
131        Self {
132            manifest_path: manifest_path.into(),
133            lock_path: lock_path.into(),
134            pkg_dir: pkg_dir.into(),
135        }
136    }
137
138    /// Conventional filenames under `root`, with an explicit [`PkgDir`].
139    ///
140    /// `manifest = <root>/mlua-pkg.toml`, `lock = <root>/mlua-pkg.lock`.
141    pub fn in_dir(root: impl AsRef<Path>, pkg_dir: impl Into<PkgDir>) -> Self {
142        let root = root.as_ref();
143        Self::new(
144            root.join(MANIFEST_FILE_NAME),
145            root.join(LOCKFILE_FILE_NAME),
146            pkg_dir,
147        )
148    }
149
150    /// Path to `mlua-pkg.toml`.
151    pub fn manifest_path(&self) -> &Path {
152        &self.manifest_path
153    }
154
155    /// Path to `mlua-pkg.lock`.
156    pub fn lock_path(&self) -> &Path {
157        &self.lock_path
158    }
159
160    /// Base directory for cache + vendored output.
161    pub fn pkg_dir(&self) -> &PkgDir {
162        &self.pkg_dir
163    }
164
165    /// Directory that contains the manifest.
166    ///
167    /// `Dep::target_dir` values are resolved relative to this.  A bare
168    /// filename (`"mlua-pkg.toml"`) yields `"."`.
169    pub fn manifest_root(&self) -> PathBuf {
170        match self.manifest_path.parent() {
171            Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
172            _ => PathBuf::from("."),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn pkgdir_derives_cache_and_vendored() {
183        let d = PkgDir::new("/x/.mlua-pkgs");
184        assert_eq!(d.base(), Path::new("/x/.mlua-pkgs"));
185        assert_eq!(d.cache(), PathBuf::from("/x/.mlua-pkgs/cache"));
186        assert_eq!(d.vendored(), PathBuf::from("/x/.mlua-pkgs/vendored"));
187    }
188
189    #[test]
190    fn pkgdir_default_in_uses_conventional_name() {
191        assert_eq!(PkgDir::default_in("/proj"), PkgDir::new("/proj/.mlua-pkgs"));
192    }
193
194    #[test]
195    fn project_in_dir_uses_conventional_filenames() {
196        let p = Project::in_dir("/proj", PkgDir::new("/elsewhere"));
197        assert_eq!(p.manifest_path(), Path::new("/proj/mlua-pkg.toml"));
198        assert_eq!(p.lock_path(), Path::new("/proj/mlua-pkg.lock"));
199        assert_eq!(p.pkg_dir().base(), Path::new("/elsewhere"));
200        assert_eq!(p.manifest_root(), PathBuf::from("/proj"));
201    }
202
203    #[test]
204    fn manifest_root_of_bare_filename_is_dot() {
205        let p = Project::new("mlua-pkg.toml", "mlua-pkg.lock", ".mlua-pkgs");
206        assert_eq!(p.manifest_root(), PathBuf::from("."));
207    }
208}