Skip to main content

cabin_core/
config.rs

1//! Features - public, additive, named-boolean capabilities used
2//! to gate optional dependencies and per-edge feature requests.
3//!
4//! A Feature may be enabled by the package's user or by a
5//! downstream consumer.  Feature implication arrows form a directed
6//! graph; the resolver expands defaults plus user requests by
7//! transitive closure.  Feature entries can enable optional
8//! dependencies (`dep:foo`) and request features on dependency
9//! packages (`crate/feature`).
10//!
11//! All declarations live on `cabin_core::Package`.  Selection happens
12//! through [`BuildConfiguration::resolve`], which consumes the
13//! declarations plus a [`SelectionRequest`] (typically built from CLI
14//! flags by `cabin`).
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20
21use crate::build_flags::ResolvedProfileFlags;
22use crate::compiler_wrapper::{CompilerWrapperSummary, ResolvedCompilerWrapper};
23use crate::error::ValidationError;
24use crate::language_standard::LanguageStandardsSummary;
25use crate::profile::ResolvedProfile;
26use crate::toolchain::ResolvedToolchain;
27
28/// The reserved feature group name.  The list of names mapped to this
29/// key in `[features]` is the package's "default" feature set: the
30/// Features Cabin enables when the user does not pass
31/// `--no-default-features`.
32pub const DEFAULT_FEATURE_KEY: &str = "default";
33
34/// `[features]` declarations for a package.
35///
36/// Feature names are stable identifiers.  The `default` group lists
37/// which features are enabled by default; other entries declare
38/// individual features and what enabling them implies.
39///
40/// Each entry on the right-hand side of a feature is a string in
41/// one of three documented forms (parsed lazily into
42/// [`FeatureEntry`] by the feature resolver):
43///
44/// - `"feature_name"` - enables another local feature on the same
45///   package (transitive feature implication).
46/// - `"dep:dependency_name"` - enables an optional Cabin package
47///   dependency declared by this package's `[dependencies]`
48///   table.
49/// - `"dependency_name/feature_name"` - requests a specific
50///   feature on a Cabin package dependency.  If the dependency is
51///   optional, this form also enables it.
52///
53/// The on-disk shape stays a flat list of strings so older
54/// readers and the canonical metadata format remain
55/// byte-identical for packages that only use the local-feature
56/// form.
57#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Features {
59    /// Default features.  Empty when there is no `default` entry in
60    /// `[features]`.
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    pub default: Vec<String>,
63    /// Declared features and their implication lists.  Stored as a
64    /// `BTreeMap` so iteration is deterministic and output is stable.
65    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
66    pub features: BTreeMap<String, Vec<String>>,
67}
68
69impl Features {
70    /// Convenience constructor for `Package::new`-style call sites.
71    ///
72    /// # Errors
73    /// Returns a [`ValidationError`] when the resulting feature set fails
74    /// [`Features::validate`] (see that method for the specific conditions).
75    pub fn new(
76        default: Vec<String>,
77        features: BTreeMap<String, Vec<String>>,
78    ) -> Result<Self, ValidationError> {
79        let me = Self { default, features };
80        me.validate()?;
81        Ok(me)
82    }
83
84    /// Validate identifier grammar, the reserved `default` key,
85    /// internal references between local features, and local
86    /// cycles. `dep:` / `dep/feature` entries are validated for
87    /// grammar only; the *feature resolver* checks that the
88    /// referenced dependency exists, that it is optional when
89    /// `dep:` is used, and that the requested feature exists on
90    /// the dependency package - those checks need the package
91    /// graph and therefore happen one layer up.
92    ///
93    /// # Errors
94    /// Returns [`ValidationError::ReservedFeatureName`] when `default` is used
95    /// as a declared feature, [`ValidationError::UnknownFeatureReference`] for a
96    /// default or implication pointing at an undeclared local feature,
97    /// [`ValidationError::InvalidFeatureEntry`] for a malformed implication
98    /// entry, a cycle error from `Self::detect_cycles`, and any identifier
99    /// grammar error from validating a feature name.
100    pub fn validate(&self) -> Result<(), ValidationError> {
101        if self.features.contains_key(DEFAULT_FEATURE_KEY) {
102            return Err(ValidationError::ReservedFeatureName(
103                DEFAULT_FEATURE_KEY.to_owned(),
104            ));
105        }
106        for name in self.features.keys() {
107            validate_feature_identifier(name)?;
108        }
109        for name in &self.default {
110            validate_feature_identifier(name)?;
111            if !self.features.contains_key(name) {
112                return Err(ValidationError::UnknownFeatureReference {
113                    referrer: DEFAULT_FEATURE_KEY.to_owned(),
114                    referenced: name.to_owned(),
115                });
116            }
117        }
118        for (name, implies) in &self.features {
119            for raw in implies {
120                let entry = FeatureEntry::parse(raw).map_err(|kind| {
121                    ValidationError::InvalidFeatureEntry {
122                        referrer: name.clone(),
123                        entry: raw.clone(),
124                        reason: kind,
125                    }
126                })?;
127                match entry {
128                    FeatureEntry::Local(local) => {
129                        if !self.features.contains_key(&local) {
130                            return Err(ValidationError::UnknownFeatureReference {
131                                referrer: name.clone(),
132                                referenced: local,
133                            });
134                        }
135                    }
136                    FeatureEntry::OptionalDep(_) | FeatureEntry::DepFeature { .. } => {
137                        // Dependency-shaped entries are validated by
138                        // the feature resolver, which has access to
139                        // the dep list.
140                    }
141                }
142            }
143        }
144        self.detect_cycles()?;
145        Ok(())
146    }
147
148    fn detect_cycles(&self) -> Result<(), ValidationError> {
149        #[derive(Clone, Copy)]
150        enum Color {
151            Visiting,
152            Done,
153        }
154        fn visit<'a>(
155            node: &'a str,
156            features: &'a BTreeMap<String, Vec<String>>,
157            state: &mut std::collections::HashMap<&'a str, Color>,
158            path: &mut Vec<&'a str>,
159        ) -> Result<(), ValidationError> {
160            match state.get(node) {
161                Some(Color::Done) => return Ok(()),
162                Some(Color::Visiting) => {
163                    let start = path.iter().position(|n| *n == node).unwrap_or(0);
164                    let mut cycle: Vec<String> =
165                        path[start..].iter().map(|s| (*s).to_owned()).collect();
166                    cycle.push(node.to_owned());
167                    return Err(ValidationError::FeatureCycle(cycle));
168                }
169                None => {}
170            }
171            state.insert(node, Color::Visiting);
172            path.push(node);
173            if let Some(implies) = features.get(node) {
174                for r in implies {
175                    // Cycle detection only follows local-feature
176                    // edges. `dep:` / `dep/feature` entries are
177                    // not part of this package's local feature
178                    // graph and never trigger a local cycle here.
179                    // Look up the referenced name in the existing
180                    // `features` keys (so the borrowed slice
181                    // outlives this call) instead of creating a
182                    // new `String` from the parsed entry.
183                    if let Ok(FeatureEntry::Local(local)) = FeatureEntry::parse(r)
184                        && let Some((stored, _)) = features.get_key_value(local.as_str())
185                    {
186                        visit(stored.as_str(), features, state, path)?;
187                    }
188                }
189            }
190            path.pop();
191            state.insert(node, Color::Done);
192            Ok(())
193        }
194        let mut state = std::collections::HashMap::new();
195        let mut path: Vec<&str> = Vec::new();
196        for name in self.features.keys() {
197            visit(name.as_str(), &self.features, &mut state, &mut path)?;
198        }
199        Ok(())
200    }
201
202    /// Expand a set of root feature names by transitive closure
203    /// over the *local* `features` map.  Caller is responsible for
204    /// ensuring every root is a declared feature.
205    ///
206    /// Entries that take the form `dep:<name>` or `<dep>/<feature>`
207    /// are skipped: they are package-level effects, not local
208    /// features, and are owned by the cross-package feature
209    /// resolver.
210    pub fn expand(&self, roots: &BTreeSet<String>) -> BTreeSet<String> {
211        let mut out = BTreeSet::new();
212        let mut stack: Vec<String> = roots.iter().cloned().collect();
213        while let Some(name) = stack.pop() {
214            if !out.insert(name.clone()) {
215                continue;
216            }
217            if let Some(implies) = self.features.get(&name) {
218                for raw in implies {
219                    if let Ok(FeatureEntry::Local(local)) = FeatureEntry::parse(raw) {
220                        stack.push(local);
221                    }
222                }
223            }
224        }
225        out
226    }
227}
228
229/// Typed view of a single right-hand-side entry in a `[features]`
230/// list (`feature_name`, `dep:dependency_name`, or
231/// `dependency_name/feature_name`).
232///
233/// `cabin-core` parses the form lazily - the on-disk shape stays
234/// the original string - so older readers are unaffected.  The
235/// feature resolver consumes the typed view to decide which
236/// effects an entry has.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum FeatureEntry {
239    /// Enables another local feature on the same package.
240    Local(String),
241    /// Enables an optional Cabin package dependency declared by
242    /// this package.  Spelled `dep:<name>` in the manifest.
243    OptionalDep(String),
244    /// Requests `feature` on `dep`.  If `dep` is optional, this
245    /// also enables it.  Spelled `<dep>/<feature>` in the
246    /// manifest.
247    DepFeature { dep: String, feature: String },
248}
249
250/// Why parsing a feature-list entry failed.  Carried inside
251/// [`ValidationError::InvalidFeatureEntry`] so user errors keep
252/// the original string and the structural reason it was
253/// rejected.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum InvalidFeatureEntryKind {
256    /// The entry was empty.
257    Empty,
258    /// The entry started with `dep:` but the name was empty.
259    EmptyDepName,
260    /// The entry contained a `/` but either side was empty.
261    EmptyDepOrFeature,
262    /// The entry contained more than one `/` separator.
263    MultiplePathSeparators,
264    /// The entry contained a character outside the supported
265    /// alphabet (`A-Z a-z 0-9 _ - .` plus the leading `dep:` or
266    /// single `/` separator).
267    UnsupportedCharacter(char),
268}
269
270impl InvalidFeatureEntryKind {
271    pub fn message(self) -> &'static str {
272        match self {
273            InvalidFeatureEntryKind::Empty => "feature entries must not be empty",
274            InvalidFeatureEntryKind::EmptyDepName => {
275                "`dep:` entries require a non-empty dependency name"
276            }
277            InvalidFeatureEntryKind::EmptyDepOrFeature => {
278                "`<dep>/<feature>` entries require both a dependency name and a feature name"
279            }
280            InvalidFeatureEntryKind::MultiplePathSeparators => {
281                "feature entries may contain at most one `/`"
282            }
283            InvalidFeatureEntryKind::UnsupportedCharacter(_) => {
284                "feature entries may only use ASCII letters, digits, `_`, `-`, `.`, plus the leading `dep:` or single `/` separator"
285            }
286        }
287    }
288}
289
290impl FeatureEntry {
291    /// Parse a single `[features]` value into a typed entry.
292    ///
293    /// # Errors
294    /// Returns [`InvalidFeatureEntryKind::Empty`] for an empty input,
295    /// [`InvalidFeatureEntryKind::EmptyDepName`] for a bare `dep:`,
296    /// [`InvalidFeatureEntryKind::MultiplePathSeparators`] for more than one
297    /// `/`, [`InvalidFeatureEntryKind::EmptyDepOrFeature`] when either side of
298    /// `<dep>/<feature>` is empty, and
299    /// [`InvalidFeatureEntryKind::UnsupportedCharacter`] for a name containing a
300    /// character outside the allowed identifier set.
301    pub fn parse(input: &str) -> Result<Self, InvalidFeatureEntryKind> {
302        if input.is_empty() {
303            return Err(InvalidFeatureEntryKind::Empty);
304        }
305        if let Some(rest) = input.strip_prefix("dep:") {
306            if rest.is_empty() {
307                return Err(InvalidFeatureEntryKind::EmptyDepName);
308            }
309            check_identifier_chars(rest)?;
310            return Ok(FeatureEntry::OptionalDep(rest.to_owned()));
311        }
312        if let Some((dep, feature)) = input.split_once('/') {
313            if feature.contains('/') {
314                return Err(InvalidFeatureEntryKind::MultiplePathSeparators);
315            }
316            if dep.is_empty() || feature.is_empty() {
317                return Err(InvalidFeatureEntryKind::EmptyDepOrFeature);
318            }
319            check_identifier_chars(dep)?;
320            check_identifier_chars(feature)?;
321            return Ok(FeatureEntry::DepFeature {
322                dep: dep.to_owned(),
323                feature: feature.to_owned(),
324            });
325        }
326        check_identifier_chars(input)?;
327        Ok(FeatureEntry::Local(input.to_owned()))
328    }
329}
330
331fn check_identifier_chars(s: &str) -> Result<(), InvalidFeatureEntryKind> {
332    for c in s.chars() {
333        match c {
334            'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '.' => {}
335            other => return Err(InvalidFeatureEntryKind::UnsupportedCharacter(other)),
336        }
337    }
338    Ok(())
339}
340
341/// User-supplied flag inputs that select features.
342/// Built by `cabin` from `--features`, `--all-features`, and
343/// `--no-default-features`.
344#[derive(Debug, Clone, Default, PartialEq, Eq)]
345pub struct SelectionRequest {
346    /// Explicit `--features a,b` entries.  Order does not matter; the
347    /// Resolver normalizes them.
348    pub features: BTreeSet<String>,
349    pub all_features: bool,
350    pub no_default_features: bool,
351}
352
353/// Resolved, validated build configuration.  Drives:
354/// - which features are enabled;
355/// - which profile its compile / link flags come from;
356/// - which toolchain compiled it;
357/// - which semantic build flags applied;
358/// - the deterministic fingerprint that future cache logic can hash on.
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360pub struct BuildConfiguration {
361    pub enabled_features: BTreeSet<String>,
362    /// Resolved profile (e.g. `dev`, `release`, or a custom
363    /// profile inheriting from a built-in).  Always populated:
364    /// every build configuration is associated with exactly one
365    /// profile.
366    pub profile: ResolvedProfile,
367    /// Toolchain summary used for fingerprinting and metadata.
368    /// Recorded as the requested spec + tool source per kind so
369    /// the fingerprint is stable across machines that resolve
370    /// `clang++` to different absolute paths.
371    pub toolchain: ToolchainSummary,
372    /// Resolved per-package build flags.  The metadata view
373    /// reports this directly; the fingerprint includes a
374    /// deterministic digest of every field.
375    pub build_flags: ResolvedProfileFlags,
376    /// Per-package effective language standards (package level plus
377    /// every target).  Values are folded into the fingerprint;
378    /// provenance labels are reporting-only.
379    #[serde(default)]
380    pub language: LanguageStandardsSummary,
381    pub fingerprint: String,
382}
383
384/// Lightweight, non-machine-specific summary of the resolved
385/// toolchain.  Stored on every [`BuildConfiguration`] so the
386/// fingerprint reflects "which compiler did this build use" without
387/// pinning the local absolute path.
388#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
389pub struct ToolchainSummary {
390    /// `(kind -> "<spec>") `, sorted alphabetically by tool key.
391    /// Each entry records the user-visible spelling (`clang++`,
392    /// `/opt/llvm/bin/clang++`, …); absolute resolved paths from
393    /// PATH discovery are deliberately omitted.
394    pub tools: BTreeMap<String, String>,
395    /// `(kind -> source label) ` parallel to `tools`.  Source
396    /// labels are stable strings (`cli`, `env`, `manifest`,
397    /// `manifest-conditional`, `default`).
398    pub sources: BTreeMap<String, String>,
399    /// Optional compiler wrapper (e.g. `ccache`, `sccache`)
400    /// applied on top of the C++ compiler.  `None` when no wrapper
401    /// is selected; otherwise the kind/spec/source/version are
402    /// folded into the configuration fingerprint so a build with a
403    /// different wrapper choice reuses neither cache layer.
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub compiler_wrapper: Option<CompilerWrapperSummary>,
406}
407
408impl ToolchainSummary {
409    /// Build a summary from a `ResolvedToolchain`.  Storage is
410    /// deterministic: tools iterate in sorted [`crate::ToolKind`]
411    /// order via [`ResolvedToolchain::iter`].
412    pub fn from_resolved(toolchain: &ResolvedToolchain) -> Self {
413        Self::from_resolved_parts(toolchain, None)
414    }
415
416    /// Build a summary from a `ResolvedToolchain` plus an optional
417    /// compiler wrapper. The wrapper is normalized into a
418    /// [`CompilerWrapperSummary`] so the fingerprint captures the
419    /// requested wrapper without leaking the local absolute path.
420    pub fn from_resolved_parts(
421        toolchain: &ResolvedToolchain,
422        wrapper: Option<&ResolvedCompilerWrapper>,
423    ) -> Self {
424        let mut tools = BTreeMap::new();
425        let mut sources = BTreeMap::new();
426        for tool in toolchain.iter() {
427            let key = tool.kind.as_key().to_owned();
428            tools.insert(key.clone(), tool.spec.display());
429            sources.insert(
430                key,
431                crate::toolchain::tool_source_label(tool.source).to_owned(),
432            );
433        }
434        Self {
435            tools,
436            sources,
437            compiler_wrapper: wrapper.map(CompilerWrapperSummary::from_resolved),
438        }
439    }
440}
441
442/// Bundled inputs for [`BuildConfiguration::resolve`].
443///
444/// `BuildConfiguration` ties together every per-package input the
445/// resolver evaluates (declared features, the requested selection,
446/// and the already-resolved profile / toolchain / build flags).
447/// Threading them through one struct keeps the call signature stable
448/// as new inputs land and stops `cabin metadata` orchestration from
449/// needing to remember a fixed positional order.
450#[derive(Debug)]
451pub struct BuildConfigurationInput<'a> {
452    /// Package name.  Used only to render clear validation errors.
453    pub package: &'a str,
454    /// Declared `[features]` table for the package.
455    pub features: &'a Features,
456    /// CLI / config selection request (features).
457    pub request: &'a SelectionRequest,
458    /// Already-resolved profile.
459    pub profile: ResolvedProfile,
460    /// Already-resolved toolchain summary.
461    pub toolchain: ToolchainSummary,
462    /// Already-resolved per-profile build flags.
463    pub build_flags: ResolvedProfileFlags,
464    /// Already-resolved per-package language standards summary.
465    pub language: LanguageStandardsSummary,
466}
467
468impl BuildConfiguration {
469    /// Resolve a [`SelectionRequest`] against a set of declarations.
470    /// `input.package` is used only to make error messages clear.
471    ///
472    /// # Errors
473    /// Returns [`ValidationError::UnknownFeature`] when the request names a
474    /// feature not declared in `input.features`.
475    pub fn resolve(input: BuildConfigurationInput<'_>) -> Result<Self, ValidationError> {
476        let BuildConfigurationInput {
477            package,
478            features,
479            request,
480            profile,
481            toolchain,
482            build_flags,
483            language,
484        } = input;
485        let enabled_features = resolve_features(package, features, request)?;
486        let fingerprint = compute_fingerprint(
487            &enabled_features,
488            &profile,
489            &toolchain,
490            &build_flags,
491            &language,
492        );
493        Ok(Self {
494            enabled_features,
495            profile,
496            toolchain,
497            build_flags,
498            language,
499            fingerprint,
500        })
501    }
502
503    /// Combined JSON view used to populate the `cabin metadata`
504    /// Configuration block.
505    pub fn as_json(&self) -> serde_json::Value {
506        let compiler_wrapper =
507            self.toolchain
508                .compiler_wrapper
509                .as_ref()
510                .map_or(serde_json::Value::Null, |w| {
511                    let mut obj = serde_json::Map::new();
512                    obj.insert(
513                        "kind".to_owned(),
514                        serde_json::Value::String(w.kind.as_key().to_owned()),
515                    );
516                    obj.insert("spec".to_owned(), serde_json::Value::String(w.spec.clone()));
517                    obj.insert(
518                        "source".to_owned(),
519                        serde_json::Value::String(w.source.as_key().to_owned()),
520                    );
521                    if let Some(v) = &w.version {
522                        obj.insert("version".to_owned(), serde_json::Value::String(v.clone()));
523                    }
524                    serde_json::Value::Object(obj)
525                });
526        serde_json::json!({
527            "features": self.enabled_features.iter().collect::<Vec<_>>(),
528            "profile": self.profile.as_json(),
529            "toolchain": {
530                "tools": &self.toolchain.tools,
531                "sources": &self.toolchain.sources,
532                "compiler_wrapper": compiler_wrapper,
533            },
534            "build_flags": self.build_flags.as_json(),
535            "language": &self.language,
536            "fingerprint": self.fingerprint,
537        })
538    }
539}
540
541fn resolve_features(
542    package: &str,
543    features: &Features,
544    request: &SelectionRequest,
545) -> Result<BTreeSet<String>, ValidationError> {
546    // Validate every requested name exists.
547    for name in &request.features {
548        if !features.features.contains_key(name) {
549            return Err(ValidationError::UnknownFeature {
550                package: package.to_owned(),
551                feature: name.clone(),
552            });
553        }
554    }
555
556    let mut roots: BTreeSet<String> = BTreeSet::new();
557    if request.all_features {
558        for name in features.features.keys() {
559            roots.insert(name.clone());
560        }
561    } else {
562        if !request.no_default_features {
563            for name in &features.default {
564                roots.insert(name.clone());
565            }
566        }
567        for name in &request.features {
568            roots.insert(name.clone());
569        }
570    }
571    Ok(features.expand(&roots))
572}
573
574fn bool_bytes(b: bool) -> &'static [u8] {
575    if b { b"true" } else { b"false" }
576}
577
578fn compute_fingerprint(
579    features: &BTreeSet<String>,
580    profile: &ResolvedProfile,
581    toolchain: &ToolchainSummary,
582    build_flags: &ResolvedProfileFlags,
583    language: &LanguageStandardsSummary,
584) -> String {
585    // Hash a stable, line-based serialization rather than JSON so the
586    // fingerprint is independent of serialiser whitespace choices.
587    let mut hasher = Sha256::new();
588    hasher.update(b"features\n");
589    for f in features {
590        hasher.update(f.as_bytes());
591        hasher.update(b"\n");
592    }
593    hasher.update(b"profile\n");
594    hasher.update(b"name=");
595    hasher.update(profile.name.as_str().as_bytes());
596    hasher.update(b"\n");
597    hasher.update(b"debug=");
598    hasher.update(bool_bytes(profile.debug));
599    hasher.update(b"\n");
600    hasher.update(b"opt-level=");
601    hasher.update(profile.opt_level.as_str().as_bytes());
602    hasher.update(b"\n");
603    hasher.update(b"assertions=");
604    hasher.update(bool_bytes(profile.assertions));
605    hasher.update(b"\n");
606    hasher.update(b"toolchain\n");
607    for (kind, spec) in &toolchain.tools {
608        hasher.update(kind.as_bytes());
609        hasher.update(b"=");
610        hasher.update(spec.as_bytes());
611        hasher.update(b"\n");
612    }
613    hasher.update(b"compiler-wrapper\n");
614    match &toolchain.compiler_wrapper {
615        Some(wrapper) => {
616            hasher.update(b"kind=");
617            hasher.update(wrapper.kind.as_key().as_bytes());
618            hasher.update(b"\n");
619            hasher.update(b"spec=");
620            hasher.update(wrapper.spec.as_bytes());
621            hasher.update(b"\n");
622            if let Some(version) = wrapper.version.as_deref() {
623                hasher.update(b"version=");
624                hasher.update(version.as_bytes());
625                hasher.update(b"\n");
626            }
627        }
628        None => {
629            hasher.update(b"kind=none\n");
630        }
631    }
632    hasher.update(b"build-flags\n");
633    hasher.update(b"defines\n");
634    for d in &build_flags.defines {
635        hasher.update(d.as_bytes());
636        hasher.update(b"\n");
637    }
638    hasher.update(b"include-dirs\n");
639    for inc in &build_flags.include_dirs {
640        hasher.update(inc.as_str().as_bytes());
641        hasher.update(b"\n");
642    }
643    // System include dirs hash under their own header so the same
644    // directory moving between the user (`-I`) and system
645    // (`-isystem`) slots changes the fingerprint even though the
646    // path bytes are identical.
647    hasher.update(b"system-include-dirs\n");
648    for inc in &build_flags.system_include_dirs {
649        hasher.update(inc.as_str().as_bytes());
650        hasher.update(b"\n");
651    }
652    hasher.update(b"language-neutral-compile-args\n");
653    for a in &build_flags.extra_compile_args {
654        hasher.update(a.as_bytes());
655        hasher.update(b"\n");
656    }
657    // The C-only and C++-only escape hatches change the
658    // generated compile commands and the resulting object
659    // contents, so they must move the fingerprint.  Each section
660    // is anchored by a labeled header so a future addition
661    // (e.g. extra-asm-compile-args) cannot accidentally collide
662    // with one of the existing buckets and produce the same
663    // fingerprint as a different input.
664    hasher.update(b"cflags\n");
665    for a in &build_flags.cflags {
666        hasher.update(a.as_bytes());
667        hasher.update(b"\n");
668    }
669    hasher.update(b"cxxflags\n");
670    for a in &build_flags.cxxflags {
671        hasher.update(a.as_bytes());
672        hasher.update(b"\n");
673    }
674    hasher.update(b"ldflags\n");
675    for a in &build_flags.ldflags {
676        hasher.update(a.as_bytes());
677        hasher.update(b"\n");
678    }
679    hasher.update(b"link-libs\n");
680    for a in &build_flags.link_libs {
681        hasher.update(a.as_bytes());
682        hasher.update(b"\n");
683    }
684    // Effective language standards, values only: provenance labels
685    // are reporting-only and must not move the fingerprint.
686    hasher.update(b"language-standards\n");
687    for line in language.fingerprint_lines() {
688        hasher.update(line.as_bytes());
689        hasher.update(b"\n");
690    }
691    crate::hash::hex_digest(&hasher.finalize())
692}
693
694/// Identifier grammar for feature names.
695pub(crate) fn validate_feature_identifier(name: &str) -> Result<(), ValidationError> {
696    if name.is_empty() {
697        return Err(ValidationError::EmptyConfigName("feature"));
698    }
699    let bad = name.chars().any(|c| {
700        !(c.is_ascii_alphanumeric() || c == '_' || c == '-')
701            || c.is_whitespace()
702            || matches!(c, '/' | '.' | ':')
703    });
704    if bad {
705        return Err(ValidationError::InvalidConfigName {
706            kind: "feature",
707            value: name.to_owned(),
708        });
709    }
710    Ok(())
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use crate::profile::{
717        ProfileDefinition, ProfileName, ProfileSelection, ResolvedProfile, resolve_profile,
718    };
719    use camino::Utf8PathBuf;
720
721    fn dev() -> ResolvedProfile {
722        resolve_profile(
723            &ProfileSelection::default_dev(),
724            &BTreeMap::<ProfileName, ProfileDefinition>::new(),
725        )
726        .expect("built-in dev resolves")
727    }
728
729    fn feats(default: &[&str], pairs: &[(&str, &[&str])]) -> Features {
730        let mut features = BTreeMap::new();
731        for (k, vs) in pairs {
732            features.insert(
733                (*k).to_owned(),
734                vs.iter().map(|s| (*s).to_owned()).collect(),
735            );
736        }
737        Features {
738            default: default.iter().map(|s| (*s).to_owned()).collect(),
739            features,
740        }
741    }
742
743    #[test]
744    fn features_validate_ok_for_simple_decls() {
745        feats(&["simd"], &[("simd", &[]), ("ssl", &[])])
746            .validate()
747            .unwrap();
748    }
749
750    #[test]
751    fn features_reject_reserved_default_key() {
752        let mut f = feats(&[], &[]);
753        f.features.insert("default".into(), vec![]);
754        match f.validate().unwrap_err() {
755            ValidationError::ReservedFeatureName(n) => assert_eq!(n, "default"),
756            other => panic!("expected ReservedFeatureName, got {other:?}"),
757        }
758    }
759
760    #[test]
761    fn features_reject_unknown_default_reference() {
762        match feats(&["nope"], &[("simd", &[])]).validate().unwrap_err() {
763            ValidationError::UnknownFeatureReference { referenced, .. } => {
764                assert_eq!(referenced, "nope");
765            }
766            other => panic!("unexpected: {other:?}"),
767        }
768    }
769
770    #[test]
771    fn features_reject_internal_unknown_reference() {
772        match feats(&[], &[("full", &["ssl"])]).validate().unwrap_err() {
773            ValidationError::UnknownFeatureReference {
774                referrer,
775                referenced,
776            } => {
777                assert_eq!(referrer, "full");
778                assert_eq!(referenced, "ssl");
779            }
780            other => panic!("unexpected: {other:?}"),
781        }
782    }
783
784    #[test]
785    fn features_reject_cycles() {
786        let f = feats(&[], &[("a", &["b"]), ("b", &["a"])]);
787        match f.validate().unwrap_err() {
788            ValidationError::FeatureCycle(cycle) => {
789                assert!(cycle.iter().any(|n| n == "a"));
790                assert!(cycle.iter().any(|n| n == "b"));
791            }
792            other => panic!("unexpected: {other:?}"),
793        }
794    }
795
796    #[test]
797    fn features_reject_invalid_name() {
798        let f = feats(&[], &[("foo/bar", &[])]);
799        match f.validate().unwrap_err() {
800            ValidationError::InvalidConfigName { kind, value } => {
801                assert_eq!(kind, "feature");
802                assert_eq!(value, "foo/bar");
803            }
804            other => panic!("unexpected: {other:?}"),
805        }
806    }
807
808    #[test]
809    fn features_expand_default_set() {
810        let f = feats(
811            &["full"],
812            &[("simd", &[]), ("ssl", &[]), ("full", &["simd", "ssl"])],
813        );
814        f.validate().unwrap();
815        let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
816            package: "demo",
817            features: &f,
818            request: &SelectionRequest::default(),
819            profile: dev(),
820            toolchain: ToolchainSummary::default(),
821            build_flags: ResolvedProfileFlags::default(),
822            language: LanguageStandardsSummary::default(),
823        })
824        .unwrap();
825        let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
826        assert_eq!(v, vec!["full", "simd", "ssl"]);
827    }
828
829    #[test]
830    fn no_default_features_drops_defaults() {
831        let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
832        f.validate().unwrap();
833        let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
834            package: "demo",
835            features: &f,
836            request: &SelectionRequest {
837                no_default_features: true,
838                ..Default::default()
839            },
840            profile: dev(),
841            toolchain: ToolchainSummary::default(),
842            build_flags: ResolvedProfileFlags::default(),
843            language: LanguageStandardsSummary::default(),
844        })
845        .unwrap();
846        assert!(cfg.enabled_features.is_empty());
847    }
848
849    #[test]
850    fn explicit_features_are_added() {
851        let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
852        f.validate().unwrap();
853        let mut req = SelectionRequest::default();
854        req.features.insert("ssl".into());
855        let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
856            package: "demo",
857            features: &f,
858            request: &req,
859            profile: dev(),
860            toolchain: ToolchainSummary::default(),
861            build_flags: ResolvedProfileFlags::default(),
862            language: LanguageStandardsSummary::default(),
863        })
864        .unwrap();
865        let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
866        assert_eq!(v, vec!["ssl"]);
867    }
868
869    #[test]
870    fn all_features_enables_every_declared_feature() {
871        let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
872        f.validate().unwrap();
873        let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
874            package: "demo",
875            features: &f,
876            request: &SelectionRequest {
877                all_features: true,
878                ..Default::default()
879            },
880            profile: dev(),
881            toolchain: ToolchainSummary::default(),
882            build_flags: ResolvedProfileFlags::default(),
883            language: LanguageStandardsSummary::default(),
884        })
885        .unwrap();
886        let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
887        assert_eq!(v, vec!["simd", "ssl"]);
888    }
889
890    #[test]
891    fn unknown_feature_in_request_errors() {
892        let f = feats(&[], &[("simd", &[])]);
893        let mut req = SelectionRequest::default();
894        req.features.insert("missing".into());
895        match BuildConfiguration::resolve(BuildConfigurationInput {
896            package: "demo",
897            features: &f,
898            request: &req,
899            profile: dev(),
900            toolchain: ToolchainSummary::default(),
901            build_flags: ResolvedProfileFlags::default(),
902            language: LanguageStandardsSummary::default(),
903        })
904        .unwrap_err()
905        {
906            ValidationError::UnknownFeature { feature, .. } => assert_eq!(feature, "missing"),
907            other => panic!("unexpected: {other:?}"),
908        }
909    }
910
911    #[test]
912    fn fingerprint_is_stable_for_same_inputs() {
913        let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
914        f.validate().unwrap();
915        let cfg1 = BuildConfiguration::resolve(BuildConfigurationInput {
916            package: "demo",
917            features: &f,
918            request: &SelectionRequest::default(),
919            profile: dev(),
920            toolchain: ToolchainSummary::default(),
921            build_flags: ResolvedProfileFlags::default(),
922            language: LanguageStandardsSummary::default(),
923        })
924        .unwrap();
925        let cfg2 = BuildConfiguration::resolve(BuildConfigurationInput {
926            package: "demo",
927            features: &f,
928            request: &SelectionRequest::default(),
929            profile: dev(),
930            toolchain: ToolchainSummary::default(),
931            build_flags: ResolvedProfileFlags::default(),
932            language: LanguageStandardsSummary::default(),
933        })
934        .unwrap();
935        assert_eq!(cfg1.fingerprint, cfg2.fingerprint);
936        assert_eq!(cfg1.fingerprint.len(), 64);
937    }
938
939    #[test]
940    fn fingerprint_differs_when_features_change() {
941        let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
942        f.validate().unwrap();
943        let mut req = SelectionRequest::default();
944        let cfg_empty = BuildConfiguration::resolve(BuildConfigurationInput {
945            package: "demo",
946            features: &f,
947            request: &req,
948            profile: dev(),
949            toolchain: ToolchainSummary::default(),
950            build_flags: ResolvedProfileFlags::default(),
951            language: LanguageStandardsSummary::default(),
952        })
953        .unwrap();
954        req.features.insert("simd".into());
955        let cfg_simd = BuildConfiguration::resolve(BuildConfigurationInput {
956            package: "demo",
957            features: &f,
958            request: &req,
959            profile: dev(),
960            toolchain: ToolchainSummary::default(),
961            build_flags: ResolvedProfileFlags::default(),
962            language: LanguageStandardsSummary::default(),
963        })
964        .unwrap();
965        assert_ne!(cfg_empty.fingerprint, cfg_simd.fingerprint);
966    }
967    /// Helper: resolve a `BuildConfiguration` with the supplied
968    /// build flags.  Every other input is the boring default so
969    /// the only difference between two calls is the `flags` arg
970    /// - used for the fingerprint-input regression tests below.
971    fn resolve_with_flags(flags: ResolvedProfileFlags) -> BuildConfiguration {
972        BuildConfiguration::resolve(BuildConfigurationInput {
973            package: "demo",
974            features: &Features::default(),
975            request: &SelectionRequest::default(),
976            profile: dev(),
977            toolchain: ToolchainSummary::default(),
978            build_flags: flags,
979            language: LanguageStandardsSummary::default(),
980        })
981        .unwrap()
982    }
983
984    /// Helper: resolve a `BuildConfiguration` with the supplied
985    /// language-standards summary; every other input is the boring
986    /// default.
987    fn resolve_with_language(language: LanguageStandardsSummary) -> BuildConfiguration {
988        BuildConfiguration::resolve(BuildConfigurationInput {
989            package: "demo",
990            features: &Features::default(),
991            request: &SelectionRequest::default(),
992            profile: dev(),
993            toolchain: ToolchainSummary::default(),
994            build_flags: ResolvedProfileFlags::default(),
995            language,
996        })
997        .unwrap()
998    }
999
1000    #[test]
1001    fn fingerprint_differs_when_package_cxx_standard_changes() {
1002        use crate::language_standard::{CxxStandard, LanguageStandardSource, ResolvedStandard};
1003        let baseline = resolve_with_language(LanguageStandardsSummary::default());
1004        let bumped = resolve_with_language(LanguageStandardsSummary {
1005            cxx: Some(ResolvedStandard {
1006                standard: CxxStandard::Cxx20,
1007                source: LanguageStandardSource::Package,
1008            }),
1009            ..Default::default()
1010        });
1011        assert_ne!(baseline.fingerprint, bumped.fingerprint);
1012    }
1013
1014    #[test]
1015    fn fingerprint_differs_when_package_c_standard_changes() {
1016        use crate::language_standard::{CStandard, LanguageStandardSource, ResolvedStandard};
1017        let baseline = resolve_with_language(LanguageStandardsSummary::default());
1018        let bumped = resolve_with_language(LanguageStandardsSummary {
1019            c: Some(ResolvedStandard {
1020                standard: CStandard::C17,
1021                source: LanguageStandardSource::Package,
1022            }),
1023            ..Default::default()
1024        });
1025        assert_ne!(baseline.fingerprint, bumped.fingerprint);
1026    }
1027
1028    #[test]
1029    fn fingerprint_differs_when_target_standard_override_changes() {
1030        use crate::language_standard::{
1031            CxxStandard, LanguageStandardSource, ResolvedStandard, TargetStandardsSummary,
1032        };
1033        let baseline = LanguageStandardsSummary::default();
1034        let mut with_target = baseline.clone();
1035        with_target.targets.insert(
1036            "core".to_owned(),
1037            TargetStandardsSummary {
1038                c: baseline.c,
1039                cxx: Some(ResolvedStandard {
1040                    standard: CxxStandard::Cxx20,
1041                    source: LanguageStandardSource::Target,
1042                }),
1043                ..Default::default()
1044            },
1045        );
1046        let mut inherited = baseline.clone();
1047        inherited.targets.insert(
1048            "core".to_owned(),
1049            TargetStandardsSummary {
1050                c: baseline.c,
1051                cxx: baseline.cxx,
1052                ..Default::default()
1053            },
1054        );
1055        assert_ne!(
1056            resolve_with_language(with_target).fingerprint,
1057            resolve_with_language(inherited).fingerprint
1058        );
1059    }
1060
1061    #[test]
1062    fn fingerprint_differs_when_interface_standard_changes() {
1063        use crate::language_standard::{
1064            CxxStandard, InterfaceRequirement, InterfaceStandard, InterfaceStandardSource,
1065            StandardRequirement, TargetStandardsSummary,
1066        };
1067        let base = LanguageStandardsSummary::default();
1068        let summary_with_interface = |min: CxxStandard| {
1069            let mut s = base.clone();
1070            s.targets.insert(
1071                "core".to_owned(),
1072                TargetStandardsSummary {
1073                    c: base.c,
1074                    cxx: base.cxx,
1075                    interface_cxx: Some(InterfaceStandard {
1076                        requirement: InterfaceRequirement::Requirement(StandardRequirement {
1077                            min,
1078                            max: None,
1079                        }),
1080                        source: InterfaceStandardSource::Target,
1081                    }),
1082                    ..Default::default()
1083                },
1084            );
1085            s
1086        };
1087        assert_ne!(
1088            resolve_with_language(summary_with_interface(CxxStandard::Cxx17)).fingerprint,
1089            resolve_with_language(summary_with_interface(CxxStandard::Cxx14)).fingerprint
1090        );
1091    }
1092
1093    #[test]
1094    fn fingerprint_is_stable_when_only_standard_provenance_differs() {
1095        use crate::language_standard::{CxxStandard, LanguageStandardSource, ResolvedStandard};
1096        let declared = LanguageStandardsSummary {
1097            cxx: Some(ResolvedStandard {
1098                standard: CxxStandard::Cxx17,
1099                source: LanguageStandardSource::Package,
1100            }),
1101            ..Default::default()
1102        };
1103        let mut inherited = declared.clone();
1104        // Same value, different provenance: the fingerprint hashes
1105        // values only.
1106        inherited.cxx = Some(ResolvedStandard {
1107            standard: CxxStandard::Cxx17,
1108            source: LanguageStandardSource::Workspace,
1109        });
1110        assert_eq!(
1111            resolve_with_language(declared).fingerprint,
1112            resolve_with_language(inherited).fingerprint
1113        );
1114    }
1115
1116    #[test]
1117    fn fingerprint_differs_when_defines_change() {
1118        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1119        let added = resolve_with_flags(ResolvedProfileFlags {
1120            defines: vec!["FOO=1".to_owned()],
1121            ..ResolvedProfileFlags::default()
1122        });
1123        assert_ne!(baseline.fingerprint, added.fingerprint);
1124    }
1125
1126    #[test]
1127    fn fingerprint_differs_when_include_dirs_change() {
1128        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1129        let added = resolve_with_flags(ResolvedProfileFlags {
1130            include_dirs: vec![Utf8PathBuf::from("include")],
1131            ..ResolvedProfileFlags::default()
1132        });
1133        assert_ne!(baseline.fingerprint, added.fingerprint);
1134    }
1135
1136    #[test]
1137    fn fingerprint_differs_when_system_include_dirs_change() {
1138        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1139        let added = resolve_with_flags(ResolvedProfileFlags {
1140            system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1141            ..ResolvedProfileFlags::default()
1142        });
1143        assert_ne!(baseline.fingerprint, added.fingerprint);
1144    }
1145
1146    #[test]
1147    fn fingerprint_distinguishes_user_from_system_include_dirs() {
1148        // The same directory spelled as a user include (`-I`) vs a
1149        // system include (`-isystem`) produces a different compile
1150        // command - header search order and warning semantics both
1151        // change - so the slot must move the fingerprint even when
1152        // the path string is identical.
1153        let user = resolve_with_flags(ResolvedProfileFlags {
1154            include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1155            ..ResolvedProfileFlags::default()
1156        });
1157        let system = resolve_with_flags(ResolvedProfileFlags {
1158            system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1159            ..ResolvedProfileFlags::default()
1160        });
1161        assert_ne!(user.fingerprint, system.fingerprint);
1162    }
1163
1164    #[test]
1165    fn fingerprint_differs_when_extra_compile_args_change() {
1166        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1167        let added = resolve_with_flags(ResolvedProfileFlags {
1168            extra_compile_args: vec!["-Wall".to_owned()],
1169            ..ResolvedProfileFlags::default()
1170        });
1171        assert_ne!(baseline.fingerprint, added.fingerprint);
1172    }
1173
1174    #[test]
1175    fn fingerprint_differs_when_cflags_change() {
1176        // The per-language escape hatches must each contribute
1177        // their own fingerprint bucket.  A C compile command's
1178        // argv changes when this slot changes, which means the
1179        // resulting `.o` bytes can change too - a future on-disk
1180        // artifact cache *must* see a different fingerprint or it
1181        // would silently reuse a stale object.  The fingerprint
1182        // must move.
1183        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1184        let added = resolve_with_flags(ResolvedProfileFlags {
1185            cflags: vec!["-std=c99".to_owned()],
1186            ..ResolvedProfileFlags::default()
1187        });
1188        assert_ne!(baseline.fingerprint, added.fingerprint);
1189    }
1190
1191    #[test]
1192    fn fingerprint_differs_when_cxxflags_change() {
1193        // Mirror of the C-only test: a C++ compile command's
1194        // argv must move the fingerprint too.
1195        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1196        let added = resolve_with_flags(ResolvedProfileFlags {
1197            cxxflags: vec!["-fno-rtti".to_owned()],
1198            ..ResolvedProfileFlags::default()
1199        });
1200        assert_ne!(baseline.fingerprint, added.fingerprint);
1201    }
1202
1203    #[test]
1204    fn fingerprint_distinguishes_c_only_from_cxx_only_extra_args() {
1205        // Belt-and-suspenders: putting the *same* flag string in
1206        // the C-only slot vs. the C++-only slot must produce
1207        // different fingerprints because the two slots route to
1208        // different compile commands.  Without this guarantee,
1209        // future cache logic could accidentally serve a C-only
1210        // object for a C++-only request that happens to share an
1211        // argv string.
1212        let c_only = resolve_with_flags(ResolvedProfileFlags {
1213            cflags: vec!["-Wsome-warning".to_owned()],
1214            ..ResolvedProfileFlags::default()
1215        });
1216        let cxx_only = resolve_with_flags(ResolvedProfileFlags {
1217            cxxflags: vec!["-Wsome-warning".to_owned()],
1218            ..ResolvedProfileFlags::default()
1219        });
1220        assert_ne!(c_only.fingerprint, cxx_only.fingerprint);
1221    }
1222
1223    #[test]
1224    fn fingerprint_differs_when_ldflags_change() {
1225        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1226        let added = resolve_with_flags(ResolvedProfileFlags {
1227            ldflags: vec!["-Wl,--as-needed".to_owned()],
1228            ..ResolvedProfileFlags::default()
1229        });
1230        assert_ne!(baseline.fingerprint, added.fingerprint);
1231    }
1232
1233    #[test]
1234    fn fingerprint_differs_when_link_libs_change() {
1235        // `link-libs` change the generated link command, so two
1236        // configurations differing only in their system libraries must
1237        // not share a build fingerprint.
1238        let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1239        let added = resolve_with_flags(ResolvedProfileFlags {
1240            link_libs: vec!["pthread".to_owned()],
1241            ..ResolvedProfileFlags::default()
1242        });
1243        assert_ne!(baseline.fingerprint, added.fingerprint);
1244    }
1245
1246    #[test]
1247    fn fingerprint_is_stable_for_same_build_flags() {
1248        // Determinism: identical inputs produce identical
1249        // fingerprints.  The fingerprint serialiser sorts every
1250        // map / set; this test pins that contract.
1251        let flags = ResolvedProfileFlags {
1252            defines: vec!["FOO=1".to_owned(), "BAR=2".to_owned()],
1253            include_dirs: vec![
1254                Utf8PathBuf::from("include"),
1255                Utf8PathBuf::from("vendor/include"),
1256            ],
1257            system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1258            extra_compile_args: vec!["-Wall".to_owned()],
1259            cflags: vec!["-std=c99".to_owned()],
1260            cxxflags: vec!["-fno-rtti".to_owned()],
1261            ldflags: vec!["-Wl,--as-needed".to_owned()],
1262            link_libs: vec!["pthread".to_owned()],
1263        };
1264        let a = resolve_with_flags(flags.clone());
1265        let b = resolve_with_flags(flags);
1266        assert_eq!(a.fingerprint, b.fingerprint);
1267        assert_eq!(a.fingerprint.len(), 64, "sha256 hex digest is 64 chars");
1268    }
1269
1270    fn release() -> ResolvedProfile {
1271        use crate::profile::{ProfileDefinition, ProfileName, ProfileSelection, resolve_profile};
1272        resolve_profile(
1273            &ProfileSelection::release_alias(),
1274            &BTreeMap::<ProfileName, ProfileDefinition>::new(),
1275        )
1276        .expect("built-in release resolves")
1277    }
1278
1279    #[test]
1280    fn fingerprint_differs_when_profile_changes() {
1281        let dev_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1282            package: "demo",
1283            features: &Features::default(),
1284            request: &SelectionRequest::default(),
1285            profile: dev(),
1286            toolchain: ToolchainSummary::default(),
1287            build_flags: ResolvedProfileFlags::default(),
1288            language: LanguageStandardsSummary::default(),
1289        })
1290        .unwrap();
1291        let release_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1292            package: "demo",
1293            features: &Features::default(),
1294            request: &SelectionRequest::default(),
1295            profile: release(),
1296            toolchain: ToolchainSummary::default(),
1297            build_flags: ResolvedProfileFlags::default(),
1298            language: LanguageStandardsSummary::default(),
1299        })
1300        .unwrap();
1301        // Built-in dev and release differ in opt-level, debug,
1302        // assertions, and name - every field participates in
1303        // the fingerprint, so the digest must move.
1304        assert_ne!(dev_cfg.fingerprint, release_cfg.fingerprint);
1305    }
1306
1307    #[test]
1308    fn fingerprint_differs_when_toolchain_summary_changes() {
1309        let mut tc_a = ToolchainSummary::default();
1310        tc_a.tools.insert("cxx".to_owned(), "g++".to_owned());
1311        let mut tc_b = ToolchainSummary::default();
1312        tc_b.tools.insert("cxx".to_owned(), "clang++".to_owned());
1313        let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1314            package: "demo",
1315            features: &Features::default(),
1316            request: &SelectionRequest::default(),
1317            profile: dev(),
1318            toolchain: tc_a,
1319            build_flags: ResolvedProfileFlags::default(),
1320            language: LanguageStandardsSummary::default(),
1321        })
1322        .unwrap();
1323        let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1324            package: "demo",
1325            features: &Features::default(),
1326            request: &SelectionRequest::default(),
1327            profile: dev(),
1328            toolchain: tc_b,
1329            build_flags: ResolvedProfileFlags::default(),
1330            language: LanguageStandardsSummary::default(),
1331        })
1332        .unwrap();
1333        assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1334    }
1335
1336    #[test]
1337    fn fingerprint_differs_when_compiler_wrapper_changes() {
1338        let no_wrapper = ToolchainSummary::default();
1339        let with_wrapper = ToolchainSummary {
1340            compiler_wrapper: Some(CompilerWrapperSummary {
1341                kind: crate::CompilerWrapperKind::from_spec(&crate::ToolSpec::parse("ccache")),
1342                spec: "ccache".into(),
1343                source: crate::CompilerWrapperSource::Cli,
1344                version: Some("4.8.0".into()),
1345            }),
1346            ..ToolchainSummary::default()
1347        };
1348        let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1349            package: "demo",
1350            features: &Features::default(),
1351            request: &SelectionRequest::default(),
1352            profile: dev(),
1353            toolchain: no_wrapper,
1354            build_flags: ResolvedProfileFlags::default(),
1355            language: LanguageStandardsSummary::default(),
1356        })
1357        .unwrap();
1358        let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1359            package: "demo",
1360            features: &Features::default(),
1361            request: &SelectionRequest::default(),
1362            profile: dev(),
1363            toolchain: with_wrapper,
1364            build_flags: ResolvedProfileFlags::default(),
1365            language: LanguageStandardsSummary::default(),
1366        })
1367        .unwrap();
1368        assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1369    }
1370}