Skip to main content

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}