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_identifier(name)?;
108 }
109 for name in &self.default {
110 validate_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("kind".to_owned(), serde_json::Value::String(w.kind.clone()));
513 obj.insert("spec".to_owned(), serde_json::Value::String(w.spec.clone()));
514 obj.insert(
515 "source".to_owned(),
516 serde_json::Value::String(w.source.clone()),
517 );
518 if let Some(v) = &w.version {
519 obj.insert("version".to_owned(), serde_json::Value::String(v.clone()));
520 }
521 serde_json::Value::Object(obj)
522 });
523 serde_json::json!({
524 "features": self.enabled_features.iter().collect::<Vec<_>>(),
525 "profile": self.profile.as_json(),
526 "toolchain": {
527 "tools": &self.toolchain.tools,
528 "sources": &self.toolchain.sources,
529 "compiler_wrapper": compiler_wrapper,
530 },
531 "build_flags": self.build_flags.as_json(),
532 "language": &self.language,
533 "fingerprint": self.fingerprint,
534 })
535 }
536}
537
538fn resolve_features(
539 package: &str,
540 features: &Features,
541 request: &SelectionRequest,
542) -> Result<BTreeSet<String>, ValidationError> {
543 for name in &request.features {
545 if !features.features.contains_key(name) {
546 return Err(ValidationError::UnknownFeature {
547 package: package.to_owned(),
548 feature: name.clone(),
549 });
550 }
551 }
552
553 let mut roots: BTreeSet<String> = BTreeSet::new();
554 if request.all_features {
555 for name in features.features.keys() {
556 roots.insert(name.clone());
557 }
558 } else {
559 if !request.no_default_features {
560 for name in &features.default {
561 roots.insert(name.clone());
562 }
563 }
564 for name in &request.features {
565 roots.insert(name.clone());
566 }
567 }
568 Ok(features.expand(&roots))
569}
570
571fn bool_bytes(b: bool) -> &'static [u8] {
572 if b { b"true" } else { b"false" }
573}
574
575fn compute_fingerprint(
576 features: &BTreeSet<String>,
577 profile: &ResolvedProfile,
578 toolchain: &ToolchainSummary,
579 build_flags: &ResolvedProfileFlags,
580 language: &LanguageStandardsSummary,
581) -> String {
582 let mut hasher = Sha256::new();
585 hasher.update(b"features\n");
586 for f in features {
587 hasher.update(f.as_bytes());
588 hasher.update(b"\n");
589 }
590 hasher.update(b"profile\n");
591 hasher.update(b"name=");
592 hasher.update(profile.name.as_str().as_bytes());
593 hasher.update(b"\n");
594 hasher.update(b"debug=");
595 hasher.update(bool_bytes(profile.debug));
596 hasher.update(b"\n");
597 hasher.update(b"opt-level=");
598 hasher.update(profile.opt_level.as_str().as_bytes());
599 hasher.update(b"\n");
600 hasher.update(b"assertions=");
601 hasher.update(bool_bytes(profile.assertions));
602 hasher.update(b"\n");
603 hasher.update(b"toolchain\n");
604 for (kind, spec) in &toolchain.tools {
605 hasher.update(kind.as_bytes());
606 hasher.update(b"=");
607 hasher.update(spec.as_bytes());
608 hasher.update(b"\n");
609 }
610 hasher.update(b"compiler-wrapper\n");
611 match &toolchain.compiler_wrapper {
612 Some(wrapper) => {
613 hasher.update(b"kind=");
614 hasher.update(wrapper.kind.as_bytes());
615 hasher.update(b"\n");
616 hasher.update(b"spec=");
617 hasher.update(wrapper.spec.as_bytes());
618 hasher.update(b"\n");
619 if let Some(version) = wrapper.version.as_deref() {
620 hasher.update(b"version=");
621 hasher.update(version.as_bytes());
622 hasher.update(b"\n");
623 }
624 }
625 None => {
626 hasher.update(b"kind=none\n");
627 }
628 }
629 hasher.update(b"build-flags\n");
630 hasher.update(b"defines\n");
631 for d in &build_flags.defines {
632 hasher.update(d.as_bytes());
633 hasher.update(b"\n");
634 }
635 hasher.update(b"include-dirs\n");
636 for inc in &build_flags.include_dirs {
637 hasher.update(inc.as_str().as_bytes());
638 hasher.update(b"\n");
639 }
640 hasher.update(b"system-include-dirs\n");
645 for inc in &build_flags.system_include_dirs {
646 hasher.update(inc.as_str().as_bytes());
647 hasher.update(b"\n");
648 }
649 hasher.update(b"language-neutral-compile-args\n");
650 for a in &build_flags.extra_compile_args {
651 hasher.update(a.as_bytes());
652 hasher.update(b"\n");
653 }
654 hasher.update(b"cflags\n");
662 for a in &build_flags.cflags {
663 hasher.update(a.as_bytes());
664 hasher.update(b"\n");
665 }
666 hasher.update(b"cxxflags\n");
667 for a in &build_flags.cxxflags {
668 hasher.update(a.as_bytes());
669 hasher.update(b"\n");
670 }
671 hasher.update(b"ldflags\n");
672 for a in &build_flags.ldflags {
673 hasher.update(a.as_bytes());
674 hasher.update(b"\n");
675 }
676 hasher.update(b"link-libs\n");
677 for a in &build_flags.link_libs {
678 hasher.update(a.as_bytes());
679 hasher.update(b"\n");
680 }
681 hasher.update(b"language-standards\n");
684 for line in language.fingerprint_lines() {
685 hasher.update(line.as_bytes());
686 hasher.update(b"\n");
687 }
688 crate::hash::hex_digest(&hasher.finalize())
689}
690
691fn validate_identifier(name: &str) -> Result<(), ValidationError> {
693 if name.is_empty() {
694 return Err(ValidationError::EmptyConfigName("feature"));
695 }
696 let bad = name.chars().any(|c| {
697 !(c.is_ascii_alphanumeric() || c == '_' || c == '-')
698 || c.is_whitespace()
699 || matches!(c, '/' | '.' | ':')
700 });
701 if bad {
702 return Err(ValidationError::InvalidConfigName {
703 kind: "feature",
704 value: name.to_owned(),
705 });
706 }
707 Ok(())
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::profile::{
714 ProfileDefinition, ProfileName, ProfileSelection, ResolvedProfile, resolve_profile,
715 };
716 use camino::Utf8PathBuf;
717
718 fn dev() -> ResolvedProfile {
719 resolve_profile(
720 &ProfileSelection::default_dev(),
721 &BTreeMap::<ProfileName, ProfileDefinition>::new(),
722 )
723 .expect("built-in dev resolves")
724 }
725
726 fn feats(default: &[&str], pairs: &[(&str, &[&str])]) -> Features {
727 let mut features = BTreeMap::new();
728 for (k, vs) in pairs {
729 features.insert(
730 (*k).to_owned(),
731 vs.iter().map(|s| (*s).to_owned()).collect(),
732 );
733 }
734 Features {
735 default: default.iter().map(|s| (*s).to_owned()).collect(),
736 features,
737 }
738 }
739
740 #[test]
741 fn features_validate_ok_for_simple_decls() {
742 feats(&["simd"], &[("simd", &[]), ("ssl", &[])])
743 .validate()
744 .unwrap();
745 }
746
747 #[test]
748 fn features_reject_reserved_default_key() {
749 let mut f = feats(&[], &[]);
750 f.features.insert("default".into(), vec![]);
751 match f.validate().unwrap_err() {
752 ValidationError::ReservedFeatureName(n) => assert_eq!(n, "default"),
753 other => panic!("expected ReservedFeatureName, got {other:?}"),
754 }
755 }
756
757 #[test]
758 fn features_reject_unknown_default_reference() {
759 match feats(&["nope"], &[("simd", &[])]).validate().unwrap_err() {
760 ValidationError::UnknownFeatureReference { referenced, .. } => {
761 assert_eq!(referenced, "nope");
762 }
763 other => panic!("unexpected: {other:?}"),
764 }
765 }
766
767 #[test]
768 fn features_reject_internal_unknown_reference() {
769 match feats(&[], &[("full", &["ssl"])]).validate().unwrap_err() {
770 ValidationError::UnknownFeatureReference {
771 referrer,
772 referenced,
773 } => {
774 assert_eq!(referrer, "full");
775 assert_eq!(referenced, "ssl");
776 }
777 other => panic!("unexpected: {other:?}"),
778 }
779 }
780
781 #[test]
782 fn features_reject_cycles() {
783 let f = feats(&[], &[("a", &["b"]), ("b", &["a"])]);
784 match f.validate().unwrap_err() {
785 ValidationError::FeatureCycle(cycle) => {
786 assert!(cycle.iter().any(|n| n == "a"));
787 assert!(cycle.iter().any(|n| n == "b"));
788 }
789 other => panic!("unexpected: {other:?}"),
790 }
791 }
792
793 #[test]
794 fn features_reject_invalid_name() {
795 let f = feats(&[], &[("foo/bar", &[])]);
796 match f.validate().unwrap_err() {
797 ValidationError::InvalidConfigName { kind, value } => {
798 assert_eq!(kind, "feature");
799 assert_eq!(value, "foo/bar");
800 }
801 other => panic!("unexpected: {other:?}"),
802 }
803 }
804
805 #[test]
806 fn features_expand_default_set() {
807 let f = feats(
808 &["full"],
809 &[("simd", &[]), ("ssl", &[]), ("full", &["simd", "ssl"])],
810 );
811 f.validate().unwrap();
812 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
813 package: "demo",
814 features: &f,
815 request: &SelectionRequest::default(),
816 profile: dev(),
817 toolchain: ToolchainSummary::default(),
818 build_flags: ResolvedProfileFlags::default(),
819 language: LanguageStandardsSummary::default(),
820 })
821 .unwrap();
822 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
823 assert_eq!(v, vec!["full", "simd", "ssl"]);
824 }
825
826 #[test]
827 fn no_default_features_drops_defaults() {
828 let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
829 f.validate().unwrap();
830 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
831 package: "demo",
832 features: &f,
833 request: &SelectionRequest {
834 no_default_features: true,
835 ..Default::default()
836 },
837 profile: dev(),
838 toolchain: ToolchainSummary::default(),
839 build_flags: ResolvedProfileFlags::default(),
840 language: LanguageStandardsSummary::default(),
841 })
842 .unwrap();
843 assert!(cfg.enabled_features.is_empty());
844 }
845
846 #[test]
847 fn explicit_features_are_added() {
848 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
849 f.validate().unwrap();
850 let mut req = SelectionRequest::default();
851 req.features.insert("ssl".into());
852 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
853 package: "demo",
854 features: &f,
855 request: &req,
856 profile: dev(),
857 toolchain: ToolchainSummary::default(),
858 build_flags: ResolvedProfileFlags::default(),
859 language: LanguageStandardsSummary::default(),
860 })
861 .unwrap();
862 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
863 assert_eq!(v, vec!["ssl"]);
864 }
865
866 #[test]
867 fn all_features_enables_every_declared_feature() {
868 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
869 f.validate().unwrap();
870 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
871 package: "demo",
872 features: &f,
873 request: &SelectionRequest {
874 all_features: true,
875 ..Default::default()
876 },
877 profile: dev(),
878 toolchain: ToolchainSummary::default(),
879 build_flags: ResolvedProfileFlags::default(),
880 language: LanguageStandardsSummary::default(),
881 })
882 .unwrap();
883 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
884 assert_eq!(v, vec!["simd", "ssl"]);
885 }
886
887 #[test]
888 fn unknown_feature_in_request_errors() {
889 let f = feats(&[], &[("simd", &[])]);
890 let mut req = SelectionRequest::default();
891 req.features.insert("missing".into());
892 match BuildConfiguration::resolve(BuildConfigurationInput {
893 package: "demo",
894 features: &f,
895 request: &req,
896 profile: dev(),
897 toolchain: ToolchainSummary::default(),
898 build_flags: ResolvedProfileFlags::default(),
899 language: LanguageStandardsSummary::default(),
900 })
901 .unwrap_err()
902 {
903 ValidationError::UnknownFeature { feature, .. } => assert_eq!(feature, "missing"),
904 other => panic!("unexpected: {other:?}"),
905 }
906 }
907
908 #[test]
909 fn fingerprint_is_stable_for_same_inputs() {
910 let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
911 f.validate().unwrap();
912 let cfg1 = BuildConfiguration::resolve(BuildConfigurationInput {
913 package: "demo",
914 features: &f,
915 request: &SelectionRequest::default(),
916 profile: dev(),
917 toolchain: ToolchainSummary::default(),
918 build_flags: ResolvedProfileFlags::default(),
919 language: LanguageStandardsSummary::default(),
920 })
921 .unwrap();
922 let cfg2 = BuildConfiguration::resolve(BuildConfigurationInput {
923 package: "demo",
924 features: &f,
925 request: &SelectionRequest::default(),
926 profile: dev(),
927 toolchain: ToolchainSummary::default(),
928 build_flags: ResolvedProfileFlags::default(),
929 language: LanguageStandardsSummary::default(),
930 })
931 .unwrap();
932 assert_eq!(cfg1.fingerprint, cfg2.fingerprint);
933 assert_eq!(cfg1.fingerprint.len(), 64);
934 }
935
936 #[test]
937 fn fingerprint_differs_when_features_change() {
938 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
939 f.validate().unwrap();
940 let mut req = SelectionRequest::default();
941 let cfg_empty = BuildConfiguration::resolve(BuildConfigurationInput {
942 package: "demo",
943 features: &f,
944 request: &req,
945 profile: dev(),
946 toolchain: ToolchainSummary::default(),
947 build_flags: ResolvedProfileFlags::default(),
948 language: LanguageStandardsSummary::default(),
949 })
950 .unwrap();
951 req.features.insert("simd".into());
952 let cfg_simd = BuildConfiguration::resolve(BuildConfigurationInput {
953 package: "demo",
954 features: &f,
955 request: &req,
956 profile: dev(),
957 toolchain: ToolchainSummary::default(),
958 build_flags: ResolvedProfileFlags::default(),
959 language: LanguageStandardsSummary::default(),
960 })
961 .unwrap();
962 assert_ne!(cfg_empty.fingerprint, cfg_simd.fingerprint);
963 }
964 fn resolve_with_flags(flags: ResolvedProfileFlags) -> BuildConfiguration {
969 BuildConfiguration::resolve(BuildConfigurationInput {
970 package: "demo",
971 features: &Features::default(),
972 request: &SelectionRequest::default(),
973 profile: dev(),
974 toolchain: ToolchainSummary::default(),
975 build_flags: flags,
976 language: LanguageStandardsSummary::default(),
977 })
978 .unwrap()
979 }
980
981 fn resolve_with_language(language: LanguageStandardsSummary) -> BuildConfiguration {
985 BuildConfiguration::resolve(BuildConfigurationInput {
986 package: "demo",
987 features: &Features::default(),
988 request: &SelectionRequest::default(),
989 profile: dev(),
990 toolchain: ToolchainSummary::default(),
991 build_flags: ResolvedProfileFlags::default(),
992 language,
993 })
994 .unwrap()
995 }
996
997 #[test]
998 fn fingerprint_differs_when_package_cxx_standard_changes() {
999 use crate::language_standard::{CxxStandard, LanguageStandardSource, ResolvedStandard};
1000 let baseline = resolve_with_language(LanguageStandardsSummary::default());
1001 let bumped = resolve_with_language(LanguageStandardsSummary {
1002 cxx: ResolvedStandard {
1003 standard: CxxStandard::Cxx20,
1004 source: LanguageStandardSource::Package,
1005 },
1006 ..Default::default()
1007 });
1008 assert_ne!(baseline.fingerprint, bumped.fingerprint);
1009 }
1010
1011 #[test]
1012 fn fingerprint_differs_when_package_c_standard_changes() {
1013 use crate::language_standard::{CStandard, LanguageStandardSource, ResolvedStandard};
1014 let baseline = resolve_with_language(LanguageStandardsSummary::default());
1015 let bumped = resolve_with_language(LanguageStandardsSummary {
1016 c: ResolvedStandard {
1017 standard: CStandard::C17,
1018 source: LanguageStandardSource::Package,
1019 },
1020 ..Default::default()
1021 });
1022 assert_ne!(baseline.fingerprint, bumped.fingerprint);
1023 }
1024
1025 #[test]
1026 fn fingerprint_differs_when_target_standard_override_changes() {
1027 use crate::language_standard::{
1028 CxxStandard, LanguageStandardSource, ResolvedStandard, TargetStandardsSummary,
1029 };
1030 let baseline = LanguageStandardsSummary::default();
1031 let mut with_target = baseline.clone();
1032 with_target.targets.insert(
1033 "core".to_owned(),
1034 TargetStandardsSummary {
1035 c: baseline.c,
1036 cxx: ResolvedStandard {
1037 standard: CxxStandard::Cxx20,
1038 source: LanguageStandardSource::Target,
1039 },
1040 interface_c: None,
1041 interface_cxx: None,
1042 },
1043 );
1044 let mut inherited = baseline.clone();
1045 inherited.targets.insert(
1046 "core".to_owned(),
1047 TargetStandardsSummary {
1048 c: baseline.c,
1049 cxx: baseline.cxx,
1050 interface_c: None,
1051 interface_cxx: None,
1052 },
1053 );
1054 assert_ne!(
1055 resolve_with_language(with_target).fingerprint,
1056 resolve_with_language(inherited).fingerprint
1057 );
1058 }
1059
1060 #[test]
1061 fn fingerprint_differs_when_interface_standard_changes() {
1062 use crate::language_standard::{
1063 CxxStandard, InterfaceStandard, InterfaceStandardSource, TargetStandardsSummary,
1064 };
1065 let base = LanguageStandardsSummary::default();
1066 let summary_with_interface = |standard: CxxStandard| {
1067 let mut s = base.clone();
1068 s.targets.insert(
1069 "core".to_owned(),
1070 TargetStandardsSummary {
1071 c: base.c,
1072 cxx: base.cxx,
1073 interface_c: None,
1074 interface_cxx: Some(InterfaceStandard {
1075 standard,
1076 source: InterfaceStandardSource::Target,
1077 }),
1078 },
1079 );
1080 s
1081 };
1082 assert_ne!(
1083 resolve_with_language(summary_with_interface(CxxStandard::Cxx17)).fingerprint,
1084 resolve_with_language(summary_with_interface(CxxStandard::Cxx14)).fingerprint
1085 );
1086 }
1087
1088 #[test]
1089 fn fingerprint_is_stable_when_only_standard_provenance_differs() {
1090 use crate::language_standard::{LanguageStandardSource, ResolvedStandard};
1091 let builtin = LanguageStandardsSummary::default();
1092 let mut declared = builtin.clone();
1093 declared.cxx = ResolvedStandard {
1096 standard: declared.cxx.standard,
1097 source: LanguageStandardSource::Package,
1098 };
1099 assert_eq!(
1100 resolve_with_language(builtin).fingerprint,
1101 resolve_with_language(declared).fingerprint
1102 );
1103 }
1104
1105 #[test]
1106 fn fingerprint_differs_when_defines_change() {
1107 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1108 let added = resolve_with_flags(ResolvedProfileFlags {
1109 defines: vec!["FOO=1".to_owned()],
1110 ..ResolvedProfileFlags::default()
1111 });
1112 assert_ne!(baseline.fingerprint, added.fingerprint);
1113 }
1114
1115 #[test]
1116 fn fingerprint_differs_when_include_dirs_change() {
1117 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1118 let added = resolve_with_flags(ResolvedProfileFlags {
1119 include_dirs: vec![Utf8PathBuf::from("include")],
1120 ..ResolvedProfileFlags::default()
1121 });
1122 assert_ne!(baseline.fingerprint, added.fingerprint);
1123 }
1124
1125 #[test]
1126 fn fingerprint_differs_when_system_include_dirs_change() {
1127 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1128 let added = resolve_with_flags(ResolvedProfileFlags {
1129 system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1130 ..ResolvedProfileFlags::default()
1131 });
1132 assert_ne!(baseline.fingerprint, added.fingerprint);
1133 }
1134
1135 #[test]
1136 fn fingerprint_distinguishes_user_from_system_include_dirs() {
1137 let user = resolve_with_flags(ResolvedProfileFlags {
1143 include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1144 ..ResolvedProfileFlags::default()
1145 });
1146 let system = resolve_with_flags(ResolvedProfileFlags {
1147 system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1148 ..ResolvedProfileFlags::default()
1149 });
1150 assert_ne!(user.fingerprint, system.fingerprint);
1151 }
1152
1153 #[test]
1154 fn fingerprint_differs_when_extra_compile_args_change() {
1155 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1156 let added = resolve_with_flags(ResolvedProfileFlags {
1157 extra_compile_args: vec!["-Wall".to_owned()],
1158 ..ResolvedProfileFlags::default()
1159 });
1160 assert_ne!(baseline.fingerprint, added.fingerprint);
1161 }
1162
1163 #[test]
1164 fn fingerprint_differs_when_cflags_change() {
1165 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1173 let added = resolve_with_flags(ResolvedProfileFlags {
1174 cflags: vec!["-std=c99".to_owned()],
1175 ..ResolvedProfileFlags::default()
1176 });
1177 assert_ne!(baseline.fingerprint, added.fingerprint);
1178 }
1179
1180 #[test]
1181 fn fingerprint_differs_when_cxxflags_change() {
1182 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1185 let added = resolve_with_flags(ResolvedProfileFlags {
1186 cxxflags: vec!["-fno-rtti".to_owned()],
1187 ..ResolvedProfileFlags::default()
1188 });
1189 assert_ne!(baseline.fingerprint, added.fingerprint);
1190 }
1191
1192 #[test]
1193 fn fingerprint_distinguishes_c_only_from_cxx_only_extra_args() {
1194 let c_only = resolve_with_flags(ResolvedProfileFlags {
1202 cflags: vec!["-Wsome-warning".to_owned()],
1203 ..ResolvedProfileFlags::default()
1204 });
1205 let cxx_only = resolve_with_flags(ResolvedProfileFlags {
1206 cxxflags: vec!["-Wsome-warning".to_owned()],
1207 ..ResolvedProfileFlags::default()
1208 });
1209 assert_ne!(c_only.fingerprint, cxx_only.fingerprint);
1210 }
1211
1212 #[test]
1213 fn fingerprint_differs_when_ldflags_change() {
1214 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1215 let added = resolve_with_flags(ResolvedProfileFlags {
1216 ldflags: vec!["-Wl,--as-needed".to_owned()],
1217 ..ResolvedProfileFlags::default()
1218 });
1219 assert_ne!(baseline.fingerprint, added.fingerprint);
1220 }
1221
1222 #[test]
1223 fn fingerprint_differs_when_link_libs_change() {
1224 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1228 let added = resolve_with_flags(ResolvedProfileFlags {
1229 link_libs: vec!["pthread".to_owned()],
1230 ..ResolvedProfileFlags::default()
1231 });
1232 assert_ne!(baseline.fingerprint, added.fingerprint);
1233 }
1234
1235 #[test]
1236 fn fingerprint_is_stable_for_same_build_flags() {
1237 let flags = ResolvedProfileFlags {
1241 defines: vec!["FOO=1".to_owned(), "BAR=2".to_owned()],
1242 include_dirs: vec![
1243 Utf8PathBuf::from("include"),
1244 Utf8PathBuf::from("vendor/include"),
1245 ],
1246 system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
1247 extra_compile_args: vec!["-Wall".to_owned()],
1248 cflags: vec!["-std=c99".to_owned()],
1249 cxxflags: vec!["-fno-rtti".to_owned()],
1250 ldflags: vec!["-Wl,--as-needed".to_owned()],
1251 link_libs: vec!["pthread".to_owned()],
1252 };
1253 let a = resolve_with_flags(flags.clone());
1254 let b = resolve_with_flags(flags);
1255 assert_eq!(a.fingerprint, b.fingerprint);
1256 assert_eq!(a.fingerprint.len(), 64, "sha256 hex digest is 64 chars");
1257 }
1258
1259 fn release() -> ResolvedProfile {
1260 use crate::profile::{ProfileDefinition, ProfileName, ProfileSelection, resolve_profile};
1261 resolve_profile(
1262 &ProfileSelection::release_alias(),
1263 &BTreeMap::<ProfileName, ProfileDefinition>::new(),
1264 )
1265 .expect("built-in release resolves")
1266 }
1267
1268 #[test]
1269 fn fingerprint_differs_when_profile_changes() {
1270 let dev_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1271 package: "demo",
1272 features: &Features::default(),
1273 request: &SelectionRequest::default(),
1274 profile: dev(),
1275 toolchain: ToolchainSummary::default(),
1276 build_flags: ResolvedProfileFlags::default(),
1277 language: LanguageStandardsSummary::default(),
1278 })
1279 .unwrap();
1280 let release_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1281 package: "demo",
1282 features: &Features::default(),
1283 request: &SelectionRequest::default(),
1284 profile: release(),
1285 toolchain: ToolchainSummary::default(),
1286 build_flags: ResolvedProfileFlags::default(),
1287 language: LanguageStandardsSummary::default(),
1288 })
1289 .unwrap();
1290 assert_ne!(dev_cfg.fingerprint, release_cfg.fingerprint);
1294 }
1295
1296 #[test]
1297 fn fingerprint_differs_when_toolchain_summary_changes() {
1298 let mut tc_a = ToolchainSummary::default();
1299 tc_a.tools.insert("cxx".to_owned(), "g++".to_owned());
1300 let mut tc_b = ToolchainSummary::default();
1301 tc_b.tools.insert("cxx".to_owned(), "clang++".to_owned());
1302 let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1303 package: "demo",
1304 features: &Features::default(),
1305 request: &SelectionRequest::default(),
1306 profile: dev(),
1307 toolchain: tc_a,
1308 build_flags: ResolvedProfileFlags::default(),
1309 language: LanguageStandardsSummary::default(),
1310 })
1311 .unwrap();
1312 let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1313 package: "demo",
1314 features: &Features::default(),
1315 request: &SelectionRequest::default(),
1316 profile: dev(),
1317 toolchain: tc_b,
1318 build_flags: ResolvedProfileFlags::default(),
1319 language: LanguageStandardsSummary::default(),
1320 })
1321 .unwrap();
1322 assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1323 }
1324
1325 #[test]
1326 fn fingerprint_differs_when_compiler_wrapper_changes() {
1327 let no_wrapper = ToolchainSummary::default();
1328 let with_wrapper = ToolchainSummary {
1329 compiler_wrapper: Some(CompilerWrapperSummary {
1330 kind: "ccache".into(),
1331 spec: "ccache".into(),
1332 source: "cli".into(),
1333 version: Some("4.8.0".into()),
1334 }),
1335 ..ToolchainSummary::default()
1336 };
1337 let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1338 package: "demo",
1339 features: &Features::default(),
1340 request: &SelectionRequest::default(),
1341 profile: dev(),
1342 toolchain: no_wrapper,
1343 build_flags: ResolvedProfileFlags::default(),
1344 language: LanguageStandardsSummary::default(),
1345 })
1346 .unwrap();
1347 let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1348 package: "demo",
1349 features: &Features::default(),
1350 request: &SelectionRequest::default(),
1351 profile: dev(),
1352 toolchain: with_wrapper,
1353 build_flags: ResolvedProfileFlags::default(),
1354 language: LanguageStandardsSummary::default(),
1355 })
1356 .unwrap();
1357 assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1358 }
1359}