Skip to main content

cabin_build/
error.rs

1use std::path::PathBuf;
2
3use camino::Utf8PathBuf;
4use thiserror::Error;
5
6/// Errors produced while planning a build graph.
7#[derive(Debug, Error)]
8pub enum BuildError {
9    #[error("target dependency cycle detected: {}", format_cycle(.0))]
10    DependencyCycle(Vec<String>),
11
12    #[error("no target named {0:?} is defined in the package graph")]
13    UnknownTargetReference(String),
14
15    #[error("target {:?} is ambiguous; use `package:target` (candidates: {})", .0, format_candidates(.1))]
16    AmbiguousTarget(String, Vec<String>),
17
18    #[error("unknown package {package:?} in target selector {selector:?}")]
19    UnknownPackageInTargetSelector { package: String, selector: String },
20
21    #[error("package {package:?} has no target {target:?}")]
22    UnknownTargetInPackage { package: String, target: String },
23
24    /// A target's `deps` entry names a package dependency by its
25    /// bare name, but the dependency declares no library or
26    /// header-only target with that name.  A bare name is shorthand
27    /// for a same-named linkable target (`foo` means `foo:foo`);
28    /// packages never export a *default* target, so anything else -
29    /// including a same-named executable - must be spelled
30    /// `package:target`.
31    #[error(
32        "`deps` entry {dep:?} matches package dependency {package:?}, but that package declares no library or header-only target named {dep:?}; a bare name is shorthand for a same-named linkable target (`{dep}:{dep}`), not a default library - use a local target name or an explicit `package:target`{}",
33        format_target_suggestions(.package, .candidates)
34    )]
35    NoSameNameTargetInDependency {
36        dep: String,
37        package: String,
38        /// Qualified `package:target` spellings of the dependency
39        /// package's library and header-only targets.
40        candidates: Vec<String>,
41    },
42
43    /// A target's `deps` entry names a package that the owning
44    /// manifest only declares under `[dev-dependencies]`, but no
45    /// active dev edge backs it: either the referencing target is
46    /// not a dev-only kind (`test` / `example`), or the invocation
47    /// did not activate dev-deps for the owning package (`cabin
48    /// test` activates them for the selected packages only).
49    #[error(
50        "dependency {dep:?} is declared under `[dev-dependencies]` of package {package:?} but is not active here; dev dependencies link only into `test` / `example` targets, and only `cabin test` activates them for the selected packages"
51    )]
52    DevDependencyNotActive { dep: String, package: String },
53
54    /// An explicitly selected target's `required-features` are not
55    /// all enabled.  Default enumeration silently skips such
56    /// targets; naming one (`cabin test --test`, `cabin run
57    /// --bin`, a manifest-target selector) is a hard error.
58    #[error(
59        "target `{target}` requires the features [{}] of package {package:?}, which are not enabled; enable them with `--features {}` or add them to `[features].default` of package {package:?}",
60        format_feature_list(.missing), .missing.join(",")
61    )]
62    TargetRequiresFeatures {
63        target: String,
64        package: String,
65        missing: Vec<String>,
66    },
67
68    /// A `deps` entry resolves to a target whose
69    /// `required-features` are not all enabled on the providing
70    /// package.  The attached [`FeatureGateFix`] names the place
71    /// that can actually enable them.
72    #[error(
73        "target `{consumer}` depends on `{dep_target}`, which requires the features [{}] of package {dep_package:?}; {}",
74        format_feature_list(.missing),
75        format_dep_feature_help(.dep_package, .missing, *.fix)
76    )]
77    TargetDepRequiresFeatures {
78        consumer: String,
79        dep_target: String,
80        dep_package: String,
81        missing: Vec<String>,
82        /// Where the missing features can be enabled.
83        fix: FeatureGateFix,
84    },
85
86    /// Every default-buildable target in the selection was skipped
87    /// because its `required-features` are not enabled.  Plain
88    /// [`BuildError::EmptySelectedPackages`] would hide the fix, so
89    /// the gated targets and their missing features are spelled
90    /// out.
91    #[error(
92        "every default-buildable target in the selected packages requires features that are not enabled: {}; enable them with `--features <name>`",
93        format_gated_targets(.gated)
94    )]
95    AllDefaultTargetsRequireFeatures {
96        /// `(package:target, missing features)` pairs, in selection
97        /// order.
98        gated: Vec<(String, Vec<String>)>,
99    },
100
101    #[error("target {0:?} has no source files; nothing to build")]
102    EmptyTargetSources(String),
103
104    #[error("source path {path} for target {target:?} is not supported: {reason}")]
105    InvalidSourcePath {
106        target: String,
107        path: Utf8PathBuf,
108        reason: String,
109    },
110
111    #[error("path {} is not valid UTF-8 and cannot be used in build commands", .0.display())]
112    NonUtf8Path(PathBuf),
113
114    #[error(
115        "selected workspace packages declare no C/C++ targets to build; pick a package with at least one library or executable"
116    )]
117    EmptySelectedPackages,
118
119    /// The detected toolchain cannot run the commands the C++
120    /// backend emits.  The wrapped error carries the specific
121    /// missing capability or unsupported compiler family.
122    #[error(transparent)]
123    UnsupportedToolchain(#[from] cabin_core::ToolDetectionError),
124
125    /// The selected tools individually run, but belong to
126    /// different command-line dialects (MSVC `cl.exe` / `lib.exe`
127    /// vs.  GCC/Clang).  Cabin emits one dialect per build, so a
128    /// mixed toolchain cannot be driven coherently.
129    #[error(
130        "selected toolchain mixes MSVC and GCC/Clang tools, which Cabin cannot drive together: {detail}"
131    )]
132    MixedToolchainDialects { detail: String },
133
134    /// A target carries a source whose extension does not match
135    /// any of Cabin's recognized C/C++ extensions.
136    #[error(
137        "target {target:?} has source `{path}` with an unrecognized extension; supported extensions are .c (C) and .cc / .cpp / .cxx / .c++ / .C (C++)"
138    )]
139    UnrecognizedSourceExtension { target: String, path: Utf8PathBuf },
140
141    /// A target carries `.c` source(s) but no C compiler is
142    /// available.  Set `CC`, pass `--cc`, or add `cc = ...` to
143    /// `[toolchain]` so Cabin can compile C translation units.
144    #[error(
145        "target {target:?} has C source `{path}` but no C compiler is available; set the `CC` environment variable, pass `--cc <path>`, or add `cc = ...` under [toolchain]"
146    )]
147    MissingCCompiler { target: String, path: Utf8PathBuf },
148
149    /// A planned compile has no effective language standard: the
150    /// target compiles sources of a language neither its
151    /// `[target.<name>]` table nor its `[package]` declares a
152    /// standard for.  Manifest loading rejects this before planning,
153    /// so this fires only for packages constructed outside the
154    /// manifest parser.
155    #[error(
156        "target `{target}` compiles {language} sources but no {language} standard is declared; add `{field} = \"<standard>\"` to its `[package]` or `[target.<name>]` table, or opt into a workspace default with `{field} = {{ workspace = true }}`"
157    )]
158    MissingLanguageStandard {
159        target: String,
160        language: &'static str,
161        field: &'static str,
162    },
163
164    /// A consuming target's effective implementation standard is
165    /// below a reachable library-like dependency's interface
166    /// requirement for the same language.  The planner records the
167    /// incompatibility on the consumer's compile;
168    /// `validate_planned_standards` surfaces the first survivor
169    /// after the `cabin check` rewrite has pruned dependency
170    /// compiles.
171    #[error(
172        "target `{consumer}` compiles {language} as `{consumer_standard}`, but its dependency `{dependency}` requires `{required}` for consumers of its public interface (from {requirement_source}); raise `{consumer}`'s {language} standard to at least `{required}`, or lower the dependency's interface standard if its public headers permit"
173    )]
174    IncompatibleLanguageStandard {
175        consumer: String,
176        dependency: String,
177        language: &'static str,
178        consumer_standard: &'static str,
179        required: &'static str,
180        requirement_source: &'static str,
181    },
182
183    /// A compile that survived into the final build graph requests a
184    /// standard `cl.exe` has no stable `/std:` flag for.  The planner
185    /// records such compiles as violations (it cannot lower them);
186    /// `validate_planned_standards` surfaces the first survivor after
187    /// the `cabin check` rewrite has pruned dependency compiles.
188    #[error(
189        "target `{target}` requests {language} standard `{standard}`, which has no stable MSVC `/std:` flag; use a standard cl.exe supports (c11, c17, c++14, c++17, c++20) or build with a GCC/Clang toolchain"
190    )]
191    StandardUnsupportedOnMsvcDialect {
192        target: String,
193        language: &'static str,
194        standard: &'static str,
195    },
196
197    /// A compile that survived into the final build graph belongs to
198    /// a target that enables `gnu-extensions` on the MSVC dialect.
199    /// `cl.exe` has no GNU dialect mode, so the flag cannot be
200    /// honored and is never silently ignored.
201    #[error(
202        "target `{target}` enables `gnu-extensions`, but this compiler has no GNU dialect mode; remove `gnu-extensions` or build with a GCC/Clang toolchain"
203    )]
204    GnuExtensionsUnsupportedOnMsvcDialect { target: String },
205
206    /// A planned compile carries both a first-class standard
207    /// declaration and an explicit `-std=` / `/std:` token in its
208    /// manifest-derived flag list.  Boxed to keep the enum small;
209    /// `#[source]` keeps the typed conflict reachable on the error
210    /// chain so the diagnostic registry can attach its stable code.
211    #[error("the manifest declares conflicting standard selections")]
212    StandardFlagConflict(#[source] Box<cabin_core::StandardFlagConflict>),
213}
214
215fn format_cycle(cycle: &[String]) -> String {
216    cycle.join(" -> ")
217}
218
219fn format_candidates(candidates: &[String]) -> String {
220    candidates.join(", ")
221}
222
223fn format_feature_list(features: &[String]) -> String {
224    features
225        .iter()
226        .map(|f| format!("{f:?}"))
227        .collect::<Vec<_>>()
228        .join(", ")
229}
230
231/// Where a gated target's missing `required-features` can be
232/// enabled, driving [`BuildError::TargetDepRequiresFeatures`]'s
233/// help text.  CLI feature selection only applies to selected root
234/// packages, so the actionable fix depends on how the gated
235/// package entered the graph - not on which target referenced it.
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum FeatureGateFix {
238    /// The gated package is a selected root: `--features` /
239    /// `[features].default` apply to it directly.
240    RootSelection,
241    /// The gated package is reached through the consumer's normal
242    /// `[dependencies]` edge: request the features there.
243    DependencyEdge,
244    /// The gated package is reached through the consumer's
245    /// activated `[dev-dependencies]` edge: request the features
246    /// there, never by promoting the dep to `[dependencies]`.
247    DevDependencyEdge,
248    /// The gate sits inside a dependency package (an ungated dep
249    /// target pulls a gated sibling): the enabling edge belongs to
250    /// whichever package depends on it, upstream of the reported
251    /// consumer.
252    UpstreamEdge,
253}
254
255fn format_dep_feature_help(dep_package: &str, missing: &[String], fix: FeatureGateFix) -> String {
256    let features = missing
257        .iter()
258        .map(|f| format!("{f:?}"))
259        .collect::<Vec<_>>()
260        .join(", ");
261    match fix {
262        FeatureGateFix::RootSelection => format!(
263            "enable them with `--features {}` or add them to `[features].default`",
264            missing.join(",")
265        ),
266        FeatureGateFix::DependencyEdge => format!(
267            "request them on the dependency edge, e.g. `{dep_package} = {{ version = \"...\", features = [{features}] }}` under `[dependencies]`"
268        ),
269        FeatureGateFix::DevDependencyEdge => format!(
270            "request them on the dependency edge, e.g. `{dep_package} = {{ version = \"...\", features = [{features}] }}` under `[dev-dependencies]`"
271        ),
272        FeatureGateFix::UpstreamEdge => format!(
273            "request them on the dependency edge that makes {dep_package:?} available, e.g. `{dep_package} = {{ version = \"...\", features = [{features}] }}` in the depending package's manifest"
274        ),
275    }
276}
277
278fn format_gated_targets(gated: &[(String, Vec<String>)]) -> String {
279    gated
280        .iter()
281        .map(|(target, missing)| format!("`{target}` requires [{}]", format_feature_list(missing)))
282        .collect::<Vec<_>>()
283        .join(", ")
284}
285
286fn format_target_suggestions(package: &str, candidates: &[String]) -> String {
287    if candidates.is_empty() {
288        format!(" (package {package:?} declares no library or header-only targets)")
289    } else {
290        format!(", e.g. `{}`", candidates.join("` or `"))
291    }
292}