1use std::io;
2use std::path::PathBuf;
3
4use cabin_manifest::ManifestError;
5use miette::Diagnostic;
6use thiserror::Error;
7
8#[derive(Debug, Error, Diagnostic)]
11pub enum WorkspaceError {
12 #[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 #[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}