Skip to main content

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}