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::language_standard::LanguageStandardsSummary;
25use crate::profile::ResolvedProfile;
26use crate::toolchain::ResolvedToolchain;
27
28pub const DEFAULT_FEATURE_KEY: &str = "default";
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Features {
59 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub default: Vec<String>,
63 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
66 pub features: BTreeMap<String, Vec<String>>,
67}
68
69impl Features {
70 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 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 }
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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum FeatureEntry {
239 Local(String),
241 OptionalDep(String),
244 DepFeature { dep: String, feature: String },
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum InvalidFeatureEntryKind {
256 Empty,
258 EmptyDepName,
260 EmptyDepOrFeature,
262 MultiplePathSeparators,
264 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 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
345pub struct SelectionRequest {
346 pub features: BTreeSet<String>,
349 pub all_features: bool,
350 pub no_default_features: bool,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360pub struct BuildConfiguration {
361 pub enabled_features: BTreeSet<String>,
362 pub profile: ResolvedProfile,
367 pub toolchain: ToolchainSummary,
372 pub build_flags: ResolvedProfileFlags,
376 #[serde(default)]
380 pub language: LanguageStandardsSummary,
381 pub fingerprint: String,
382}
383
384#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
389pub struct ToolchainSummary {
390 pub tools: BTreeMap<String, String>,
395 pub sources: BTreeMap<String, String>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub compiler_wrapper: Option<CompilerWrapperSummary>,
406}
407
408impl ToolchainSummary {
409 pub fn from_resolved(toolchain: &ResolvedToolchain) -> Self {
413 Self::from_resolved_parts(toolchain, None)
414 }
415
416 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#[derive(Debug)]
451pub struct BuildConfigurationInput<'a> {
452 pub package: &'a str,
454 pub features: &'a Features,
456 pub request: &'a SelectionRequest,
458 pub profile: ResolvedProfile,
460 pub toolchain: ToolchainSummary,
462 pub build_flags: ResolvedProfileFlags,
464 pub language: LanguageStandardsSummary,
466}
467
468impl BuildConfiguration {
469 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 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 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 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 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 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 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
694pub(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 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 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 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 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 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 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 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 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 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 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}