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.as_str().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 hasher.update(b"link-libs\n");
651 for a in &build_flags.link_libs {
652 hasher.update(a.as_bytes());
653 hasher.update(b"\n");
654 }
655 crate::hash::hex_digest(&hasher.finalize())
656}
657
658fn validate_identifier(name: &str) -> Result<(), ValidationError> {
660 if name.is_empty() {
661 return Err(ValidationError::EmptyConfigName("feature"));
662 }
663 let bad = name.chars().any(|c| {
664 !(c.is_ascii_alphanumeric() || c == '_' || c == '-')
665 || c.is_whitespace()
666 || matches!(c, '/' | '.' | ':')
667 });
668 if bad {
669 return Err(ValidationError::InvalidConfigName {
670 kind: "feature",
671 value: name.to_owned(),
672 });
673 }
674 Ok(())
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use crate::profile::{
681 ProfileDefinition, ProfileName, ProfileSelection, ResolvedProfile, resolve_profile,
682 };
683 use camino::Utf8PathBuf;
684
685 fn dev() -> ResolvedProfile {
686 resolve_profile(
687 &ProfileSelection::default_dev(),
688 &BTreeMap::<ProfileName, ProfileDefinition>::new(),
689 )
690 .expect("built-in dev resolves")
691 }
692
693 fn feats(default: &[&str], pairs: &[(&str, &[&str])]) -> Features {
694 let mut features = BTreeMap::new();
695 for (k, vs) in pairs {
696 features.insert(
697 (*k).to_owned(),
698 vs.iter().map(|s| (*s).to_owned()).collect(),
699 );
700 }
701 Features {
702 default: default.iter().map(|s| (*s).to_owned()).collect(),
703 features,
704 }
705 }
706
707 #[test]
708 fn features_validate_ok_for_simple_decls() {
709 feats(&["simd"], &[("simd", &[]), ("ssl", &[])])
710 .validate()
711 .unwrap();
712 }
713
714 #[test]
715 fn features_reject_reserved_default_key() {
716 let mut f = feats(&[], &[]);
717 f.features.insert("default".into(), vec![]);
718 match f.validate().unwrap_err() {
719 ValidationError::ReservedFeatureName(n) => assert_eq!(n, "default"),
720 other => panic!("expected ReservedFeatureName, got {other:?}"),
721 }
722 }
723
724 #[test]
725 fn features_reject_unknown_default_reference() {
726 match feats(&["nope"], &[("simd", &[])]).validate().unwrap_err() {
727 ValidationError::UnknownFeatureReference { referenced, .. } => {
728 assert_eq!(referenced, "nope");
729 }
730 other => panic!("unexpected: {other:?}"),
731 }
732 }
733
734 #[test]
735 fn features_reject_internal_unknown_reference() {
736 match feats(&[], &[("full", &["ssl"])]).validate().unwrap_err() {
737 ValidationError::UnknownFeatureReference {
738 referrer,
739 referenced,
740 } => {
741 assert_eq!(referrer, "full");
742 assert_eq!(referenced, "ssl");
743 }
744 other => panic!("unexpected: {other:?}"),
745 }
746 }
747
748 #[test]
749 fn features_reject_cycles() {
750 let f = feats(&[], &[("a", &["b"]), ("b", &["a"])]);
751 match f.validate().unwrap_err() {
752 ValidationError::FeatureCycle(cycle) => {
753 assert!(cycle.iter().any(|n| n == "a"));
754 assert!(cycle.iter().any(|n| n == "b"));
755 }
756 other => panic!("unexpected: {other:?}"),
757 }
758 }
759
760 #[test]
761 fn features_reject_invalid_name() {
762 let f = feats(&[], &[("foo/bar", &[])]);
763 match f.validate().unwrap_err() {
764 ValidationError::InvalidConfigName { kind, value } => {
765 assert_eq!(kind, "feature");
766 assert_eq!(value, "foo/bar");
767 }
768 other => panic!("unexpected: {other:?}"),
769 }
770 }
771
772 #[test]
773 fn features_expand_default_set() {
774 let f = feats(
775 &["full"],
776 &[("simd", &[]), ("ssl", &[]), ("full", &["simd", "ssl"])],
777 );
778 f.validate().unwrap();
779 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
780 package: "demo",
781 features: &f,
782 request: &SelectionRequest::default(),
783 profile: dev(),
784 toolchain: ToolchainSummary::default(),
785 build_flags: ResolvedProfileFlags::default(),
786 })
787 .unwrap();
788 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
789 assert_eq!(v, vec!["full", "simd", "ssl"]);
790 }
791
792 #[test]
793 fn no_default_features_drops_defaults() {
794 let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
795 f.validate().unwrap();
796 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
797 package: "demo",
798 features: &f,
799 request: &SelectionRequest {
800 no_default_features: true,
801 ..Default::default()
802 },
803 profile: dev(),
804 toolchain: ToolchainSummary::default(),
805 build_flags: ResolvedProfileFlags::default(),
806 })
807 .unwrap();
808 assert!(cfg.enabled_features.is_empty());
809 }
810
811 #[test]
812 fn explicit_features_are_added() {
813 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
814 f.validate().unwrap();
815 let mut req = SelectionRequest::default();
816 req.features.insert("ssl".into());
817 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
818 package: "demo",
819 features: &f,
820 request: &req,
821 profile: dev(),
822 toolchain: ToolchainSummary::default(),
823 build_flags: ResolvedProfileFlags::default(),
824 })
825 .unwrap();
826 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
827 assert_eq!(v, vec!["ssl"]);
828 }
829
830 #[test]
831 fn all_features_enables_every_declared_feature() {
832 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
833 f.validate().unwrap();
834 let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
835 package: "demo",
836 features: &f,
837 request: &SelectionRequest {
838 all_features: true,
839 ..Default::default()
840 },
841 profile: dev(),
842 toolchain: ToolchainSummary::default(),
843 build_flags: ResolvedProfileFlags::default(),
844 })
845 .unwrap();
846 let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
847 assert_eq!(v, vec!["simd", "ssl"]);
848 }
849
850 #[test]
851 fn unknown_feature_in_request_errors() {
852 let f = feats(&[], &[("simd", &[])]);
853 let mut req = SelectionRequest::default();
854 req.features.insert("missing".into());
855 match 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 })
863 .unwrap_err()
864 {
865 ValidationError::UnknownFeature { feature, .. } => assert_eq!(feature, "missing"),
866 other => panic!("unexpected: {other:?}"),
867 }
868 }
869
870 #[test]
871 fn fingerprint_is_stable_for_same_inputs() {
872 let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
873 f.validate().unwrap();
874 let cfg1 = BuildConfiguration::resolve(BuildConfigurationInput {
875 package: "demo",
876 features: &f,
877 request: &SelectionRequest::default(),
878 profile: dev(),
879 toolchain: ToolchainSummary::default(),
880 build_flags: ResolvedProfileFlags::default(),
881 })
882 .unwrap();
883 let cfg2 = BuildConfiguration::resolve(BuildConfigurationInput {
884 package: "demo",
885 features: &f,
886 request: &SelectionRequest::default(),
887 profile: dev(),
888 toolchain: ToolchainSummary::default(),
889 build_flags: ResolvedProfileFlags::default(),
890 })
891 .unwrap();
892 assert_eq!(cfg1.fingerprint, cfg2.fingerprint);
893 assert_eq!(cfg1.fingerprint.len(), 64);
894 }
895
896 #[test]
897 fn fingerprint_differs_when_features_change() {
898 let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
899 f.validate().unwrap();
900 let mut req = SelectionRequest::default();
901 let cfg_empty = BuildConfiguration::resolve(BuildConfigurationInput {
902 package: "demo",
903 features: &f,
904 request: &req,
905 profile: dev(),
906 toolchain: ToolchainSummary::default(),
907 build_flags: ResolvedProfileFlags::default(),
908 })
909 .unwrap();
910 req.features.insert("simd".into());
911 let cfg_simd = BuildConfiguration::resolve(BuildConfigurationInput {
912 package: "demo",
913 features: &f,
914 request: &req,
915 profile: dev(),
916 toolchain: ToolchainSummary::default(),
917 build_flags: ResolvedProfileFlags::default(),
918 })
919 .unwrap();
920 assert_ne!(cfg_empty.fingerprint, cfg_simd.fingerprint);
921 }
922 fn resolve_with_flags(flags: ResolvedProfileFlags) -> BuildConfiguration {
927 BuildConfiguration::resolve(BuildConfigurationInput {
928 package: "demo",
929 features: &Features::default(),
930 request: &SelectionRequest::default(),
931 profile: dev(),
932 toolchain: ToolchainSummary::default(),
933 build_flags: flags,
934 })
935 .unwrap()
936 }
937
938 #[test]
939 fn fingerprint_differs_when_defines_change() {
940 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
941 let added = resolve_with_flags(ResolvedProfileFlags {
942 defines: vec!["FOO=1".to_owned()],
943 ..ResolvedProfileFlags::default()
944 });
945 assert_ne!(baseline.fingerprint, added.fingerprint);
946 }
947
948 #[test]
949 fn fingerprint_differs_when_include_dirs_change() {
950 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
951 let added = resolve_with_flags(ResolvedProfileFlags {
952 include_dirs: vec![Utf8PathBuf::from("include")],
953 ..ResolvedProfileFlags::default()
954 });
955 assert_ne!(baseline.fingerprint, added.fingerprint);
956 }
957
958 #[test]
959 fn fingerprint_differs_when_extra_compile_args_change() {
960 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
961 let added = resolve_with_flags(ResolvedProfileFlags {
962 extra_compile_args: vec!["-Wall".to_owned()],
963 ..ResolvedProfileFlags::default()
964 });
965 assert_ne!(baseline.fingerprint, added.fingerprint);
966 }
967
968 #[test]
969 fn fingerprint_differs_when_cflags_change() {
970 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
978 let added = resolve_with_flags(ResolvedProfileFlags {
979 cflags: vec!["-std=c99".to_owned()],
980 ..ResolvedProfileFlags::default()
981 });
982 assert_ne!(baseline.fingerprint, added.fingerprint);
983 }
984
985 #[test]
986 fn fingerprint_differs_when_cxxflags_change() {
987 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
990 let added = resolve_with_flags(ResolvedProfileFlags {
991 cxxflags: vec!["-fno-rtti".to_owned()],
992 ..ResolvedProfileFlags::default()
993 });
994 assert_ne!(baseline.fingerprint, added.fingerprint);
995 }
996
997 #[test]
998 fn fingerprint_distinguishes_c_only_from_cxx_only_extra_args() {
999 let c_only = resolve_with_flags(ResolvedProfileFlags {
1007 cflags: vec!["-Wsome-warning".to_owned()],
1008 ..ResolvedProfileFlags::default()
1009 });
1010 let cxx_only = resolve_with_flags(ResolvedProfileFlags {
1011 cxxflags: vec!["-Wsome-warning".to_owned()],
1012 ..ResolvedProfileFlags::default()
1013 });
1014 assert_ne!(c_only.fingerprint, cxx_only.fingerprint);
1015 }
1016
1017 #[test]
1018 fn fingerprint_differs_when_ldflags_change() {
1019 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1020 let added = resolve_with_flags(ResolvedProfileFlags {
1021 ldflags: vec!["-Wl,--as-needed".to_owned()],
1022 ..ResolvedProfileFlags::default()
1023 });
1024 assert_ne!(baseline.fingerprint, added.fingerprint);
1025 }
1026
1027 #[test]
1028 fn fingerprint_differs_when_link_libs_change() {
1029 let baseline = resolve_with_flags(ResolvedProfileFlags::default());
1033 let added = resolve_with_flags(ResolvedProfileFlags {
1034 link_libs: vec!["pthread".to_owned()],
1035 ..ResolvedProfileFlags::default()
1036 });
1037 assert_ne!(baseline.fingerprint, added.fingerprint);
1038 }
1039
1040 #[test]
1041 fn fingerprint_is_stable_for_same_build_flags() {
1042 let flags = ResolvedProfileFlags {
1046 defines: vec!["FOO=1".to_owned(), "BAR=2".to_owned()],
1047 include_dirs: vec![
1048 Utf8PathBuf::from("include"),
1049 Utf8PathBuf::from("vendor/include"),
1050 ],
1051 extra_compile_args: vec!["-Wall".to_owned()],
1052 cflags: vec!["-std=c99".to_owned()],
1053 cxxflags: vec!["-fno-rtti".to_owned()],
1054 ldflags: vec!["-Wl,--as-needed".to_owned()],
1055 link_libs: vec!["pthread".to_owned()],
1056 };
1057 let a = resolve_with_flags(flags.clone());
1058 let b = resolve_with_flags(flags);
1059 assert_eq!(a.fingerprint, b.fingerprint);
1060 assert_eq!(a.fingerprint.len(), 64, "sha256 hex digest is 64 chars");
1061 }
1062
1063 fn release() -> ResolvedProfile {
1064 use crate::profile::{ProfileDefinition, ProfileName, ProfileSelection, resolve_profile};
1065 resolve_profile(
1066 &ProfileSelection::release_alias(),
1067 &BTreeMap::<ProfileName, ProfileDefinition>::new(),
1068 )
1069 .expect("built-in release resolves")
1070 }
1071
1072 #[test]
1073 fn fingerprint_differs_when_profile_changes() {
1074 let dev_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1075 package: "demo",
1076 features: &Features::default(),
1077 request: &SelectionRequest::default(),
1078 profile: dev(),
1079 toolchain: ToolchainSummary::default(),
1080 build_flags: ResolvedProfileFlags::default(),
1081 })
1082 .unwrap();
1083 let release_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
1084 package: "demo",
1085 features: &Features::default(),
1086 request: &SelectionRequest::default(),
1087 profile: release(),
1088 toolchain: ToolchainSummary::default(),
1089 build_flags: ResolvedProfileFlags::default(),
1090 })
1091 .unwrap();
1092 assert_ne!(dev_cfg.fingerprint, release_cfg.fingerprint);
1096 }
1097
1098 #[test]
1099 fn fingerprint_differs_when_toolchain_summary_changes() {
1100 let mut tc_a = ToolchainSummary::default();
1101 tc_a.tools.insert("cxx".to_owned(), "g++".to_owned());
1102 let mut tc_b = ToolchainSummary::default();
1103 tc_b.tools.insert("cxx".to_owned(), "clang++".to_owned());
1104 let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1105 package: "demo",
1106 features: &Features::default(),
1107 request: &SelectionRequest::default(),
1108 profile: dev(),
1109 toolchain: tc_a,
1110 build_flags: ResolvedProfileFlags::default(),
1111 })
1112 .unwrap();
1113 let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1114 package: "demo",
1115 features: &Features::default(),
1116 request: &SelectionRequest::default(),
1117 profile: dev(),
1118 toolchain: tc_b,
1119 build_flags: ResolvedProfileFlags::default(),
1120 })
1121 .unwrap();
1122 assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1123 }
1124
1125 #[test]
1126 fn fingerprint_differs_when_compiler_wrapper_changes() {
1127 let no_wrapper = ToolchainSummary::default();
1128 let with_wrapper = ToolchainSummary {
1129 compiler_wrapper: Some(CompilerWrapperSummary {
1130 kind: "ccache".into(),
1131 spec: "ccache".into(),
1132 source: "cli".into(),
1133 version: Some("4.8.0".into()),
1134 }),
1135 ..ToolchainSummary::default()
1136 };
1137 let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
1138 package: "demo",
1139 features: &Features::default(),
1140 request: &SelectionRequest::default(),
1141 profile: dev(),
1142 toolchain: no_wrapper,
1143 build_flags: ResolvedProfileFlags::default(),
1144 })
1145 .unwrap();
1146 let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
1147 package: "demo",
1148 features: &Features::default(),
1149 request: &SelectionRequest::default(),
1150 profile: dev(),
1151 toolchain: with_wrapper,
1152 build_flags: ResolvedProfileFlags::default(),
1153 })
1154 .unwrap();
1155 assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
1156 }
1157}