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}