mlua_pkg/error.rs
1//! Crate-wide error type for the PkgMgr subsystem.
2//!
3//! [`PkgError`] is the single error type for all PkgMgr operations
4//! (manifest parsing, lockfile I/O, git fetch, CLI). It is distinct from
5//! the existing [`crate::ResolveError`], which covers runtime `resolve()` failures.
6//! Additional variants are added by subsequent subtasks.
7
8use std::path::PathBuf;
9
10/// Crate-wide error for PkgMgr operations.
11///
12/// # Error taxonomy
13///
14/// | Variant | Phase | Source |
15/// |---------|-------|--------|
16/// | [`ManifestParse`](Self::ManifestParse) | Parse | [`toml::de::Error`] via `#[from]` |
17/// | [`LockfileParse`](Self::LockfileParse) | Parse | [`toml::de::Error`] via `map_err` |
18/// | [`LockfileWrite`](Self::LockfileWrite) | Serialize | [`toml::ser::Error`] via `#[from]` |
19/// | [`MissingLockfile`](Self::MissingLockfile) | I/O | path not found |
20/// | [`SameNameConflict`](Self::SameNameConflict) | Validate | duplicate name in lockfile |
21/// | [`Io`](Self::Io) | I/O | [`std::io::Error`] via `#[from]` |
22/// | [`Validation`](Self::Validation) | Post-parse | custom message |
23/// | [`GitFetch`](Self::GitFetch) | Fetch | [`git2::Error`] via `#[from]` |
24/// | [`TagShaMismatch`](Self::TagShaMismatch) | Fetch | tag ↔ SHA conflict |
25/// | [`DepConflict`](Self::DepConflict) | Resolve | one name, two different `Dep` specs |
26/// | [`EntryNotFound`](Self::EntryNotFound) | Install | no `src/` / `lua/` / `.` under the cache dir |
27/// | [`UnknownDep`](Self::UnknownDep) | Validate | `ops::update` name not in `[deps]` |
28/// | [`Fetch`](Self::Fetch) | Fetch | per-dep wrapper around another variant |
29/// | [`ManifestEdit`](Self::ManifestEdit) | Edit | `toml_edit` re-parse / structural edit |
30/// | [`ManifestSerialize`](Self::ManifestSerialize) | Serialize | [`toml::ser::Error`] via `map_err` |
31///
32/// Variants are `#[non_exhaustive]` so that future subtasks can add new
33/// variants without breaking downstream `match` arms that include a wildcard.
34///
35/// The existing [`crate::ResolveError`] is intentionally not modified.
36#[non_exhaustive]
37#[derive(Debug, thiserror::Error)]
38pub enum PkgError {
39 /// TOML parse failure when reading `mlua-pkg.toml`.
40 ///
41 /// Automatically constructed from [`toml::de::Error`] via `?`.
42 #[error("manifest parse error: {source}")]
43 ManifestParse {
44 #[from]
45 source: toml::de::Error,
46 },
47
48 /// TOML parse failure when reading `mlua-pkg.lock`.
49 ///
50 /// Does **not** carry `#[from]` — [`ManifestParse`](Self::ManifestParse)
51 /// already claims `From<toml::de::Error>`. Lockfile read paths must use
52 /// `.map_err(|e| PkgError::LockfileParse { source: e })` explicitly.
53 #[error("lockfile parse error: {source}")]
54 LockfileParse { source: toml::de::Error },
55
56 /// TOML serialization failure when writing `mlua-pkg.lock`.
57 ///
58 /// Automatically constructed from [`toml::ser::Error`] via `?`.
59 #[error("lockfile write error: {source}")]
60 LockfileWrite {
61 #[from]
62 source: toml::ser::Error,
63 },
64
65 /// `mlua-pkg.lock` not found at the expected path.
66 ///
67 /// Raised by [`Lockfile::read`](crate::lockfile::Lockfile::read) when the
68 /// file is absent, to distinguish the "not yet installed" case from other
69 /// I/O failures.
70 #[error("lockfile not found: {}", path.display())]
71 MissingLockfile { path: PathBuf },
72
73 /// Two packages share the same `name` field within a lockfile.
74 ///
75 /// Package names must be unique within a lockfile. Raised during
76 /// [`Lockfile::read`](crate::lockfile::Lockfile::read) and at install time.
77 #[error("duplicate package name in lockfile: {name:?}")]
78 SameNameConflict { name: String },
79
80 /// The same package name is requested with two different `Dep` specs
81 /// (git URL / pin / entry / target_dir) during transitive resolution.
82 ///
83 /// `first` / `second` name who asked: `<manifest>` for a direct dep, else the
84 /// package whose `mlua-pkg.toml` declared it. There is no version
85 /// unification; make both sides agree on one spec.
86 #[error("dependency conflict for {name:?}: requested by {first} and by {second} with different specs")]
87 DepConflict {
88 name: String,
89 first: String,
90 second: String,
91 },
92
93 /// I/O error while reading or writing files (manifest, lockfile, cache).
94 ///
95 /// Automatically constructed from [`std::io::Error`] via `?`.
96 #[error("I/O error: {source}")]
97 Io {
98 #[from]
99 source: std::io::Error,
100 },
101
102 /// Post-parse validation failure.
103 ///
104 /// Raised when the parsed manifest satisfies TOML grammar but violates
105 /// semantic constraints, e.g. specifying both `tag` and `rev` in a
106 /// single dependency entry.
107 #[error("manifest validation error: {message}")]
108 Validation { message: String },
109
110 /// git2 operation failure during fetch or clone.
111 ///
112 /// Automatically constructed from [`git2::Error`] via `?`.
113 #[error("git fetch error: {source}")]
114 GitFetch {
115 #[from]
116 source: git2::Error,
117 },
118
119 /// The resolved SHA for a tag did not match the expected SHA.
120 ///
121 /// Raised when the caller pins a specific SHA together with a tag and
122 /// the tag resolves to a different commit.
123 #[error("tag SHA mismatch: expected {expected}, got {actual}")]
124 TagShaMismatch { expected: String, actual: String },
125
126 /// A dep with `patch_drift = "error"` whose `patch_dir` cannot be used.
127 ///
128 /// Raised by `install` instead of the warning-and-fall-back that
129 /// `patch_drift = "warn"` (the default) applies. `reason` says why the
130 /// patch was rejected (pin moved / directory missing / no recorded
131 /// base); `pinned` is the commit the pin currently resolves to.
132 #[error(
133 "patch_dir '{patch_dir}' for '{name}' cannot be used: {reason}; \
134 run `mlua-pkg patch {name} --force` to rebuild it from {pinned}"
135 )]
136 PatchDrift {
137 name: String,
138 patch_dir: PathBuf,
139 reason: String,
140 pinned: String,
141 },
142
143 /// No suitable entry point directory found within a cached package.
144 ///
145 /// Raised by [`mlua_pkg::resolve_entry`](crate::resolve_entry) when neither
146 /// the override path nor any fallback candidate (`src/`, `lua/`, `.`) exists
147 /// as a directory under `cache_path`.
148 ///
149 /// Also raised during [`VendoredResolver::from_lockfile`](crate::resolvers::VendoredResolver::from_lockfile)
150 /// construction when a package in the lockfile has no resolvable entry.
151 #[error("entry not found for {name:?}: tried {}", format_paths(attempted))]
152 EntryNotFound {
153 name: String,
154 attempted: Vec<PathBuf>,
155 },
156
157 /// A dependency name was given that is not present in `[deps]`.
158 ///
159 /// Raised by [`ops::update`](crate::ops::update) when the caller
160 /// restricts the update to a single package that the manifest does not
161 /// declare.
162 #[error("unknown package {name:?} in {}", manifest.display())]
163 UnknownDep { name: String, manifest: PathBuf },
164
165 /// Fetch of one named dependency failed.
166 ///
167 /// Wraps the underlying error so callers of [`ops::install`](crate::ops::install)
168 /// can tell *which* package broke without parsing messages.
169 /// `requested_by` is `<manifest>` for a direct dep, else the package
170 /// whose `mlua-pkg.toml` declared it.
171 #[error("fetching {name:?} (requested by {requested_by}): {source}")]
172 Fetch {
173 name: String,
174 requested_by: String,
175 #[source]
176 source: Box<PkgError>,
177 },
178
179 /// Format-preserving manifest edit failed.
180 ///
181 /// `update` rewrites `mlua-pkg.toml` through `toml_edit` so comments and
182 /// key order survive; this variant covers both the re-parse and the
183 /// structural edit (e.g. `[deps]` table missing).
184 #[error("manifest edit error: {message}")]
185 ManifestEdit { message: String },
186
187 /// TOML serialization failure when writing `mlua-pkg.toml`.
188 ///
189 /// Does **not** carry `#[from]` — [`LockfileWrite`](Self::LockfileWrite)
190 /// already claims `From<toml::ser::Error>`.
191 #[error("manifest write error: {source}")]
192 ManifestSerialize { source: toml::ser::Error },
193}
194
195impl From<toml_edit::TomlError> for PkgError {
196 fn from(e: toml_edit::TomlError) -> Self {
197 PkgError::ManifestEdit {
198 message: e.to_string(),
199 }
200 }
201}
202
203fn format_paths(paths: &[PathBuf]) -> String {
204 paths
205 .iter()
206 .map(|p| p.display().to_string())
207 .collect::<Vec<_>>()
208 .join(", ")
209}