1use 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::profile::ResolvedProfile;
25use crate::toolchain::ResolvedToolchain;
26
27pub const DEFAULT_FEATURE_KEY: &str = "default";
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub struct Features {
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 pub default: Vec<String>,
62 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
65 pub features: BTreeMap<String, Vec<String>>,
66}
67
68impl Features {
69 pub fn new(
75 default: Vec<String>,
76 features: BTreeMap<String, Vec<String>>,
77 ) -> Result<Self, ValidationError> {
78 let me = Self { default, features };
79 me.validate()?;
80 Ok(me)
81 }
82
83 pub fn validate(&self) -> Result<(), ValidationError> {
100 if self.features.contains_key(DEFAULT_FEATURE_KEY) {
101 return Err(ValidationError::ReservedFeatureName(
102 DEFAULT_FEATURE_KEY.to_owned(),
103 ));
104 }
105 for name in self.features.keys() {
106 validate_identifier(name)?;
107 }
108 for name in &self.default {
109 validate_identifier(name)?;
110 if !self.features.contains_key(name) {
111 return Err(ValidationError::UnknownFeatureReference {
112 referrer: DEFAULT_FEATURE_KEY.to_owned(),
113 referenced: name.to_owned(),
114 });
115 }
116 }
117 for (name, implies) in &self.features {
118 for raw in implies {
119 let entry = FeatureEntry::parse(raw).map_err(|kind| {
120 ValidationError::InvalidFeatureEntry {
121 referrer: name.clone(),
122 entry: raw.clone(),
123 reason: kind,
124 }
125 })?;
126 match entry {
127 FeatureEntry::Local(local) => {
128 if !self.features.contains_key(&local) {
129 return Err(ValidationError::UnknownFeatureReference {
130 referrer: name.clone(),
131 referenced: local,
132 });
133 }
134 }
135 FeatureEntry::OptionalDep(_) | FeatureEntry::DepFeature { .. } => {
136 }
140 }
141 }
142 }
143 self.detect_cycles()?;
144 Ok(())
145 }
146
147 fn detect_cycles(&self) -> Result<(), ValidationError> {
148 #[derive(Clone, Copy)]
149 enum Color {
150 Visiting,
151 Done,
152 }
153 fn visit<'a>(
154 node: &'a str,
155 features: &'a BTreeMap<String, Vec<String>>,
156 state: &mut std::collections::HashMap<&'a str, Color>,
157 path: &mut Vec<&'a str>,
158 ) -> Result<(), ValidationError> {
159 match state.get(node) {
160 Some(Color::Done) => return Ok(()),
161 Some(Color::Visiting) => {
162 let start = path.iter().position(|n| *n == node).unwrap_or(0);
163 let mut cycle: Vec<String> =
164 path[start..].iter().map(|s| (*s).to_owned()).collect();
165 cycle.push(node.to_owned());
166 return Err(ValidationError::FeatureCycle(cycle));
167 }
168 None => {}
169 }
170 state.insert(node, Color::Visiting);
171 path.push(node);
172 if let Some(implies) = features.get(node) {
173 for r in implies {
174 if let Ok(FeatureEntry::Local(local)) = FeatureEntry::parse(r)
183 && let Some((stored, _)) = features.get_key_value(local.as_str())
184 {
185 visit(stored.as_str(), features, state, path)?;
186 }
187 }
188 }
189 path.pop();
190 state.insert(node, Color::Done);
191 Ok(())
192 }
193 let mut state = std::collections::HashMap::new();
194 let mut path: Vec<&str> = Vec::new();
195 for name in self.features.keys() {
196 visit(name.as_str(), &self.features, &mut state, &mut path)?;
197 }
198 Ok(())
199 }
200
201 pub fn expand(&self, roots: &BTreeSet<String>) -> BTreeSet<String> {
210 let mut out = BTreeSet::new();
211 let mut stack: Vec<String> = roots.iter().cloned().collect();
212 while let Some(name) = stack.pop() {
213 if !out.insert(name.clone()) {
214 continue;
215 }
216 if let Some(implies) = self.features.get(&name) {
217 for raw in implies {
218 if let Ok(FeatureEntry::Local(local)) = FeatureEntry::parse(raw) {
219 stack.push(local);
220 }
221 }
222 }
223 }
224 out
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
237pub enum FeatureEntry {
238 Local(String),
240 OptionalDep(String),
243 DepFeature { dep: String, feature: String },
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum InvalidFeatureEntryKind {
255 Empty,
257 EmptyDepName,
259 EmptyDepOrFeature,
261 MultiplePathSeparators,
263 UnsupportedCharacter(char),
267}
268
269impl InvalidFeatureEntryKind {
270 pub fn message(self) -> &'static str {
271 match self {
272 InvalidFeatureEntryKind::Empty => "feature entries must not be empty",
273 InvalidFeatureEntryKind::EmptyDepName => {
274 "`dep:` entries require a non-empty dependency name"
275 }
276 InvalidFeatureEntryKind::EmptyDepOrFeature => {
277 "`<dep>/<feature>` entries require both a dependency name and a feature name"
278 }
279 InvalidFeatureEntryKind::MultiplePathSeparators => {
280 "feature entries may contain at most one `/`"
281 }
282 InvalidFeatureEntryKind::UnsupportedCharacter(_) => {
283 "feature entries may only use ASCII letters, digits, `_`, `-`, `.`, plus the leading `dep:` or single `/` separator"
284 }
285 }
286 }
287}
288
289impl FeatureEntry {
290 pub fn parse(input: &str) -> Result<Self, InvalidFeatureEntryKind> {
301 if input.is_empty() {
302 return Err(InvalidFeatureEntryKind::Empty);
303 }
304 if let Some(rest) = input.strip_prefix("dep:") {
305 if rest.is_empty() {
306 return Err(InvalidFeatureEntryKind::EmptyDepName);
307 }
308 check_identifier_chars(rest)?;
309 return Ok(FeatureEntry::OptionalDep(rest.to_owned()));
310 }
311 if let Some((dep, feature)) = input.split_once('/') {
312 if feature.contains('/') {
313 return Err(InvalidFeatureEntryKind::MultiplePathSeparators);
314 }
315 if dep.is_empty() || feature.is_empty() {
316 return Err(InvalidFeatureEntryKind::EmptyDepOrFeature);
317 }
318 check_identifier_chars(dep)?;
319 check_identifier_chars(feature)?;
320 return Ok(FeatureEntry::DepFeature {
321 dep: dep.to_owned(),
322 feature: feature.to_owned(),
323 });
324 }
325 check_identifier_chars(input)?;
326 Ok(FeatureEntry::Local(input.to_owned()))
327 }
328}
329
330fn check_identifier_chars(s: &str) -> Result<(), InvalidFeatureEntryKind> {
331 for c in s.chars() {
332 match c {
333 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '.' => {}
334 other => return Err(InvalidFeatureEntryKind::UnsupportedCharacter(other)),
335 }
336 }
337 Ok(())
338}
339
340#[derive(Debug, Clone, Default, PartialEq, Eq)]
344pub struct SelectionRequest {
345 pub features: BTreeSet<String>,
348 pub all_features: bool,
349 pub no_default_features: bool,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub struct BuildConfiguration {
360 pub enabled_features: BTreeSet<String>,
361 pub profile: ResolvedProfile,
366 pub toolchain: ToolchainSummary,
371 pub build_flags: ResolvedProfileFlags,
375 pub fingerprint: String,
376}
377
378#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
383pub struct ToolchainSummary {
384 pub tools: BTreeMap<String, String>,
389 pub sources: BTreeMap<String, String>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub compiler_wrapper: Option<CompilerWrapperSummary>,
400}
401
402impl ToolchainSummary {
403 pub fn from_resolved(toolchain: &ResolvedToolchain) -> Self {
407 Self::from_resolved_parts(toolchain, None)
408 }
409
410 pub fn from_resolved_parts(
415 toolchain: &ResolvedToolchain,
416 wrapper: Option<&ResolvedCompilerWrapper>,
417 ) -> Self {
418 let mut tools = BTreeMap::new();
419 let mut sources = BTreeMap::new();
420 for tool in toolchain.iter() {
421 let key = tool.kind.as_key().to_owned();
422 tools.insert(key.clone(), tool.spec.display());
423 sources.insert(
424 key,
425 crate::toolchain::tool_source_label(tool.source).to_owned(),
426 );
427 }
428 Self {
429 tools,
430 sources,
431 compiler_wrapper: wrapper.map(CompilerWrapperSummary::from_resolved),
432 }
433 }
434}
435
436#[derive(Debug)]
445pub struct BuildConfigurationInput<'a> {
446 pub package: &'a str,
448 pub features: &'a Features,
450 pub request: &'a SelectionRequest,
452 pub profile: ResolvedProfile,
454 pub toolchain: ToolchainSummary,
456 pub build_flags: ResolvedProfileFlags,
458}
459
460impl BuildConfiguration {
461 pub fn resolve(input: BuildConfigurationInput<'_>) -> Result<Self, ValidationError> {
468 let BuildConfigurationInput {
469 package,
470 features,
471 request,
472 profile,
473 toolchain,
474 build_flags,
475 } = input;
476 let enabled_features = resolve_features(package, features, request)?;
477 let fingerprint =
478 compute_fingerprint(&enabled_features, &profile, &toolchain, &build_flags);
479 Ok(Self {
480 enabled_features,
481 profile,
482 toolchain,
483 build_flags,
484 fingerprint,
485 })
486 }
487
488 pub fn as_json(&self) -> serde_json::Value {
491 let compiler_wrapper =
492 self.toolchain
493 .compiler_wrapper
494 .as_ref()
495 .map_or(serde_json::Value::Null, |w| {
496 let mut obj = serde_json::Map::new();
497 obj.insert("kind".to_owned(), serde_json::Value::String(w.kind.clone()));
498 obj.insert("spec".to_owned(), serde_json::Value::String(w.spec.clone()));
499 obj.insert(
500 "source".to_owned(),
501 serde_json::Value::String(w.source.clone()),
502 );
503 if let Some(v) = &w.version {
504 obj.insert("version".to_owned(), serde_json::Value::String(v.clone()));
505 }
506 serde_json::Value::Object(obj)
507 });
508 serde_json::json!({
509 "features": self.enabled_features.iter().collect::<Vec<_>>(),
510 "profile": self.profile.as_json(),
511 "toolchain": {
512 "tools": &self.toolchain.tools,
513 "sources": &self.toolchain.sources,
514 "compiler_wrapper": compiler_wrapper,
515 },
516 "build_flags": self.build_flags.as_json(),
517 "fingerprint": self.fingerprint,
518 })
519 }
520}
521
522fn resolve_features(
523 package: &str,
524 features: &Features,
525 request: &SelectionRequest,
526) -> Result<BTreeSet<String>, ValidationError> {
527 for name in &request.features {
529 if !features.features.contains_key(name) {
530 return Err(ValidationError::UnknownFeature {
531 package: package.to_owned(),
532 feature: name.clone(),
533 });
534 }
535 }
536
537 let mut roots: BTreeSet<String> = BTreeSet::new();
538 if request.all_features {
539 for name in features.features.keys() {
540 roots.insert(name.clone());
541 }
542 } else {
543 if !request.no_default_features {
544 for name in &features.default {
545 roots.insert(name.clone());
546 }
547 }
548 for name in &request.features {
549 roots.insert(name.clone());
550 }
551 }
552 Ok(features.expand(&roots))
553}
554
555fn bool_bytes(b: bool) -> &'static [u8] {
556 if b { b"true" } else { b"false" }
557}
558
559fn compute_fingerprint(
560 features: &BTreeSet<String>,
561 profile: &ResolvedProfile,
562 toolchain: &ToolchainSummary,
563 build_flags: &ResolvedProfileFlags,
564) -> String {
565 let mut hasher = Sha256::new();
568 hasher.update(b"features\n");
569 for f in features {
570 hasher.update(f.as_bytes());
571 hasher.update(b"\n");
572 }
573 hasher.update(b"profile\n");
574 hasher.update(b"name=");
575 hasher.update(profile.name.as_str().as_bytes());
576 hasher.update(b"\n");
577 hasher.update(b"debug=");
578 hasher.update(bool_bytes(profile.debug));
579 hasher.update(b"\n");
580 hasher.update(b"opt-level=");
581 hasher.update(profile.opt_level.as_str().as_bytes());
582 hasher.update(b"\n");
583 hasher.update(b"assertions=");
584 hasher.update(bool_bytes(profile.assertions));
585 hasher.update(b"\n");
586 hasher.update(b"toolchain\n");
587 for (kind, spec) in &toolchain.tools {
588 hasher.update(kind.as_bytes());
589 hasher.update(b"=");
590 hasher.update(spec.as_bytes());
591 hasher.update(b"\n");
592 }
593 hasher.update(b"compiler-wrapper\n");
594 match &toolchain.compiler_wrapper {
595 Some(wrapper) => {
596 hasher.update(b"kind=");
597 hasher.update(wrapper.kind.as_bytes());
598 hasher.update(b"\n");
599 hasher.update(b"spec=");
600 hasher.update(wrapper.spec.as_bytes());
601 hasher.update(b"\n");
602 if let Some(version) = wrapper.version.as_deref() {
603 hasher.update(b"version=");
604 hasher.update(version.as_bytes());
605 hasher.update(b"\n");
606 }
607 }
608 None => {
609 hasher.update(b"kind=none\n");
610 }
611 }
612 hasher.update(b"build-flags\n");
613 hasher.update(b"defines\n");
614 for d in &build_flags.defines {
615 hasher.update(d.as_bytes());
616 hasher.update(b"\n");
617 }
618 hasher.update(b"include-dirs\n");
619 for inc in &build_flags.include_dirs {
620 hasher.update(inc.to_string_lossy().as_bytes());
621 hasher.update(b"\n");
622 }
623 hasher.update(b"language-neutral-compile-args\n");
624 for a in &build_flags.extra_compile_args {
625 hasher.update(a.as_bytes());
626 hasher.update(b"\n");
627 }
628 hasher.update(b"cflags\n");
636 for a in &build_flags.cflags {
637 hasher.update(a.as_bytes());
638 hasher.update(b"\n");
639 }
640 hasher.update(b"cxxflags\n");
641 for a in &build_flags.cxxflags {
642 hasher.update(a.as_bytes());
643 hasher.update(b"\n");
644 }
645 hasher.update(b"ldflags\n");
646 for a in &build_flags.ldflags {
647 hasher.update(a.as_bytes());
648 hasher.update(b"\n");
649 }
650 crate::hash::hex_digest(&hasher.finalize())
651}
652
653fn validate_identifier(name: &str) -> Result<(), ValidationError> {
655 if name.is_empty() {
656 return Err(ValidationError::EmptyConfigName("feature"));
657 }
658 let bad = name.chars().any(|c| {
659 !(c.is_ascii_alphanumeric() || c == '_' || c == '-')
660 || c.is_whitespace()
661 || matches!(c, '/' | '.' | ':')
662 });
663 if bad {
664 return Err(ValidationError::InvalidConfigName {
665 kind: "feature",
666 value: name.to_owned(),
667 });
668 }
669 Ok(())
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use crate::profile::{
676 ProfileDefinition, ProfileName, ProfileSelection, ResolvedProfile, resolve_profile,
677 };
678 use std::path::PathBuf;
679
680 fn dev() -> ResolvedProfile {
681 resolve_profile(
682 &ProfileSelection::default_dev(),
683 &BTreeMap::<ProfileName, ProfileDefinition>::new(),
684 )
685 .expect("built-in dev resolves")
686 }
687
688 fn feats(default: &[&str], pairs: &[(&str, &[&str])]) -> Features {
689 let mut features = BTreeMap::new();
690 for (k, vs) in pairs {
691 features.insert(
692 (*k).to_owned(),
693 vs.iter().map(|s| (*s).to_owned()).collect(),
694 );
695 }
696 Features {
697 default: default.iter().map(|s| (*s).to_owned()).collect(),
698 features,
699 }
700 }
701
702 #[test]
703 fn features_validate_ok_for_simple_decls() {
704 feats(&["simd"], &[("simd", &[]), ("ssl", &[])])
705 .validate()
706 .unwrap();
707 }
708
709 #[test]
710 fn features_reject_reserved_default_key() {
711 let mut f = feats(&[], &[]);
712 f.features.insert("default".into(), vec![]);
713 match f.validate().unwrap_err() {
714 ValidationError::ReservedFeatureName(n) => assert_eq!(n, "default"),
715 other => panic!("expected ReservedFeatureName, got {other:?}"),
716 }
717 }
718
719 #[test]
720 fn features_reject_unknown_default_reference() {
721 match feats(&["nope"], &[("simd", &[])]).validate().unwrap_err() {
722 ValidationError::UnknownFeatureReference { referenced, .. } => {
723 assert_eq!(referenced, "nope");
724 }
725 other => panic!("unexpected: {other:?}"),
726 }
727 }
728
729 #[test]
730 fn features_reject_internal_unknown_reference() {
731 match feats(&[], &[("full", &["ssl"])]).validate().unwrap_err() {
732 ValidationError::UnknownFeatureReference {
733 referrer,
734 referenced,
735 } => {
736 assert_eq!(referrer, "full");
737 assert_eq!(referenced, "ssl");
738 }
739 other => panic!("unexpected: {other:?}"),
740 }
741 }
742
743 #[test]
744 fn features_reject_cycles() {
745 let f = feats(&[], &[("a", &["b"]), ("b", &["a"])]);
746 match f.validate().unwrap_err() {
747 ValidationError::FeatureCycle(cycle) => {
748 assert!(cycle.iter().any(|n| n == "a"));
749 assert!(cycle.iter().any(|n| n == "b"));
750 }
751 other => panic!("unexpected: {other:?}"),
752 }
753 }
754
755 #[test]
756 fn features_reject_invalid_name() {
757 let f = feats(&[], &[("foo/bar", &[])]);
758 match f.validate().unwrap_err() {
759 ValidationError::InvalidConfigName { kind, value } => {
760 assert_eq!(kind, "feature");
761 assert_eq!(value, "foo/bar");
762 }
763 other => panic!("unexpected: {other:?}"),
764 }
765 }
766
767 #[test]
768 fn features_expand_default_set() {
769 let f = feats(
770 &["full"],
771 &[("simd", &[]), ("ssl", &[]), ("full", &["simd", "ssl"])],
772 );
773 f.validate().unwrap();
774 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
775 package: "demo",
776 features: &f,
777 request: &SelectionRequest::default(),
778 profile: dev(),
779 toolchain: ToolchainSummary::default(),
780 build_flags: ResolvedProfileFlags::default(),
781 })
782 .unwrap();
783 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
784 assert_eq!(v, vec!["full", "simd", "ssl"]);
785 }
786
787 #[test]
788 fn no_default_features_drops_defaults() {
789 let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
790 f.validate().unwrap();
791 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
792 package: "demo",
793 features: &f,
794 request: &SelectionRequest {
795 no_default_features: true,
796 ..Default::default()
797 },
798 profile: dev(),
799 toolchain: ToolchainSummary::default(),
800 build_flags: ResolvedProfileFlags::default(),
801 })
802 .unwrap();
803 assert!(cfg.enabled_features.is_empty());
804 }
805
806 #[test]
807 fn explicit_features_are_added() {
808 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
809 f.validate().unwrap();
810 let mut req = SelectionRequest::default();
811 req.features.insert("ssl".into());
812 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
813 package: "demo",
814 features: &f,
815 request: &req,
816 profile: dev(),
817 toolchain: ToolchainSummary::default(),
818 build_flags: ResolvedProfileFlags::default(),
819 })
820 .unwrap();
821 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
822 assert_eq!(v, vec!["ssl"]);
823 }
824
825 #[test]
826 fn all_features_enables_every_declared_feature() {
827 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
828 f.validate().unwrap();
829 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
830 package: "demo",
831 features: &f,
832 request: &SelectionRequest {
833 all_features: true,
834 ..Default::default()
835 },
836 profile: dev(),
837 toolchain: ToolchainSummary::default(),
838 build_flags: ResolvedProfileFlags::default(),
839 })
840 .unwrap();
841 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
842 assert_eq!(v, vec!["simd", "ssl"]);
843 }
844
845 #[test]
846 fn unknown_feature_in_request_errors() {
847 let f = feats(&[], &[("simd", &[])]);
848 let mut req = SelectionRequest::default();
849 req.features.insert("missing".into());
850 match BuildConfiguration::resolve(BuildConfigurationInput {
851 package: "demo",
852 features: &f,
853 request: &req,
854 profile: dev(),
855 toolchain: ToolchainSummary::default(),
856 build_flags: ResolvedProfileFlags::default(),
857 })
858 .unwrap_err()
859 {
860 ValidationError::UnknownFeature { feature, .. } => assert_eq!(feature, "missing"),
861 other => panic!("unexpected: {other:?}"),
862 }
863 }
864
865 #[test]
866 fn fingerprint_is_stable_for_same_inputs() {
867 let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
868 f.validate().unwrap();
869 let cfg1 = BuildConfiguration::resolve(BuildConfigurationInput {
870 package: "demo",
871 features: &f,
872 request: &SelectionRequest::default(),
873 profile: dev(),
874 toolchain: ToolchainSummary::default(),
875 build_flags: ResolvedProfileFlags::default(),
876 })
877 .unwrap();
878 let cfg2 = BuildConfiguration::resolve(BuildConfigurationInput {
879 package: "demo",
880 features: &f,
881 request: &SelectionRequest::default(),
882 profile: dev(),
883 toolchain: ToolchainSummary::default(),
884 build_flags: ResolvedProfileFlags::default(),
885 })
886 .unwrap();
887 assert_eq!(cfg1.fingerprint, cfg2.fingerprint);
888 assert_eq!(cfg1.fingerprint.len(), 64);
889 }
890
891 #[test]
892 fn fingerprint_differs_when_features_change() {
893 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
894 f.validate().unwrap();
895 let mut req = SelectionRequest::default();
896 let cfg_empty = BuildConfiguration::resolve(BuildConfigurationInput {
897 package: "demo",
898 features: &f,
899 request: &req,
900 profile: dev(),
901 toolchain: ToolchainSummary::default(),
902 build_flags: ResolvedProfileFlags::default(),
903 })
904 .unwrap();
905 req.features.insert("simd".into());
906 let cfg_simd = BuildConfiguration::resolve(BuildConfigurationInput {
907 package: "demo",
908 features: &f,
909 request: &req,
910 profile: dev(),
911 toolchain: ToolchainSummary::default(),
912 build_flags: ResolvedProfileFlags::default(),
913 })
914 .unwrap();
915 assert_ne!(cfg_empty.fingerprint, cfg_simd.fingerprint);
916 }
917 fn resolve_with_flags(flags: ResolvedProfileFlags) -> BuildConfiguration {
922 BuildConfiguration::resolve(BuildConfigurationInput {
923 package: "demo",
924 features: &Features::default(),
925 request: &SelectionRequest::default(),
926 profile: dev(),
927 toolchain: ToolchainSummary::default(),
928 build_flags: flags,
929 })
930 .unwrap()
931 }
932
933 #[test]
934 fn fingerprint_differs_when_defines_change() {
935 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
936 let added = resolve_with_flags(ResolvedProfileFlags {
937 defines: vec!["FOO=1".to_owned()],
938 ..ResolvedProfileFlags::default()
939 });
940 assert_ne!(baseline.fingerprint, added.fingerprint);
941 }
942
943 #[test]
944 fn fingerprint_differs_when_include_dirs_change() {
945 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
946 let added = resolve_with_flags(ResolvedProfileFlags {
947 include_dirs: vec![PathBuf::from("include")],
948 ..ResolvedProfileFlags::default()
949 });
950 assert_ne!(baseline.fingerprint, added.fingerprint);
951 }
952
953 #[test]
954 fn fingerprint_differs_when_extra_compile_args_change() {
955 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
956 let added = resolve_with_flags(ResolvedProfileFlags {
957 extra_compile_args: vec!["-Wall".to_owned()],
958 ..ResolvedProfileFlags::default()
959 });
960 assert_ne!(baseline.fingerprint, added.fingerprint);
961 }
962
963 #[test]
964 fn fingerprint_differs_when_cflags_change() {
965 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
973 let added = resolve_with_flags(ResolvedProfileFlags {
974 cflags: vec!["-std=c99".to_owned()],
975 ..ResolvedProfileFlags::default()
976 });
977 assert_ne!(baseline.fingerprint, added.fingerprint);
978 }
979
980 #[test]
981 fn fingerprint_differs_when_cxxflags_change() {
982 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
985 let added = resolve_with_flags(ResolvedProfileFlags {
986 cxxflags: vec!["-fno-rtti".to_owned()],
987 ..ResolvedProfileFlags::default()
988 });
989 assert_ne!(baseline.fingerprint, added.fingerprint);
990 }
991
992 #[test]
993 fn fingerprint_distinguishes_c_only_from_cxx_only_extra_args() {
994 let c_only = resolve_with_flags(ResolvedProfileFlags {
1002 cflags: vec!["-Wsome-warning".to_owned()],
1003 ..ResolvedProfileFlags::default()
1004 });
1005 let cxx_only = resolve_with_flags(ResolvedProfileFlags {
1006 cxxflags: vec!["-Wsome-warning".to_owned()],
1007 ..ResolvedProfileFlags::default()
1008 });
1009 assert_ne!(c_only.fingerprint, cxx_only.fingerprint);
1010 }
1011
1012 #[test]
1013 fn fingerprint_differs_when_ldflags_change() {
1014 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1015 let added = resolve_with_flags(ResolvedProfileFlags {
1016 ldflags: vec!["-Wl,--as-needed".to_owned()],
1017 ..ResolvedProfileFlags::default()
1018 });
1019 assert_ne!(baseline.fingerprint, added.fingerprint);
1020 }
1021
1022 #[test]
1023 fn fingerprint_is_stable_for_same_build_flags() {
1024 let flags = ResolvedProfileFlags {
1028 defines: vec!["FOO=1".to_owned(), "BAR=2".to_owned()],
1029 include_dirs: vec![PathBuf::from("include"), PathBuf::from("vendor/include")],
1030 extra_compile_args: vec!["-Wall".to_owned()],
1031 cflags: vec!["-std=c99".to_owned()],
1032 cxxflags: vec!["-fno-rtti".to_owned()],
1033 ldflags: vec!["-Wl,--as-needed".to_owned()],
1034 };
1035 let a = resolve_with_flags(flags.clone());
1036 let b = resolve_with_flags(flags);
1037 assert_eq!(a.fingerprint, b.fingerprint);
1038 assert_eq!(a.fingerprint.len(), 64, "sha256 hex digest is 64 chars");
1039 }
1040
1041 fn release() -> ResolvedProfile {
1042 use crate::profile::{ProfileDefinition, ProfileName, ProfileSelection, resolve_profile};
1043 resolve_profile(
1044 &ProfileSelection::release_alias(),
1045 &BTreeMap::<ProfileName, ProfileDefinition>::new(),
1046 )
1047 .expect("built-in release resolves")
1048 }
1049
1050 #[test]
1051 fn fingerprint_differs_when_profile_changes() {
1052 let dev_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1053 package: "demo",
1054 features: &Features::default(),
1055 request: &SelectionRequest::default(),
1056 profile: dev(),
1057 toolchain: ToolchainSummary::default(),
1058 build_flags: ResolvedProfileFlags::default(),
1059 })
1060 .unwrap();
1061 let release_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1062 package: "demo",
1063 features: &Features::default(),
1064 request: &SelectionRequest::default(),
1065 profile: release(),
1066 toolchain: ToolchainSummary::default(),
1067 build_flags: ResolvedProfileFlags::default(),
1068 })
1069 .unwrap();
1070 assert_ne!(dev_cfg.fingerprint, release_cfg.fingerprint);
1074 }
1075
1076 #[test]
1077 fn fingerprint_differs_when_toolchain_summary_changes() {
1078 let mut tc_a = ToolchainSummary::default();
1079 tc_a.tools.insert("cxx".to_owned(), "g++".to_owned());
1080 let mut tc_b = ToolchainSummary::default();
1081 tc_b.tools.insert("cxx".to_owned(), "clang++".to_owned());
1082 let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1083 package: "demo",
1084 features: &Features::default(),
1085 request: &SelectionRequest::default(),
1086 profile: dev(),
1087 toolchain: tc_a,
1088 build_flags: ResolvedProfileFlags::default(),
1089 })
1090 .unwrap();
1091 let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1092 package: "demo",
1093 features: &Features::default(),
1094 request: &SelectionRequest::default(),
1095 profile: dev(),
1096 toolchain: tc_b,
1097 build_flags: ResolvedProfileFlags::default(),
1098 })
1099 .unwrap();
1100 assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1101 }
1102
1103 #[test]
1104 fn fingerprint_differs_when_compiler_wrapper_changes() {
1105 let no_wrapper = ToolchainSummary::default();
1106 let with_wrapper = ToolchainSummary {
1107 compiler_wrapper: Some(CompilerWrapperSummary {
1108 kind: "ccache".into(),
1109 spec: "ccache".into(),
1110 source: "cli".into(),
1111 version: Some("4.8.0".into()),
1112 }),
1113 ..ToolchainSummary::default()
1114 };
1115 let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1116 package: "demo",
1117 features: &Features::default(),
1118 request: &SelectionRequest::default(),
1119 profile: dev(),
1120 toolchain: no_wrapper,
1121 build_flags: ResolvedProfileFlags::default(),
1122 })
1123 .unwrap();
1124 let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1125 package: "demo",
1126 features: &Features::default(),
1127 request: &SelectionRequest::default(),
1128 profile: dev(),
1129 toolchain: with_wrapper,
1130 build_flags: ResolvedProfileFlags::default(),
1131 })
1132 .unwrap();
1133 assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1134 }
1135}