Skip to main content

cabin_workspace/
error.rs

1use std::io;
2use std::path::PathBuf;
3
4use cabin_manifest::ManifestError;
5use miette::Diagnostic;
6use thiserror::Error;
7
8/// Errors produced while loading a workspace, a single package, or its
9/// transitive local path dependencies.
10#[derive(Debug, Error, Diagnostic)]
11pub enum WorkspaceError {
12    /// No `cabin.toml` was found at the requested path. Distinct
13    /// from [`WorkspaceError::Io`] so the diagnostic layer can
14    /// emit a single, deduplicated `manifest_not_found` report
15    /// with help text instead of leaking the underlying
16    /// `io::ErrorKind::NotFound` chain.
17    #[error("could not find a Cabin workspace at {path}", path = path.display())]
18    #[diagnostic(
19        code(cabin::workspace::manifest_not_found),
20        help(
21            "run `cabin init` in the current directory to create a new package, or pass `--manifest-path <path>` to point at an existing `cabin.toml`"
22        )
23    )]
24    ManifestNotFound { path: PathBuf },
25
26    /// The manifest exists but Cabin could not read it. Captures
27    /// permission denied, `IsADirectory`, and similar failures —
28    /// anything except plain `NotFound`, which uses
29    /// [`WorkspaceError::ManifestNotFound`].
30    #[error("could not read the Cabin manifest at {path}: {source}", path = path.display())]
31    #[diagnostic(code(cabin::workspace::manifest_unreadable))]
32    ManifestUnreadable {
33        path: PathBuf,
34        #[source]
35        source: io::Error,
36    },
37
38    #[error("failed to read {path}: {source}", path = path.display())]
39    #[diagnostic(code(cabin::workspace::load_failed))]
40    Io {
41        path: PathBuf,
42        #[source]
43        source: io::Error,
44    },
45
46    #[error("failed to load manifest at {path}: {source}", path = path.display())]
47    #[diagnostic(code(cabin::workspace::load_failed))]
48    Manifest {
49        path: PathBuf,
50        #[source]
51        source: Box<ManifestError>,
52    },
53
54    #[error("manifest at {path} contains neither [package] nor [workspace]", path = path.display())]
55    EmptyManifest { path: PathBuf },
56
57    #[error(
58        "local dependency {dep_name:?} expects a cabin.toml at {expected}, but no such file exists",
59        expected = expected.display()
60    )]
61    LocalDependencyManifestMissing { dep_name: String, expected: PathBuf },
62
63    #[error(
64        "local dependency {dep_name:?} resolves to a workspace root at {path}, but path dependencies must point at a single package",
65        path = path.display()
66    )]
67    LocalDependencyIsWorkspace { dep_name: String, path: PathBuf },
68
69    #[error(
70        "dependency {dep_name:?} points to package {actual_name:?} at {path}; local dependency aliases are not supported",
71        path = path.display()
72    )]
73    DependencyNameMismatch {
74        dep_name: String,
75        actual_name: String,
76        path: PathBuf,
77    },
78
79    #[error("duplicate package name {name:?} in workspace (manifests: {first} and {second})",
80        first = first.display(), second = second.display())]
81    DuplicatePackageName {
82        name: String,
83        first: PathBuf,
84        second: PathBuf,
85    },
86
87    #[error("package dependency cycle detected: {}", format_cycle(.0))]
88    PackageDependencyCycle(Vec<String>),
89
90    #[error(
91        "workspace member pattern {pattern:?} does not match any directory containing a cabin.toml under {root}",
92        root = root.display()
93    )]
94    WorkspaceMemberMissing { pattern: String, root: PathBuf },
95
96    #[error(
97        "workspace member pattern {pattern:?} is not supported; only exact paths and a single trailing '*' (for example: 'packages/*') are supported"
98    )]
99    UnsupportedWorkspacePattern { pattern: String },
100
101    #[error(
102        "{field} entry {pattern:?} must be relative to the workspace root; absolute paths and `..` components are rejected"
103    )]
104    WorkspacePatternEscapesRoot {
105        field: &'static str,
106        pattern: String,
107    },
108
109    #[error(
110        "registry dependency {dep_name:?} declared by package {parent:?} is not in the resolved set"
111    )]
112    UnresolvedRegistryDependency { dep_name: String, parent: String },
113
114    #[error(
115        "foundation-port dependency {dep_name:?} declared by package {parent:?} has not been prepared; this is an internal invariant violation — the CLI orchestration layer must call `cabin_port::prepare` before the workspace loader runs"
116    )]
117    PortDependencyNotPrepared {
118        dep_name: String,
119        parent: String,
120        port_dir: PathBuf,
121    },
122
123    #[error(
124        "bundled foundation-port dependency {dep_name:?} declared by package {parent:?} has not been prepared; this is an internal invariant violation — the CLI orchestration layer must call `cabin_port::prepare` before the workspace loader runs"
125    )]
126    BuiltinPortDependencyNotPrepared { dep_name: String, parent: String },
127
128    #[error(
129        "foundation-port directory {} declared by package {parent:?} does not exist",
130        port_dir.display()
131    )]
132    PortDirectoryMissing {
133        dep_name: String,
134        parent: String,
135        port_dir: PathBuf,
136    },
137
138    #[error(
139        "registry package source {path} is named {actual_name:?} {actual_version}, but the resolver expected {name:?} {version}",
140        path = path.display()
141    )]
142    RegistryPackageMismatch {
143        name: String,
144        version: String,
145        actual_name: String,
146        actual_version: String,
147        path: PathBuf,
148    },
149
150    #[error(
151        "dependency `{dep_name}` uses workspace = true under {section} in package `{parent}`, but {workspace_section} does not define `{dep_name}`",
152        section = kind.manifest_section(),
153        workspace_section = workspace_section_for(*kind),
154    )]
155    UnresolvedWorkspaceDependency {
156        dep_name: String,
157        parent: String,
158        kind: cabin_core::DependencyKind,
159    },
160
161    #[error("workspace default member `{member}` is not listed in workspace.members")]
162    DefaultMemberNotInMembers { member: String },
163
164    #[error(
165        "workspace exclude pattern {pattern:?} does not match any directory under {root}",
166        root = root.display()
167    )]
168    UnusedExcludePattern { pattern: String, root: PathBuf },
169
170    #[error(
171        "nested workspace at {path}: a workspace member must not declare its own [workspace] table",
172        path = path.display()
173    )]
174    NestedWorkspace { path: PathBuf },
175
176    #[error(
177        "workspace dependency `{name}` declared under [workspace.dependencies] is not a valid version requirement: {source}"
178    )]
179    InvalidWorkspaceDependency {
180        name: String,
181        #[source]
182        source: Box<cabin_manifest::ManifestError>,
183    },
184
185    #[error(
186        "package `{name}` is not a member of this workspace; available members: {}",
187        members.join(", ")
188    )]
189    PackageNotInWorkspace { name: String, members: Vec<String> },
190
191    #[error("--exclude requires --workspace or --default-members")]
192    ExcludeWithoutWorkspaceSelection,
193
194    #[error("--default-members requires a workspace root")]
195    DefaultMembersWithoutWorkspace,
196
197    #[error("package selection is ambiguous in this workspace; pass --package <name>")]
198    AmbiguousPackageSelection,
199
200    #[error("incompatible workspace requirements for `{name}`: {requirements}: {source}")]
201    IncompatibleWorkspaceRequirements {
202        name: String,
203        requirements: String,
204        #[source]
205        source: semver::Error,
206    },
207
208    #[error(
209        "registry package source {path} declares package `{actual_name}`, but the resolver expected `{name}`",
210        path = path.display()
211    )]
212    RegistryPackageNameMismatch {
213        name: String,
214        actual_name: String,
215        path: PathBuf,
216    },
217
218    #[error(
219        "package `{name}` is not path-safe for registry publishing; package names cannot contain `/`, `\\`, `..`, or path-prefix-like forms"
220    )]
221    UnsafeRegistryPackageName { name: String },
222
223    #[error(
224        "nested workspace at {nested} cannot be the entry point because it is already a member of the workspace at {parent}",
225        nested = nested.display(),
226        parent = parent.display()
227    )]
228    NestedWorkspaceFromInside { nested: PathBuf, parent: PathBuf },
229
230    #[error(
231        "nested workspace detected: nearest workspace is {nearest} but outer workspace is {outer}",
232        nearest = nearest.display(),
233        outer = outer.display()
234    )]
235    NestedWorkspaceDiscovery { nearest: PathBuf, outer: PathBuf },
236
237    #[error(
238        "package `{package}` at {path} declares `[profile.*]` tables, but profile tables may only appear in the workspace root manifest",
239        path = path.display()
240    )]
241    MemberDeclaresProfiles { package: String, path: PathBuf },
242
243    #[error(
244        "package `{package}` at {path} declares a `[toolchain]` table, but toolchain selection may only appear in the workspace root manifest",
245        path = path.display()
246    )]
247    MemberDeclaresToolchain { package: String, path: PathBuf },
248
249    #[error(
250        "package `{package}` at {path} declares a `[profile.cache]` or `[target.'cfg(...)'.profile.cache]` table, but compiler-cache wrapper settings may only appear in the workspace root manifest",
251        path = path.display()
252    )]
253    MemberDeclaresCompilerWrapper { package: String, path: PathBuf },
254
255    #[error(
256        "package `{package}` at {path} declares a `[patch]` table, but patch declarations may only appear in the workspace root manifest",
257        path = path.display()
258    )]
259    MemberDeclaresPatches { package: String, path: PathBuf },
260
261    #[error(
262        "registry package `{package}` at {path} declares a `path` dependency on `{dep_name}`, but a downloaded registry package may only depend on other packages by version",
263        path = path.display()
264    )]
265    RegistryPackageDeclaresPathDependency {
266        package: String,
267        dep_name: String,
268        path: PathBuf,
269    },
270
271    #[error(
272        "registry package `{package}` at {path} declares a port dependency on `{dep_name}`, but a downloaded registry package may only depend on other packages by version",
273        path = path.display()
274    )]
275    RegistryPackageDeclaresPortDependency {
276        package: String,
277        dep_name: String,
278        path: PathBuf,
279    },
280
281    #[error(
282        "patch for package `{package}` collides with a registry entry at {path}; remove the duplicate registry source or the patch declaration",
283        path = path.display()
284    )]
285    PatchConflictsWithRegistry { package: String, path: PathBuf },
286}
287
288fn format_cycle(cycle: &[String]) -> String {
289    cycle.join(" -> ")
290}
291
292fn workspace_section_for(kind: cabin_core::DependencyKind) -> &'static str {
293    use cabin_core::DependencyKind::{Dev, Normal};
294    match kind {
295        Normal => "[workspace.dependencies]",
296        Dev => "[workspace.dev-dependencies]",
297    }
298}