cabin_core/model.rs
1use std::collections::{BTreeMap, HashSet};
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::build_flags::ProfileSettings;
7use crate::compiler_wrapper::CompilerWrapperManifestSettings;
8use crate::config::Features;
9use crate::error::ValidationError;
10use crate::patch::PatchManifestSettings;
11use crate::profile::{ProfileDefinition, ProfileName};
12use crate::toolchain::ToolchainSettings;
13
14/// Validated package name.
15///
16/// Newtype wrapper so future versions can centralize package-name syntax
17/// rules (e.g. registry-specific patterns) without touching every callsite.
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
19#[serde(try_from = "String", into = "String")]
20pub struct PackageName(String);
21
22impl PackageName {
23 /// Construct a [`PackageName`] after running validation rules.
24 ///
25 /// The grammar enforced here covers filesystem path
26 /// components, sparse-HTTP path segments, package archive
27 /// filenames, and Windows-reserved filename characters in a
28 /// single rule. See [`is_path_safe_package_name`] for the
29 /// full predicate.
30 ///
31 /// # Errors
32 /// Returns [`ValidationError::EmptyPackageName`] for an empty name,
33 /// [`ValidationError::PackageNameContainsWhitespace`] when the name contains
34 /// whitespace, and [`ValidationError::UnsafePackageName`] when it fails the
35 /// [`is_path_safe_package_name`] predicate.
36 pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
37 let value = value.into();
38 if value.is_empty() {
39 return Err(ValidationError::EmptyPackageName);
40 }
41 if value.chars().any(char::is_whitespace) {
42 return Err(ValidationError::PackageNameContainsWhitespace(value));
43 }
44 if !is_path_safe_package_name(&value) {
45 return Err(ValidationError::UnsafePackageName(value));
46 }
47 Ok(Self(value))
48 }
49
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53}
54
55/// Shared package-name validity predicate.
56///
57/// A name passes when it is safe to use **simultaneously** as
58/// (a) a single filesystem path component on every supported
59/// host OS, (b) a single sparse-HTTP URL path segment, and
60/// (c) a fragment of a package archive filename. The grammar is
61/// deliberately strict so the same `PackageName` value can flow
62/// from manifest parsing through the workspace loader, the
63/// resolver, the lockfile, the artifact cache, and the registry
64/// (file or sparse HTTP) without any per-stage re-encoding.
65///
66/// A name is valid iff:
67///
68/// - it is non-empty;
69/// - it consists only of ASCII letters (`A-Z`, `a-z`), ASCII
70/// digits (`0-9`), `_`, `-`, and `.`;
71/// - it is not literally `.` or `..`;
72/// - it does not start with `.` or `-`.
73///
74/// Consequences worth calling out:
75///
76/// - `foo..bar` is **accepted**: it's not a parent reference
77/// because the name is not literally `..` and does not start
78/// with a dot. Path resolvers do not interpret the embedded
79/// `..` substring as a navigation. This is intentional so that
80/// common library names like `boost..hana` (hypothetical) stay
81/// legal under the registry grammar.
82/// - A leading `-` is rejected so the name cannot be mistaken
83/// for a flag when it reaches an argv-driven tool (e.g.,
84/// `pkg-config`, the linker), or for the start of a CLI
85/// short-option block. An embedded `-` (like `foo-bar`) is
86/// still fine.
87/// - URL-reserved characters (`?`, `#`, `%`, `:`), Windows-
88/// reserved filename characters (`< > : " | ? *`), and path
89/// separators (`/`, `\`) are all outside the allowed alphabet,
90/// so they are rejected without needing a separate enumeration.
91/// - Control characters and non-ASCII characters are also outside
92/// the alphabet, so they fall under the same rule.
93///
94/// The shared helper keeps `cabin-package`, `cabin-registry-file`,
95/// and `cabin-index-http` from drifting on this rule.
96pub fn is_path_safe_package_name(name: &str) -> bool {
97 if name.is_empty() {
98 return false;
99 }
100 if name == "." || name == ".." {
101 return false;
102 }
103 if name.starts_with('.') || name.starts_with('-') {
104 return false;
105 }
106 name.bytes()
107 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
108}
109
110impl AsRef<str> for PackageName {
111 fn as_ref(&self) -> &str {
112 &self.0
113 }
114}
115
116impl std::fmt::Display for PackageName {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.write_str(&self.0)
119 }
120}
121
122impl TryFrom<String> for PackageName {
123 type Error = ValidationError;
124
125 fn try_from(value: String) -> Result<Self, Self::Error> {
126 PackageName::new(value)
127 }
128}
129
130impl From<PackageName> for String {
131 fn from(value: PackageName) -> Self {
132 value.0
133 }
134}
135
136/// Validated target name.
137#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
138#[serde(try_from = "String", into = "String")]
139pub struct TargetName(String);
140
141impl TargetName {
142 /// Construct a [`TargetName`] after running validation.
143 ///
144 /// Target names are joined into filesystem paths by the build
145 /// planner (object directories, executable paths, Cargo target
146 /// directories), so they share the path-component grammar with
147 /// [`PackageName`]: a name like `[target."../escape"]` would
148 /// otherwise let a malicious manifest write artifacts outside
149 /// the selected `--build-dir`. The grammar is enforced through
150 /// [`is_path_safe_package_name`], which already covers path
151 /// separators, `..` / `.`, leading `.` or `-`, control characters,
152 /// non-ASCII bytes, and Windows-reserved filename characters in a
153 /// single rule.
154 ///
155 /// # Errors
156 /// Returns [`ValidationError::EmptyTargetName`] for an empty name,
157 /// [`ValidationError::TargetNameContainsWhitespace`] when the name contains
158 /// whitespace, and [`ValidationError::UnsafeTargetName`] when it fails the
159 /// [`is_path_safe_package_name`] predicate.
160 pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
161 let value = value.into();
162 if value.is_empty() {
163 return Err(ValidationError::EmptyTargetName);
164 }
165 if value.chars().any(char::is_whitespace) {
166 return Err(ValidationError::TargetNameContainsWhitespace(value));
167 }
168 if !is_path_safe_package_name(&value) {
169 return Err(ValidationError::UnsafeTargetName(value));
170 }
171 Ok(Self(value))
172 }
173
174 pub fn as_str(&self) -> &str {
175 &self.0
176 }
177}
178
179impl AsRef<str> for TargetName {
180 fn as_ref(&self) -> &str {
181 &self.0
182 }
183}
184
185impl std::fmt::Display for TargetName {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 f.write_str(&self.0)
188 }
189}
190
191impl TryFrom<String> for TargetName {
192 type Error = ValidationError;
193
194 fn try_from(value: String) -> Result<Self, Self::Error> {
195 TargetName::new(value)
196 }
197}
198
199impl From<TargetName> for String {
200 fn from(value: TargetName) -> Self {
201 value.0
202 }
203}
204
205/// What kind of artifact a target produces.
206///
207/// Target kinds describe artifact role only. Source-language
208/// classification is per-file, based on source extension: `.c`
209/// compiles as C, `.cc` / `.cpp` / `.cxx` / `.c++` / `.C` compile
210/// as C++. A single target may freely mix C/C++ sources; the
211/// planner selects the compiler per source and selects the link
212/// driver from the direct and transitive source-language closure
213/// (C++ if any object is C++, otherwise C).
214///
215/// The string representations are stable: they are written by the manifest
216/// parser, surfaced by `cabin metadata`, and consumed by the build graph
217/// planner.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
219pub enum TargetKind {
220 /// Static-archive library (`lib<name>.a`).
221 #[serde(rename = "library")]
222 Library,
223 /// A header-only library. Has no translation units of its own;
224 /// the planner emits no compile or archive actions, and consumers
225 /// pick up its `include_dirs` through the dependency graph.
226 #[serde(rename = "header_only")]
227 HeaderOnly,
228 /// A linked executable. Built by default by `cabin build`.
229 #[serde(rename = "executable")]
230 Executable,
231 /// A test executable. Built and run by `cabin test`. Excluded
232 /// from the default `cabin build` selection.
233 #[serde(rename = "test")]
234 Test,
235 /// An example executable. Excluded from the default
236 /// `cabin build` selection. Today the only way an example
237 /// reaches the build graph is as a transitive dep of another
238 /// selected target; a dedicated explicit-kind selector flag
239 /// is reserved for future work (the historic `--target` name
240 /// is reserved for platform/toolchain target selection).
241 #[serde(rename = "example")]
242 Example,
243}
244
245impl TargetKind {
246 pub const fn as_str(self) -> &'static str {
247 match self {
248 Self::Library => "library",
249 Self::HeaderOnly => "header_only",
250 Self::Executable => "executable",
251 Self::Test => "test",
252 Self::Example => "example",
253 }
254 }
255
256 /// All kinds, in declaration order. Useful for error messages that list
257 /// the supported types.
258 pub const fn all() -> &'static [TargetKind] {
259 &[
260 Self::Library,
261 Self::HeaderOnly,
262 Self::Executable,
263 Self::Test,
264 Self::Example,
265 ]
266 }
267
268 /// Whether this kind produces an executable (linked binary).
269 /// Library kinds return `false`.
270 pub const fn produces_executable(self) -> bool {
271 matches!(self, Self::Executable | Self::Test | Self::Example)
272 }
273
274 /// Whether this kind produces a static-archive library (`lib<name>.a`).
275 pub const fn produces_archive(self) -> bool {
276 matches!(self, Self::Library)
277 }
278
279 /// Whether this kind is a header-only library (no compile/
280 /// archive actions; consumers pick up `include_dirs`).
281 pub const fn is_header_only(self) -> bool {
282 matches!(self, Self::HeaderOnly)
283 }
284
285 /// Whether ordinary `cabin build` selects this kind by default.
286 /// Dev-only kinds (`test` / `example`) are excluded
287 /// from the default set: tests are built by `cabin test`,
288 /// and examples only reach the build graph as a
289 /// transitive dep of another selected target.
290 ///
291 /// Header-only libraries are included so the dependency
292 /// closure walk reaches them; the planner emits no compile or
293 /// archive actions for them, so saying "yes, this is part of
294 /// the default selection" is a no-op on Ninja's side.
295 pub const fn is_default_buildable(self) -> bool {
296 matches!(self, Self::Library | Self::HeaderOnly | Self::Executable)
297 }
298
299 /// Whether this kind is a *development-only* target — a target
300 /// that exists to support workspace development but is not part
301 /// of the package's public surface. Production callers use this
302 /// to decide whether dev-dependencies should be activated and
303 /// whether the target may be run by `cabin test`.
304 pub const fn is_dev_only(self) -> bool {
305 matches!(self, Self::Test | Self::Example)
306 }
307
308 /// Whether `cabin test` runs this kind after building it. Today
309 /// only `test` runs; `example` is build-only.
310 pub const fn is_test(self) -> bool {
311 matches!(self, Self::Test)
312 }
313}
314
315impl std::fmt::Display for TargetKind {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 f.write_str(self.as_str())
318 }
319}
320
321/// A buildable unit within a package.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct Target {
324 pub name: TargetName,
325 pub kind: TargetKind,
326 #[serde(default)]
327 pub sources: Vec<PathBuf>,
328 #[serde(default)]
329 pub include_dirs: Vec<PathBuf>,
330 #[serde(default)]
331 pub defines: Vec<String>,
332 /// Same-package target names or cross-package references. Cross-package
333 /// references take the form `package` (resolves to the package's default
334 /// library target) or `package:target` (qualified). Resolution against a
335 /// concrete package graph lives in `cabin-build`, not here.
336 ///
337 /// Stored as raw strings, not [`TargetName`], because the qualified
338 /// `package:target` form contains a `:` that the path-safe target-name
339 /// grammar rejects. Validation happens at resolution time against the
340 /// already-validated package / target graph; dep strings never flow
341 /// directly into a filesystem path.
342 #[serde(default)]
343 pub deps: Vec<String>,
344}
345
346fn default_true() -> bool {
347 true
348}
349
350/// A package-level Cabin dependency declared in
351/// `[dependencies]` or `[dev-dependencies]`.
352///
353/// System dependencies (`system = true` entries) are *not*
354/// represented here — they live in [`SystemDependency`] because
355/// they have a different schema and never enter Cabin
356/// resolution.
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct Dependency {
359 /// The dependency alias used in the manifest. The alias must
360 /// equal the depended-on package's `[package].name`.
361 pub name: PackageName,
362 pub source: DependencySource,
363 /// Which manifest section the dependency was declared in.
364 /// Defaults to [`DependencyKind::Normal`] so manifests that
365 /// only use `[dependencies]` keep their previous serialized
366 /// shape.
367 #[serde(default, skip_serializing_if = "DependencyKind::is_normal")]
368 pub kind: DependencyKind,
369 /// Whether the dependency is optional. Optional dependencies
370 /// only enter ordinary resolution / fetch / build when a
371 /// feature enables them via `dep:<name>` or
372 /// `<name>/<feature>`.
373 #[serde(default, skip_serializing_if = "is_false")]
374 pub optional: bool,
375 /// Features requested on the dependency package by this edge.
376 /// Stored as the raw manifest strings; the feature resolver
377 /// validates them against the depended-on package's
378 /// `[features]` table.
379 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub features: Vec<String>,
381 /// Whether this edge requests the dependency package's
382 /// `default` feature. Defaults to `true`. `default-features =
383 /// false` only narrows *this* edge — if another edge requests
384 /// defaults for the same package, the unified result still
385 /// includes them.
386 #[serde(default = "default_true", skip_serializing_if = "is_true")]
387 pub default_features: bool,
388 /// Optional target condition. `Some` when the dependency was
389 /// declared inside a `[target.'cfg(...)'.<kind>]` table;
390 /// `None` for unconditional declarations. Conditional
391 /// dependencies whose condition does not match the
392 /// evaluation [`crate::TargetPlatform`] are filtered out by
393 /// `cabin-workspace` / `cabin-feature` / `cabin-build`
394 /// before reaching the resolver or the build planner, but they
395 /// stay on `Package::dependencies` for metadata round-trip.
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub condition: Option<crate::Condition>,
398}
399
400fn is_false<T>(value: &T) -> bool
401where
402 T: PartialEq + Default,
403{
404 *value == T::default()
405}
406
407fn is_true<T>(value: &T) -> bool
408where
409 T: PartialEq + Default + std::ops::Not<Output = T>,
410{
411 *value == !T::default()
412}
413
414impl Dependency {
415 /// Whether this declaration is active for the given
416 /// [`crate::TargetPlatform`]. Unconditional declarations
417 /// are always active; conditional declarations are active
418 /// iff their condition evaluates to `true`.
419 pub fn matches_platform(&self, platform: &crate::TargetPlatform) -> bool {
420 match &self.condition {
421 None => true,
422 Some(cond) => cond.evaluate(platform),
423 }
424 }
425}
426
427/// Which kind of dependency is declared.
428///
429/// Cabin distinguishes package dependency kinds (`Normal`, `Dev`)
430/// — both of which are sourced from other Cabin packages — from
431/// system dependencies, which are externally provided and never
432/// enter Cabin resolution. System declarations live alongside the
433/// package kinds as a separate `system = true` flag on a regular
434/// `[dependencies]` / `[dev-dependencies]` entry and are modeled
435/// by [`SystemDependency`].
436///
437/// The wire format mirrors the manifest section names: `"normal"`,
438/// `"dev"`.
439#[derive(
440 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
441)]
442#[serde(rename_all = "lowercase")]
443pub enum DependencyKind {
444 /// `[dependencies]`. Linked into ordinary builds.
445 #[default]
446 Normal,
447 /// `[dev-dependencies]`. Declaration-only for ordinary
448 /// commands; activated for the selected primary packages by
449 /// `cabin test`.
450 Dev,
451}
452
453impl DependencyKind {
454 /// Stable lowercase label, matching the manifest section name.
455 pub const fn as_str(self) -> &'static str {
456 match self {
457 DependencyKind::Normal => "normal",
458 DependencyKind::Dev => "dev",
459 }
460 }
461
462 /// All kinds in canonical order. `cabin metadata` and the
463 /// canonical package metadata both iterate kinds in this order
464 /// so output stays deterministic.
465 pub const fn all() -> &'static [DependencyKind] {
466 &[DependencyKind::Normal, DependencyKind::Dev]
467 }
468
469 /// Whether this kind is included in the resolver / fetch /
470 /// build pipeline by default. Dev dependencies are excluded.
471 pub const fn is_resolved_by_default(self) -> bool {
472 matches!(self, DependencyKind::Normal)
473 }
474
475 /// Whether this kind contributes link / include edges to
476 /// ordinary `cabin build` targets. Only `Normal` does.
477 pub const fn affects_ordinary_build(self) -> bool {
478 matches!(self, DependencyKind::Normal)
479 }
480
481 /// Helper for `#[serde(skip_serializing_if = ...)]` so
482 /// existing on-disk metadata that omits the `kind` field
483 /// stays byte-identical for `[dependencies]`-only manifests.
484 pub fn is_normal(&self) -> bool {
485 matches!(self, DependencyKind::Normal)
486 }
487
488 /// The manifest section name (`[dependencies]`,
489 /// `[dev-dependencies]`) corresponding to this kind.
490 /// Used in error messages.
491 pub const fn manifest_section(self) -> &'static str {
492 match self {
493 DependencyKind::Normal => "[dependencies]",
494 DependencyKind::Dev => "[dev-dependencies]",
495 }
496 }
497}
498
499impl std::fmt::Display for DependencyKind {
500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501 f.write_str(self.as_str())
502 }
503}
504
505/// A system dependency declared with `system = true` on a
506/// `[dependencies]` / `[dev-dependencies]` entry.
507///
508/// System dependencies are externally provided (system libraries,
509/// SDKs, installed tools). Cabin never resolves, fetches,
510/// downloads, or installs them — `cabin-system-deps` probes them
511/// via `pkg-config` at build time, and the resulting cflags /
512/// ldflags are merged into the per-package build flags before
513/// the planner runs. The typed value round-trips through
514/// `cabin metadata`, the canonical package metadata, and the
515/// index metadata so external tooling sees the system-dep set
516/// alongside the Cabin-package deps.
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518pub struct SystemDependency {
519 /// The dependency name as written in the manifest.
520 pub name: PackageName,
521 /// Version requirement string for `pkg-config`. Cabin does
522 /// not interpret it as a `SemVer` constraint; the system-deps
523 /// layer translates the supported comparators for
524 /// `pkg-config` and reports unsupported forms as errors.
525 pub version: String,
526 /// Which dependency table the entry was declared in
527 /// (`[dependencies]` or `[dev-dependencies]`). Drives per-kind
528 /// activation: a dev-kind system dep is only probed when
529 /// `cabin test` is running, mirroring the Cabin-package
530 /// dev-dep rule.
531 #[serde(default)]
532 pub kind: DependencyKind,
533 /// Optional target condition. `Some` when the system
534 /// dependency was declared inside a
535 /// `[target.'cfg(...)'.<kind>-dependencies]` table. The
536 /// condition is preserved so package / index metadata stays
537 /// portable across platforms.
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub condition: Option<crate::Condition>,
540}
541
542/// Where a foundation-port dependency's recipe comes from.
543///
544/// Constructed by the manifest parser from one of the two
545/// recipe-locator fields:
546///
547/// - `{ port = true, version = "..." }` → `Builtin { name, version_req }`. The recipe
548/// is resolved from `cabin_port::builtin::BUILTIN` by the discovery layer using the
549/// consumer-supplied `version_req`.
550/// - `{ port-path = "..." }` → `Path(PathBuf)`. The recipe lives
551/// on disk at the given path, interpreted relative to the
552/// manifest directory that declared it.
553#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
554pub enum PortDepSource {
555 /// Bundled curated recipe. `version_req` is the consumer-supplied requirement,
556 /// resolved against `cabin_port::builtin::BUILTIN` by the discovery layer.
557 Builtin {
558 name: PackageName,
559 version_req: semver::VersionReq,
560 },
561 Path(PathBuf),
562}
563
564/// Where a dependency is sourced from.
565///
566/// Covers [`DependencySource::Path`] for local path dependencies,
567/// [`DependencySource::Version`] for registry-resolved versioned
568/// dependencies, [`DependencySource::Port`] for foundation-port
569/// dependencies (curated recipes under `crates/cabin-port/ports/`), and
570/// [`DependencySource::Workspace`] for the `{ workspace = true }`
571/// opt-in into the workspace's shared dependency table. The
572/// `Workspace` variant is an unresolved marker —
573/// `cabin-workspace::load_workspace` rewrites it into the
574/// matching `Path` / `Version` / `Port` source from
575/// `[workspace.dependencies]` before any consumer sees a
576/// [`crate::Package`] returned from the workspace loader. If a
577/// `Workspace` source ever reaches a planner or resolver it
578/// indicates the package was loaded outside of
579/// `cabin-workspace`, which is a workspace invariant violation
580/// worth surfacing as a clear error in the caller.
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub enum DependencySource {
583 /// Local path dependency. The path is interpreted relative to the
584 /// manifest directory of the package that declared the dependency.
585 #[serde(rename = "path")]
586 Path(PathBuf),
587 /// Versioned registry dependency. The requirement is matched against
588 /// candidate versions during dependency resolution.
589 #[serde(rename = "version")]
590 Version(semver::VersionReq),
591 /// Foundation-port dependency. The recipe source is one of two
592 /// shapes (see [`PortDepSource`]): a relative path to a port
593 /// directory on disk (`Path`), or a bundled curated recipe keyed
594 /// by the dependency name (`Builtin`). The CLI orchestration
595 /// layer prepares the port (download → verify → safe-extract
596 /// with `strip_prefix` → overlay copy) before the workspace
597 /// loader resolves the dependency to the prepared directory.
598 #[serde(rename = "port")]
599 Port(PortDepSource),
600 /// `dep = { workspace = true }`. An unresolved opt-in
601 /// into the workspace's `[workspace.dependencies]` table.
602 /// `cabin-workspace::load_workspace` resolves these to a
603 /// concrete [`DependencySource::Path`] or
604 /// [`DependencySource::Version`] before producing a
605 /// `PackageGraph`.
606 #[serde(rename = "workspace")]
607 Workspace,
608}
609
610/// Top-level validated package.
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612pub struct Package {
613 pub name: PackageName,
614 pub version: semver::Version,
615 pub targets: Vec<Target>,
616 /// Cabin package dependencies declared under
617 /// `[dependencies]` or `[dev-dependencies]`. Each entry
618 /// carries its [`DependencyKind`]; iteration order is sorted
619 /// by `(kind, name)` so callers see deterministic output.
620 #[serde(default)]
621 pub dependencies: Vec<Dependency>,
622 /// `system = true` declarations. Empty if not
623 /// declared. System dependencies never enter the resolver,
624 /// the lockfile, or the artifact cache; they are
625 /// declaration-only and round-trip through metadata.
626 #[serde(default, skip_serializing_if = "Vec::is_empty")]
627 pub system_dependencies: Vec<SystemDependency>,
628 /// `[features]` declarations. Empty if the manifest has
629 /// no `[features]` table.
630 #[serde(default, skip_serializing_if = "is_empty_features")]
631 pub features: Features,
632 /// `[profile.<name>]` declarations from the manifest, keyed
633 /// by profile name. Built-in profiles do not need to appear
634 /// here; entries that match a built-in name override those
635 /// defaults. Empty for manifests with no profile tables, so
636 /// older manifests stay byte-identical through round-tripping.
637 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
638 pub profiles: BTreeMap<ProfileName, ProfileDefinition>,
639 /// `[toolchain]` plus any `[target.'cfg(...)'.toolchain]`
640 /// overrides declared on this manifest. Only the workspace
641 /// root manifest's settings are honored; member manifests
642 /// that declare a `[toolchain]` table are rejected by the
643 /// workspace loader.
644 #[serde(default, skip_serializing_if = "ToolchainSettings::is_empty")]
645 pub toolchain: ToolchainSettings,
646 /// `[profile]` plus any `[target.'cfg(...)'.profile]`
647 /// declarations for this package. Per-package by design — each
648 /// package may add its own defines / include dirs / extra args.
649 ///
650 /// The raw compiler / linker flag arrays (`cflags` / `cxxflags`
651 /// / `ldflags`) are honored only for local packages — the
652 /// workspace root, its members, and `path` dependencies. They
653 /// are dropped for registry dependencies during flag resolution
654 /// (see `resolve_build_flags`), because they are unvalidated and
655 /// could otherwise smuggle build-time code-execution options
656 /// such as `-fplugin=`. `defines` and `include_dirs` are
657 /// validated and kept for every package.
658 #[serde(default, skip_serializing_if = "ProfileSettings::is_empty")]
659 pub build: ProfileSettings,
660 /// `[profile.cache]` plus any `[target.'cfg(...)'.profile.cache]`
661 /// declarations from the workspace root manifest. Member
662 /// manifests cannot declare cache settings — the workspace
663 /// loader rejects them — so reading off the root is sufficient.
664 /// Round-trips through metadata so packaged manifests preserve
665 /// a publisher's declared wrapper preferences.
666 #[serde(
667 default,
668 skip_serializing_if = "CompilerWrapperManifestSettings::is_empty"
669 )]
670 pub compiler_wrapper: CompilerWrapperManifestSettings,
671 /// `[patch]` declarations on the workspace-root manifest.
672 /// Member manifests cannot declare patches — the workspace
673 /// loader rejects them — and `cabin package` refuses to
674 /// archive a manifest with a non-empty `[patch]` table.
675 /// Patches are *local development policy*, not package
676 /// metadata.
677 #[serde(default, skip_serializing_if = "PatchManifestSettings::is_empty")]
678 pub patches: PatchManifestSettings,
679}
680
681fn is_empty_features(f: &Features) -> bool {
682 f.default.is_empty() && f.features.is_empty()
683}
684
685impl Package {
686 /// Build a validated [`Package`].
687 ///
688 /// Validation:
689 /// - target names are unique
690 /// - dependency names are unique within each kind (the same
691 /// name may legitimately appear under multiple kinds)
692 /// - system dependency names are unique within the
693 /// collected `system = true` declarations
694 /// - feature declarations are well-formed
695 ///
696 /// Target-dep references (same-package, cross-package, or
697 /// qualified `package:target`) are resolved by `cabin-build`
698 /// against the full package graph, not here.
699 ///
700 /// # Errors
701 /// Returns a [`ValidationError`] when validation fails: see
702 /// [`Package::with_config`], which performs the checks
703 /// ([`ValidationError::DuplicateTargetName`],
704 /// [`ValidationError::DuplicateDependency`], and feature-table errors).
705 pub fn new(
706 name: PackageName,
707 version: semver::Version,
708 targets: Vec<Target>,
709 dependencies: Vec<Dependency>,
710 ) -> Result<Self, ValidationError> {
711 Self::with_config(PackageConfigInput {
712 name,
713 version,
714 targets,
715 dependencies,
716 system_dependencies: Vec::new(),
717 features: Features::default(),
718 })
719 }
720
721 /// Build a validated [`Package`] with `[features]` declarations
722 /// attached. `cabin-manifest` calls this after parsing the
723 /// `[features]` table.
724 ///
725 /// # Errors
726 /// Returns [`ValidationError::DuplicateTargetName`] for repeated target
727 /// names, [`ValidationError::DuplicateDependency`] for a duplicate
728 /// dependency within a kind, [`ValidationError::DuplicateSystemDependency`]
729 /// for a duplicate system dependency, and propagates any
730 /// [`ValidationError`] from validating the `[features]` table.
731 pub fn with_config(input: PackageConfigInput) -> Result<Self, ValidationError> {
732 let PackageConfigInput {
733 name,
734 version,
735 targets,
736 dependencies,
737 system_dependencies,
738 features,
739 } = input;
740 Self::validate_targets(&targets)?;
741 Self::validate_dependencies(&dependencies)?;
742 Self::validate_system_dependencies(&system_dependencies)?;
743 features.validate()?;
744 Ok(Self {
745 name,
746 version,
747 targets,
748 dependencies,
749 system_dependencies,
750 features,
751 profiles: BTreeMap::new(),
752 toolchain: ToolchainSettings::default(),
753 build: ProfileSettings::default(),
754 compiler_wrapper: CompilerWrapperManifestSettings::default(),
755 patches: PatchManifestSettings::default(),
756 })
757 }
758
759 /// Attach manifest-declared `[profile.*]` definitions to this
760 /// package. Returns the same package so callers can chain it
761 /// after [`Package::with_config`] without exploding the
762 /// constructor signature for every new optional table.
763 pub fn with_profiles(mut self, profiles: BTreeMap<ProfileName, ProfileDefinition>) -> Self {
764 self.profiles = profiles;
765 self
766 }
767}
768
769/// Bundled inputs for [`Package::with_config`].
770///
771/// `cabin-manifest` builds this from the parsed `cabin.toml` and hands
772/// it to [`Package::with_config`]. Threading inputs through one struct
773/// keeps `with_config` callable across the workspace without a fixed
774/// positional argument order.
775#[derive(Debug, Clone)]
776pub struct PackageConfigInput {
777 /// `package.name` from the manifest.
778 pub name: PackageName,
779 /// `package.version` from the manifest.
780 pub version: semver::Version,
781 /// Parsed `[target.*]` definitions.
782 pub targets: Vec<Target>,
783 /// Parsed `[dependencies]` / `[dev-dependencies]`.
784 pub dependencies: Vec<Dependency>,
785 /// Parsed `[system-dependencies]`.
786 pub system_dependencies: Vec<SystemDependency>,
787 /// Parsed `[features]`.
788 pub features: Features,
789}
790
791impl Package {
792 /// Attach the manifest-declared `[toolchain]` /
793 /// `[target.'cfg(...)'.toolchain]` block. Workspace loaders
794 /// reject these declarations on member / path-dep manifests
795 /// so only the entry-point manifest's value reaches downstream
796 /// crates.
797 pub fn with_toolchain(mut self, toolchain: ToolchainSettings) -> Self {
798 self.toolchain = toolchain;
799 self
800 }
801
802 /// Attach the manifest-declared `[profile]` /
803 /// `[target.'cfg(...)'.profile]` block. Per-package by design.
804 pub fn with_build(mut self, build: ProfileSettings) -> Self {
805 self.build = build;
806 self
807 }
808
809 /// Attach the manifest-declared `[profile.cache]` /
810 /// `[target.'cfg(...)'.profile.cache]` blocks. Workspace
811 /// loaders reject these declarations on member / path-dep
812 /// manifests so only the entry-point manifest's value reaches
813 /// downstream crates.
814 pub fn with_compiler_wrapper(mut self, settings: CompilerWrapperManifestSettings) -> Self {
815 self.compiler_wrapper = settings;
816 self
817 }
818
819 /// Attach the manifest-declared `[patch]` block. Workspace
820 /// loaders reject these declarations on member / path-dep
821 /// manifests so only the entry-point manifest's value
822 /// reaches downstream crates.
823 pub fn with_patches(mut self, patches: PatchManifestSettings) -> Self {
824 self.patches = patches;
825 self
826 }
827
828 fn validate_targets(targets: &[Target]) -> Result<(), ValidationError> {
829 let mut seen: HashSet<&str> = HashSet::with_capacity(targets.len());
830 for target in targets {
831 if !seen.insert(target.name.as_str()) {
832 return Err(ValidationError::DuplicateTargetName(
833 target.name.as_str().to_owned(),
834 ));
835 }
836 }
837 Ok(())
838 }
839
840 fn validate_dependencies(deps: &[Dependency]) -> Result<(), ValidationError> {
841 let mut seen: HashSet<(DependencyKind, &str)> = HashSet::with_capacity(deps.len());
842 for dep in deps {
843 if !seen.insert((dep.kind, dep.name.as_str())) {
844 return Err(ValidationError::DuplicateDependency {
845 name: dep.name.as_str().to_owned(),
846 kind: dep.kind,
847 });
848 }
849 }
850 Ok(())
851 }
852
853 fn validate_system_dependencies(deps: &[SystemDependency]) -> Result<(), ValidationError> {
854 let mut seen: HashSet<&str> = HashSet::with_capacity(deps.len());
855 for dep in deps {
856 if !seen.insert(dep.name.as_str()) {
857 return Err(ValidationError::DuplicateSystemDependency(
858 dep.name.as_str().to_owned(),
859 ));
860 }
861 }
862 Ok(())
863 }
864
865 /// Iterator over the package dependencies that participate in
866 /// the resolver / fetch / build pipeline by default — i.e.
867 /// every Cabin package dependency except `Dev`.
868 pub fn resolved_dependencies(&self) -> impl Iterator<Item = &Dependency> {
869 self.dependencies
870 .iter()
871 .filter(|d| d.kind.is_resolved_by_default())
872 }
873
874 /// Iterator over dependencies of a specific kind. Order is
875 /// the same as `dependencies` (sorted by `(kind, name)`).
876 pub fn dependencies_of_kind(&self, kind: DependencyKind) -> impl Iterator<Item = &Dependency> {
877 self.dependencies.iter().filter(move |d| d.kind == kind)
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884
885 fn version() -> semver::Version {
886 semver::Version::parse("0.1.0").unwrap()
887 }
888
889 fn pkg(name: &str) -> PackageName {
890 PackageName::new(name).unwrap()
891 }
892
893 fn tgt(name: &str) -> TargetName {
894 TargetName::new(name).unwrap()
895 }
896
897 fn target(name: &str, kind: TargetKind, deps: &[&str]) -> Target {
898 Target {
899 name: tgt(name),
900 kind,
901 sources: Vec::new(),
902 include_dirs: Vec::new(),
903 defines: Vec::new(),
904 deps: deps.iter().map(|d| (*d).to_owned()).collect(),
905 }
906 }
907
908 #[test]
909 fn package_name_rejects_empty() {
910 assert_eq!(
911 PackageName::new("").unwrap_err(),
912 ValidationError::EmptyPackageName
913 );
914 }
915
916 #[test]
917 fn package_name_rejects_whitespace() {
918 let err = PackageName::new("hello world").unwrap_err();
919 assert!(matches!(
920 err,
921 ValidationError::PackageNameContainsWhitespace(_)
922 ));
923 }
924
925 /// The displayed error must describe the actual grammar so a
926 /// user reading the message can fix their manifest without
927 /// reading the source. Pin the exact phrasing so the wording
928 /// can only change deliberately.
929 #[test]
930 fn package_name_error_describes_grammar() {
931 let err = PackageName::new("foo?bar").unwrap_err();
932 let displayed = err.to_string();
933 assert!(
934 displayed.contains("\"foo?bar\""),
935 "error must echo the offending name: {displayed}"
936 );
937 assert!(
938 displayed.contains("ASCII letters")
939 && displayed.contains("ASCII digits")
940 && displayed.contains("`_`")
941 && displayed.contains("`-`")
942 && displayed.contains("`.`"),
943 "error must describe the allowed alphabet: {displayed}"
944 );
945 assert!(
946 displayed.contains("must not start with `.` or `-`")
947 && displayed.contains("must not be `.` or `..`"),
948 "error must describe the structural restrictions: {displayed}"
949 );
950 }
951
952 // -----------------------------------------------------------------
953 // PackageName grammar covers filesystem, URL, and
954 // windows-filename safety simultaneously.
955 // -----------------------------------------------------------------
956
957 #[test]
958 fn package_name_accepts_simple_alphanumeric() {
959 assert!(PackageName::new("fmt").is_ok());
960 }
961
962 #[test]
963 fn package_name_accepts_hyphen_and_underscore() {
964 assert!(PackageName::new("foo-bar").is_ok());
965 assert!(PackageName::new("foo_bar").is_ok());
966 assert!(PackageName::new("foo-bar-baz").is_ok());
967 }
968
969 #[test]
970 fn package_name_accepts_dot_in_middle() {
971 // Dots in the middle of a name are allowed; only literal
972 // `.` / `..` and a leading dot are rejected.
973 assert!(PackageName::new("foo.bar").is_ok());
974 assert!(PackageName::new("foo..bar").is_ok());
975 }
976
977 #[test]
978 fn package_name_rejects_path_traversal() {
979 for raw in [".", "..", "../evil", ".hidden", "foo/bar", "foo\\bar"] {
980 assert!(
981 matches!(
982 PackageName::new(raw).unwrap_err(),
983 ValidationError::UnsafePackageName(_)
984 ),
985 "{raw:?} should be rejected as unsafe"
986 );
987 }
988 }
989
990 /// A leading `-` is rejected so the name cannot be parsed as
991 /// a flag when it reaches an argv-driven tool (e.g.,
992 /// `pkg-config` for `system = true` deps, the linker, or
993 /// `clap` short-option splitting).
994 #[test]
995 fn package_name_rejects_leading_dash() {
996 for raw in ["-foo", "--list-all", "-Lfoo", "-"] {
997 assert!(
998 matches!(
999 PackageName::new(raw).unwrap_err(),
1000 ValidationError::UnsafePackageName(_)
1001 ),
1002 "{raw:?} must be rejected because of the leading `-`"
1003 );
1004 }
1005 // Embedded `-` is still fine.
1006 assert!(PackageName::new("foo-bar").is_ok());
1007 assert!(PackageName::new("foo--bar").is_ok());
1008 }
1009
1010 #[test]
1011 fn package_name_rejects_url_reserved() {
1012 for raw in [
1013 "foo?bar",
1014 "foo#bar",
1015 "foo%2Fbar",
1016 "foo:bar",
1017 "foo&bar",
1018 "foo=bar",
1019 "foo+bar",
1020 "foo@bar",
1021 ] {
1022 assert!(
1023 matches!(
1024 PackageName::new(raw).unwrap_err(),
1025 ValidationError::UnsafePackageName(_)
1026 ),
1027 "{raw:?} should be rejected as URL-reserved / outside grammar"
1028 );
1029 }
1030 }
1031
1032 #[test]
1033 fn package_name_rejects_windows_reserved_filename_chars() {
1034 for raw in [
1035 "foo<bar", "foo>bar", "foo|bar", "foo\"bar", "foo*bar", "foo:bar",
1036 ] {
1037 assert!(
1038 matches!(
1039 PackageName::new(raw).unwrap_err(),
1040 ValidationError::UnsafePackageName(_)
1041 ),
1042 "{raw:?} should be rejected as Windows-reserved filename char"
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn package_name_rejects_non_ascii() {
1049 // A grammar limited to ASCII alphanumerics + `_-.` keeps
1050 // the encoding in URLs and tar archives unambiguous.
1051 for raw in ["foo\u{00E9}bar", "\u{4E2D}\u{6587}", "emoji\u{1F600}"] {
1052 assert!(
1053 matches!(
1054 PackageName::new(raw).unwrap_err(),
1055 ValidationError::UnsafePackageName(_)
1056 ),
1057 "{raw:?} should be rejected as non-ASCII"
1058 );
1059 }
1060 }
1061
1062 #[test]
1063 fn package_name_rejects_control_chars() {
1064 for raw in ["foo\u{0000}bar", "foo\u{0007}bar", "foo\u{007F}bar"] {
1065 assert!(PackageName::new(raw).is_err(), "{raw:?} should be rejected");
1066 }
1067 }
1068
1069 #[test]
1070 fn target_name_rejects_empty() {
1071 assert_eq!(
1072 TargetName::new("").unwrap_err(),
1073 ValidationError::EmptyTargetName
1074 );
1075 }
1076
1077 #[test]
1078 fn target_name_rejects_whitespace() {
1079 let err = TargetName::new("a b").unwrap_err();
1080 assert!(matches!(
1081 err,
1082 ValidationError::TargetNameContainsWhitespace(_)
1083 ));
1084 }
1085
1086 /// Symmetric with `package_name_rejects_leading_dash`. Target
1087 /// names eventually thread into argv (cargo flags, archiver
1088 /// inputs); a leading `-` would be ambiguous with a flag.
1089 /// Post-tightening this case is reported as `UnsafeTargetName`
1090 /// because the path-safe predicate rejects leading dashes as
1091 /// part of the same rule that excludes path separators.
1092 #[test]
1093 fn target_name_rejects_leading_dash() {
1094 for raw in ["-foo", "--release", "-"] {
1095 assert!(
1096 matches!(
1097 TargetName::new(raw).unwrap_err(),
1098 ValidationError::UnsafeTargetName(_)
1099 ),
1100 "{raw:?} must be rejected because of the leading `-`"
1101 );
1102 }
1103 // Embedded `-` is still fine.
1104 assert!(TargetName::new("foo-bar").is_ok());
1105 }
1106
1107 /// Target names are joined into object, executable, and Cargo
1108 /// target directory paths by the build planner. A manifest like
1109 /// `[target."/tmp/out"]` would otherwise let an attacker write
1110 /// build artifacts outside the selected `--build-dir`. Reject
1111 /// the full path-component grammar: path separators, parent
1112 /// references, leading dots, absolute paths, drive letters,
1113 /// and non-ASCII bytes.
1114 #[test]
1115 fn target_name_rejects_path_unsafe_values() {
1116 for raw in [
1117 "/foo",
1118 "foo/bar",
1119 "\\foo",
1120 "foo\\bar",
1121 "..",
1122 "../evil",
1123 ".",
1124 ".hidden",
1125 "/tmp/out",
1126 "C:foo",
1127 "foo\u{00E9}bar",
1128 "foo\u{0000}bar",
1129 ] {
1130 assert!(
1131 matches!(
1132 TargetName::new(raw).unwrap_err(),
1133 ValidationError::UnsafeTargetName(_)
1134 ),
1135 "{raw:?} should be rejected as path-unsafe"
1136 );
1137 }
1138 }
1139
1140 #[test]
1141 fn target_name_accepts_path_safe_values() {
1142 for raw in ["foo", "foo-bar", "foo_bar", "foo.bar", "lib1", "a"] {
1143 assert!(TargetName::new(raw).is_ok(), "{raw:?} should be accepted");
1144 }
1145 }
1146
1147 #[test]
1148 fn project_accepts_valid_targets() {
1149 let package = Package::new(
1150 pkg("hello"),
1151 version(),
1152 vec![
1153 target("lib", TargetKind::Library, &[]),
1154 target("exe", TargetKind::Executable, &["lib"]),
1155 ],
1156 Vec::new(),
1157 )
1158 .unwrap();
1159 assert_eq!(package.targets.len(), 2);
1160 assert!(package.dependencies.is_empty());
1161 }
1162
1163 #[test]
1164 fn project_rejects_duplicate_targets() {
1165 let err = Package::new(
1166 pkg("hello"),
1167 version(),
1168 vec![
1169 target("a", TargetKind::Library, &[]),
1170 target("a", TargetKind::Executable, &[]),
1171 ],
1172 Vec::new(),
1173 )
1174 .unwrap_err();
1175 assert_eq!(err, ValidationError::DuplicateTargetName("a".into()));
1176 }
1177
1178 #[test]
1179 fn project_accepts_unknown_target_dep_for_planner_resolution() {
1180 // target-dep existence is resolved by cabin-build against
1181 // the full package graph, so cabin-core no longer rejects unknown
1182 // names here.
1183 let package = Package::new(
1184 pkg("hello"),
1185 version(),
1186 vec![target("exe", TargetKind::Executable, &["external"])],
1187 Vec::new(),
1188 )
1189 .unwrap();
1190 assert_eq!(package.targets[0].deps[0].as_str(), "external");
1191 }
1192
1193 fn dep(name: &str, kind: DependencyKind) -> Dependency {
1194 Dependency {
1195 name: pkg(name),
1196 source: DependencySource::Path(PathBuf::from("../somewhere")),
1197 kind,
1198 optional: false,
1199 features: Vec::new(),
1200 default_features: true,
1201 condition: None,
1202 }
1203 }
1204
1205 #[test]
1206 fn project_rejects_duplicate_dependencies_within_a_kind() {
1207 let err = Package::new(
1208 pkg("hello"),
1209 version(),
1210 Vec::new(),
1211 vec![
1212 dep("greet", DependencyKind::Normal),
1213 dep("greet", DependencyKind::Normal),
1214 ],
1215 )
1216 .unwrap_err();
1217 assert_eq!(
1218 err,
1219 ValidationError::DuplicateDependency {
1220 name: "greet".into(),
1221 kind: DependencyKind::Normal,
1222 }
1223 );
1224 }
1225
1226 #[test]
1227 fn project_accepts_same_name_across_different_kinds() {
1228 // The same package may appear under multiple dependency
1229 // kind sections — that is the documented duplicate policy.
1230 let package = Package::new(
1231 pkg("hello"),
1232 version(),
1233 Vec::new(),
1234 vec![
1235 dep("fmt", DependencyKind::Normal),
1236 dep("fmt", DependencyKind::Dev),
1237 ],
1238 )
1239 .expect("same name across distinct kinds is allowed");
1240 assert_eq!(package.dependencies.len(), 2);
1241 }
1242
1243 #[test]
1244 fn project_rejects_duplicate_system_dependencies() {
1245 let sys = |n: &str| SystemDependency {
1246 name: pkg(n),
1247 version: ">=1".into(),
1248 kind: DependencyKind::Normal,
1249 condition: None,
1250 };
1251 let err = Package::with_config(PackageConfigInput {
1252 name: pkg("hello"),
1253 version: version(),
1254 targets: Vec::new(),
1255 dependencies: Vec::new(),
1256 system_dependencies: vec![sys("zlib"), sys("zlib")],
1257 features: Features::default(),
1258 })
1259 .unwrap_err();
1260 assert_eq!(
1261 err,
1262 ValidationError::DuplicateSystemDependency("zlib".into())
1263 );
1264 }
1265
1266 #[test]
1267 fn dependency_kind_lists_are_consistent() {
1268 // `all()` covers every variant.
1269 let all = DependencyKind::all();
1270 assert_eq!(all.len(), 2);
1271 // Resolution policy: dev is excluded by default.
1272 assert!(DependencyKind::Normal.is_resolved_by_default());
1273 assert!(!DependencyKind::Dev.is_resolved_by_default());
1274 // Linkage policy: only Normal contributes to ordinary builds.
1275 assert!(DependencyKind::Normal.affects_ordinary_build());
1276 assert!(!DependencyKind::Dev.affects_ordinary_build());
1277 }
1278
1279 #[test]
1280 fn target_kind_str_round_trip() {
1281 for kind in TargetKind::all() {
1282 assert_eq!(kind.to_string(), kind.as_str());
1283 }
1284 }
1285
1286 #[test]
1287 fn target_kind_classification_matches_documented_policy() {
1288 // `library` / `executable` are the production surface
1289 // that `cabin build` enumerates by default.
1290 for kind in [TargetKind::Library, TargetKind::Executable] {
1291 assert!(
1292 kind.is_default_buildable(),
1293 "{kind} must be default-buildable"
1294 );
1295 assert!(!kind.is_dev_only(), "{kind} must not be dev-only");
1296 assert!(!kind.is_test(), "{kind} must not be classed as a test");
1297 }
1298 // The dev-only kinds: `cabin build` ignores them; `cabin
1299 // test` runs `test` only.
1300 for kind in [TargetKind::Test, TargetKind::Example] {
1301 assert!(
1302 !kind.is_default_buildable(),
1303 "{kind} must NOT be default-buildable"
1304 );
1305 assert!(kind.is_dev_only(), "{kind} must be dev-only");
1306 assert!(kind.produces_executable(), "{kind} produces an executable");
1307 }
1308 assert!(TargetKind::Test.is_test());
1309 assert!(!TargetKind::Example.is_test());
1310 }
1311
1312 #[test]
1313 fn produces_executable_matches_kind_intent() {
1314 assert!(!TargetKind::Library.produces_executable());
1315 assert!(!TargetKind::HeaderOnly.produces_executable());
1316 assert!(TargetKind::Executable.produces_executable());
1317 assert!(TargetKind::Test.produces_executable());
1318 assert!(TargetKind::Example.produces_executable());
1319 }
1320
1321 #[test]
1322 fn header_only_is_default_buildable_but_produces_nothing() {
1323 // Header-only is included in the default selection so the
1324 // dep-closure walk reaches it, but the planner emits no
1325 // compile / archive / link actions for it.
1326 assert!(TargetKind::HeaderOnly.is_default_buildable());
1327 assert!(TargetKind::HeaderOnly.is_header_only());
1328 assert!(!TargetKind::HeaderOnly.produces_archive());
1329 assert!(!TargetKind::HeaderOnly.produces_executable());
1330 }
1331}