use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::build_flags::ResolvedProfileFlags;
use crate::compiler_wrapper::{CompilerWrapperSummary, ResolvedCompilerWrapper};
use crate::error::ValidationError;
use crate::profile::ResolvedProfile;
use crate::toolchain::ResolvedToolchain;
pub const DEFAULT_FEATURE_KEY: &str = "default";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Features {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub features: BTreeMap<String, Vec<String>>,
}
impl Features {
pub fn new(
default: Vec<String>,
features: BTreeMap<String, Vec<String>>,
) -> Result<Self, ValidationError> {
let me = Self { default, features };
me.validate()?;
Ok(me)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.features.contains_key(DEFAULT_FEATURE_KEY) {
return Err(ValidationError::ReservedFeatureName(
DEFAULT_FEATURE_KEY.to_owned(),
));
}
for name in self.features.keys() {
validate_identifier(name)?;
}
for name in &self.default {
validate_identifier(name)?;
if !self.features.contains_key(name) {
return Err(ValidationError::UnknownFeatureReference {
referrer: DEFAULT_FEATURE_KEY.to_owned(),
referenced: name.to_owned(),
});
}
}
for (name, implies) in &self.features {
for raw in implies {
let entry = FeatureEntry::parse(raw).map_err(|kind| {
ValidationError::InvalidFeatureEntry {
referrer: name.clone(),
entry: raw.clone(),
reason: kind,
}
})?;
match entry {
FeatureEntry::Local(local) => {
if !self.features.contains_key(&local) {
return Err(ValidationError::UnknownFeatureReference {
referrer: name.clone(),
referenced: local,
});
}
}
FeatureEntry::OptionalDep(_) | FeatureEntry::DepFeature { .. } => {
}
}
}
}
self.detect_cycles()?;
Ok(())
}
fn detect_cycles(&self) -> Result<(), ValidationError> {
#[derive(Clone, Copy)]
enum Color {
Visiting,
Done,
}
fn visit<'a>(
node: &'a str,
features: &'a BTreeMap<String, Vec<String>>,
state: &mut std::collections::HashMap<&'a str, Color>,
path: &mut Vec<&'a str>,
) -> Result<(), ValidationError> {
match state.get(node) {
Some(Color::Done) => return Ok(()),
Some(Color::Visiting) => {
let start = path.iter().position(|n| *n == node).unwrap_or(0);
let mut cycle: Vec<String> =
path[start..].iter().map(|s| (*s).to_owned()).collect();
cycle.push(node.to_owned());
return Err(ValidationError::FeatureCycle(cycle));
}
None => {}
}
state.insert(node, Color::Visiting);
path.push(node);
if let Some(implies) = features.get(node) {
for r in implies {
if let Ok(FeatureEntry::Local(local)) = FeatureEntry::parse(r)
&& let Some((stored, _)) = features.get_key_value(local.as_str())
{
visit(stored.as_str(), features, state, path)?;
}
}
}
path.pop();
state.insert(node, Color::Done);
Ok(())
}
let mut state = std::collections::HashMap::new();
let mut path: Vec<&str> = Vec::new();
for name in self.features.keys() {
visit(name.as_str(), &self.features, &mut state, &mut path)?;
}
Ok(())
}
pub fn expand(&self, roots: &BTreeSet<String>) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let mut stack: Vec<String> = roots.iter().cloned().collect();
while let Some(name) = stack.pop() {
if !out.insert(name.clone()) {
continue;
}
if let Some(implies) = self.features.get(&name) {
for raw in implies {
if let Ok(FeatureEntry::Local(local)) = FeatureEntry::parse(raw) {
stack.push(local);
}
}
}
}
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FeatureEntry {
Local(String),
OptionalDep(String),
DepFeature { dep: String, feature: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidFeatureEntryKind {
Empty,
EmptyDepName,
EmptyDepOrFeature,
MultiplePathSeparators,
UnsupportedCharacter(char),
}
impl InvalidFeatureEntryKind {
pub fn message(self) -> &'static str {
match self {
InvalidFeatureEntryKind::Empty => "feature entries must not be empty",
InvalidFeatureEntryKind::EmptyDepName => {
"`dep:` entries require a non-empty dependency name"
}
InvalidFeatureEntryKind::EmptyDepOrFeature => {
"`<dep>/<feature>` entries require both a dependency name and a feature name"
}
InvalidFeatureEntryKind::MultiplePathSeparators => {
"feature entries may contain at most one `/`"
}
InvalidFeatureEntryKind::UnsupportedCharacter(_) => {
"feature entries may only use ASCII letters, digits, `_`, `-`, `.`, plus the leading `dep:` or single `/` separator"
}
}
}
}
impl FeatureEntry {
pub fn parse(input: &str) -> Result<Self, InvalidFeatureEntryKind> {
if input.is_empty() {
return Err(InvalidFeatureEntryKind::Empty);
}
if let Some(rest) = input.strip_prefix("dep:") {
if rest.is_empty() {
return Err(InvalidFeatureEntryKind::EmptyDepName);
}
check_identifier_chars(rest)?;
return Ok(FeatureEntry::OptionalDep(rest.to_owned()));
}
if let Some((dep, feature)) = input.split_once('/') {
if feature.contains('/') {
return Err(InvalidFeatureEntryKind::MultiplePathSeparators);
}
if dep.is_empty() || feature.is_empty() {
return Err(InvalidFeatureEntryKind::EmptyDepOrFeature);
}
check_identifier_chars(dep)?;
check_identifier_chars(feature)?;
return Ok(FeatureEntry::DepFeature {
dep: dep.to_owned(),
feature: feature.to_owned(),
});
}
check_identifier_chars(input)?;
Ok(FeatureEntry::Local(input.to_owned()))
}
}
fn check_identifier_chars(s: &str) -> Result<(), InvalidFeatureEntryKind> {
for c in s.chars() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '.' => {}
other => return Err(InvalidFeatureEntryKind::UnsupportedCharacter(other)),
}
}
Ok(())
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SelectionRequest {
pub features: BTreeSet<String>,
pub all_features: bool,
pub no_default_features: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildConfiguration {
pub enabled_features: BTreeSet<String>,
pub profile: ResolvedProfile,
pub toolchain: ToolchainSummary,
pub build_flags: ResolvedProfileFlags,
pub fingerprint: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolchainSummary {
pub tools: BTreeMap<String, String>,
pub sources: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler_wrapper: Option<CompilerWrapperSummary>,
}
impl ToolchainSummary {
pub fn from_resolved(toolchain: &ResolvedToolchain) -> Self {
Self::from_resolved_parts(toolchain, None)
}
pub fn from_resolved_parts(
toolchain: &ResolvedToolchain,
wrapper: Option<&ResolvedCompilerWrapper>,
) -> Self {
let mut tools = BTreeMap::new();
let mut sources = BTreeMap::new();
for tool in toolchain.iter() {
let key = tool.kind.as_key().to_owned();
tools.insert(key.clone(), tool.spec.display());
sources.insert(
key,
crate::toolchain::tool_source_label(tool.source).to_owned(),
);
}
Self {
tools,
sources,
compiler_wrapper: wrapper.map(CompilerWrapperSummary::from_resolved),
}
}
}
#[derive(Debug)]
pub struct BuildConfigurationInput<'a> {
pub package: &'a str,
pub features: &'a Features,
pub request: &'a SelectionRequest,
pub profile: ResolvedProfile,
pub toolchain: ToolchainSummary,
pub build_flags: ResolvedProfileFlags,
}
impl BuildConfiguration {
pub fn resolve(input: BuildConfigurationInput<'_>) -> Result<Self, ValidationError> {
let BuildConfigurationInput {
package,
features,
request,
profile,
toolchain,
build_flags,
} = input;
let enabled_features = resolve_features(package, features, request)?;
let fingerprint =
compute_fingerprint(&enabled_features, &profile, &toolchain, &build_flags);
Ok(Self {
enabled_features,
profile,
toolchain,
build_flags,
fingerprint,
})
}
pub fn as_json(&self) -> serde_json::Value {
let compiler_wrapper =
self.toolchain
.compiler_wrapper
.as_ref()
.map_or(serde_json::Value::Null, |w| {
let mut obj = serde_json::Map::new();
obj.insert("kind".to_owned(), serde_json::Value::String(w.kind.clone()));
obj.insert("spec".to_owned(), serde_json::Value::String(w.spec.clone()));
obj.insert(
"source".to_owned(),
serde_json::Value::String(w.source.clone()),
);
if let Some(v) = &w.version {
obj.insert("version".to_owned(), serde_json::Value::String(v.clone()));
}
serde_json::Value::Object(obj)
});
serde_json::json!({
"features": self.enabled_features.iter().collect::<Vec<_>>(),
"profile": self.profile.as_json(),
"toolchain": {
"tools": &self.toolchain.tools,
"sources": &self.toolchain.sources,
"compiler_wrapper": compiler_wrapper,
},
"build_flags": self.build_flags.as_json(),
"fingerprint": self.fingerprint,
})
}
}
fn resolve_features(
package: &str,
features: &Features,
request: &SelectionRequest,
) -> Result<BTreeSet<String>, ValidationError> {
for name in &request.features {
if !features.features.contains_key(name) {
return Err(ValidationError::UnknownFeature {
package: package.to_owned(),
feature: name.clone(),
});
}
}
let mut roots: BTreeSet<String> = BTreeSet::new();
if request.all_features {
for name in features.features.keys() {
roots.insert(name.clone());
}
} else {
if !request.no_default_features {
for name in &features.default {
roots.insert(name.clone());
}
}
for name in &request.features {
roots.insert(name.clone());
}
}
Ok(features.expand(&roots))
}
fn bool_bytes(b: bool) -> &'static [u8] {
if b { b"true" } else { b"false" }
}
fn compute_fingerprint(
features: &BTreeSet<String>,
profile: &ResolvedProfile,
toolchain: &ToolchainSummary,
build_flags: &ResolvedProfileFlags,
) -> String {
let mut hasher = Sha256::new();
hasher.update(b"features\n");
for f in features {
hasher.update(f.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"profile\n");
hasher.update(b"name=");
hasher.update(profile.name.as_str().as_bytes());
hasher.update(b"\n");
hasher.update(b"debug=");
hasher.update(bool_bytes(profile.debug));
hasher.update(b"\n");
hasher.update(b"opt-level=");
hasher.update(profile.opt_level.as_str().as_bytes());
hasher.update(b"\n");
hasher.update(b"assertions=");
hasher.update(bool_bytes(profile.assertions));
hasher.update(b"\n");
hasher.update(b"toolchain\n");
for (kind, spec) in &toolchain.tools {
hasher.update(kind.as_bytes());
hasher.update(b"=");
hasher.update(spec.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"compiler-wrapper\n");
match &toolchain.compiler_wrapper {
Some(wrapper) => {
hasher.update(b"kind=");
hasher.update(wrapper.kind.as_bytes());
hasher.update(b"\n");
hasher.update(b"spec=");
hasher.update(wrapper.spec.as_bytes());
hasher.update(b"\n");
if let Some(version) = wrapper.version.as_deref() {
hasher.update(b"version=");
hasher.update(version.as_bytes());
hasher.update(b"\n");
}
}
None => {
hasher.update(b"kind=none\n");
}
}
hasher.update(b"build-flags\n");
hasher.update(b"defines\n");
for d in &build_flags.defines {
hasher.update(d.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"include-dirs\n");
for inc in &build_flags.include_dirs {
hasher.update(inc.to_string_lossy().as_bytes());
hasher.update(b"\n");
}
hasher.update(b"language-neutral-compile-args\n");
for a in &build_flags.extra_compile_args {
hasher.update(a.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"cflags\n");
for a in &build_flags.cflags {
hasher.update(a.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"cxxflags\n");
for a in &build_flags.cxxflags {
hasher.update(a.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"ldflags\n");
for a in &build_flags.ldflags {
hasher.update(a.as_bytes());
hasher.update(b"\n");
}
crate::hash::hex_digest(&hasher.finalize())
}
fn validate_identifier(name: &str) -> Result<(), ValidationError> {
if name.is_empty() {
return Err(ValidationError::EmptyConfigName("feature"));
}
let bad = name.chars().any(|c| {
!(c.is_ascii_alphanumeric() || c == '_' || c == '-')
|| c.is_whitespace()
|| matches!(c, '/' | '.' | ':')
});
if bad {
return Err(ValidationError::InvalidConfigName {
kind: "feature",
value: name.to_owned(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::{
ProfileDefinition, ProfileName, ProfileSelection, ResolvedProfile, resolve_profile,
};
use std::path::PathBuf;
fn dev() -> ResolvedProfile {
resolve_profile(
&ProfileSelection::default_dev(),
&BTreeMap::<ProfileName, ProfileDefinition>::new(),
)
.expect("built-in dev resolves")
}
fn feats(default: &[&str], pairs: &[(&str, &[&str])]) -> Features {
let mut features = BTreeMap::new();
for (k, vs) in pairs {
features.insert(
(*k).to_owned(),
vs.iter().map(|s| (*s).to_owned()).collect(),
);
}
Features {
default: default.iter().map(|s| (*s).to_owned()).collect(),
features,
}
}
#[test]
fn features_validate_ok_for_simple_decls() {
feats(&["simd"], &[("simd", &[]), ("ssl", &[])])
.validate()
.unwrap();
}
#[test]
fn features_reject_reserved_default_key() {
let mut f = feats(&[], &[]);
f.features.insert("default".into(), vec![]);
match f.validate().unwrap_err() {
ValidationError::ReservedFeatureName(n) => assert_eq!(n, "default"),
other => panic!("expected ReservedFeatureName, got {other:?}"),
}
}
#[test]
fn features_reject_unknown_default_reference() {
match feats(&["nope"], &[("simd", &[])]).validate().unwrap_err() {
ValidationError::UnknownFeatureReference { referenced, .. } => {
assert_eq!(referenced, "nope");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn features_reject_internal_unknown_reference() {
match feats(&[], &[("full", &["ssl"])]).validate().unwrap_err() {
ValidationError::UnknownFeatureReference {
referrer,
referenced,
} => {
assert_eq!(referrer, "full");
assert_eq!(referenced, "ssl");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn features_reject_cycles() {
let f = feats(&[], &[("a", &["b"]), ("b", &["a"])]);
match f.validate().unwrap_err() {
ValidationError::FeatureCycle(cycle) => {
assert!(cycle.iter().any(|n| n == "a"));
assert!(cycle.iter().any(|n| n == "b"));
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn features_reject_invalid_name() {
let f = feats(&[], &[("foo/bar", &[])]);
match f.validate().unwrap_err() {
ValidationError::InvalidConfigName { kind, value } => {
assert_eq!(kind, "feature");
assert_eq!(value, "foo/bar");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn features_expand_default_set() {
let f = feats(
&["full"],
&[("simd", &[]), ("ssl", &[]), ("full", &["simd", "ssl"])],
);
f.validate().unwrap();
let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &SelectionRequest::default(),
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
assert_eq!(v, vec!["full", "simd", "ssl"]);
}
#[test]
fn no_default_features_drops_defaults() {
let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
f.validate().unwrap();
let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &SelectionRequest {
no_default_features: true,
..Default::default()
},
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
assert!(cfg.enabled_features.is_empty());
}
#[test]
fn explicit_features_are_added() {
let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
f.validate().unwrap();
let mut req = SelectionRequest::default();
req.features.insert("ssl".into());
let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &req,
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
assert_eq!(v, vec!["ssl"]);
}
#[test]
fn all_features_enables_every_declared_feature() {
let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
f.validate().unwrap();
let cfg = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &SelectionRequest {
all_features: true,
..Default::default()
},
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let v: Vec<&str> = cfg.enabled_features.iter().map(String::as_str).collect();
assert_eq!(v, vec!["simd", "ssl"]);
}
#[test]
fn unknown_feature_in_request_errors() {
let f = feats(&[], &[("simd", &[])]);
let mut req = SelectionRequest::default();
req.features.insert("missing".into());
match BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &req,
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap_err()
{
ValidationError::UnknownFeature { feature, .. } => assert_eq!(feature, "missing"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn fingerprint_is_stable_for_same_inputs() {
let f = feats(&["simd"], &[("simd", &[]), ("ssl", &[])]);
f.validate().unwrap();
let cfg1 = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &SelectionRequest::default(),
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let cfg2 = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &SelectionRequest::default(),
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
assert_eq!(cfg1.fingerprint, cfg2.fingerprint);
assert_eq!(cfg1.fingerprint.len(), 64);
}
#[test]
fn fingerprint_differs_when_features_change() {
let f = feats(&[], &[("simd", &[]), ("ssl", &[])]);
f.validate().unwrap();
let mut req = SelectionRequest::default();
let cfg_empty = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &req,
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
req.features.insert("simd".into());
let cfg_simd = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &f,
request: &req,
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
assert_ne!(cfg_empty.fingerprint, cfg_simd.fingerprint);
}
fn resolve_with_flags(flags: ResolvedProfileFlags) -> BuildConfiguration {
BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: flags,
})
.unwrap()
}
#[test]
fn fingerprint_differs_when_defines_change() {
let baseline = resolve_with_flags(ResolvedProfileFlags::default());
let added = resolve_with_flags(ResolvedProfileFlags {
defines: vec!["FOO=1".to_owned()],
..ResolvedProfileFlags::default()
});
assert_ne!(baseline.fingerprint, added.fingerprint);
}
#[test]
fn fingerprint_differs_when_include_dirs_change() {
let baseline = resolve_with_flags(ResolvedProfileFlags::default());
let added = resolve_with_flags(ResolvedProfileFlags {
include_dirs: vec![PathBuf::from("include")],
..ResolvedProfileFlags::default()
});
assert_ne!(baseline.fingerprint, added.fingerprint);
}
#[test]
fn fingerprint_differs_when_extra_compile_args_change() {
let baseline = resolve_with_flags(ResolvedProfileFlags::default());
let added = resolve_with_flags(ResolvedProfileFlags {
extra_compile_args: vec!["-Wall".to_owned()],
..ResolvedProfileFlags::default()
});
assert_ne!(baseline.fingerprint, added.fingerprint);
}
#[test]
fn fingerprint_differs_when_cflags_change() {
let baseline = resolve_with_flags(ResolvedProfileFlags::default());
let added = resolve_with_flags(ResolvedProfileFlags {
cflags: vec!["-std=c99".to_owned()],
..ResolvedProfileFlags::default()
});
assert_ne!(baseline.fingerprint, added.fingerprint);
}
#[test]
fn fingerprint_differs_when_cxxflags_change() {
let baseline = resolve_with_flags(ResolvedProfileFlags::default());
let added = resolve_with_flags(ResolvedProfileFlags {
cxxflags: vec!["-fno-rtti".to_owned()],
..ResolvedProfileFlags::default()
});
assert_ne!(baseline.fingerprint, added.fingerprint);
}
#[test]
fn fingerprint_distinguishes_c_only_from_cxx_only_extra_args() {
let c_only = resolve_with_flags(ResolvedProfileFlags {
cflags: vec!["-Wsome-warning".to_owned()],
..ResolvedProfileFlags::default()
});
let cxx_only = resolve_with_flags(ResolvedProfileFlags {
cxxflags: vec!["-Wsome-warning".to_owned()],
..ResolvedProfileFlags::default()
});
assert_ne!(c_only.fingerprint, cxx_only.fingerprint);
}
#[test]
fn fingerprint_differs_when_ldflags_change() {
let baseline = resolve_with_flags(ResolvedProfileFlags::default());
let added = resolve_with_flags(ResolvedProfileFlags {
ldflags: vec!["-Wl,--as-needed".to_owned()],
..ResolvedProfileFlags::default()
});
assert_ne!(baseline.fingerprint, added.fingerprint);
}
#[test]
fn fingerprint_is_stable_for_same_build_flags() {
let flags = ResolvedProfileFlags {
defines: vec!["FOO=1".to_owned(), "BAR=2".to_owned()],
include_dirs: vec![PathBuf::from("include"), PathBuf::from("vendor/include")],
extra_compile_args: vec!["-Wall".to_owned()],
cflags: vec!["-std=c99".to_owned()],
cxxflags: vec!["-fno-rtti".to_owned()],
ldflags: vec!["-Wl,--as-needed".to_owned()],
};
let a = resolve_with_flags(flags.clone());
let b = resolve_with_flags(flags);
assert_eq!(a.fingerprint, b.fingerprint);
assert_eq!(a.fingerprint.len(), 64, "sha256 hex digest is 64 chars");
}
fn release() -> ResolvedProfile {
use crate::profile::{ProfileDefinition, ProfileName, ProfileSelection, resolve_profile};
resolve_profile(
&ProfileSelection::release_alias(),
&BTreeMap::<ProfileName, ProfileDefinition>::new(),
)
.expect("built-in release resolves")
}
#[test]
fn fingerprint_differs_when_profile_changes() {
let dev_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: dev(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let release_cfg = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: release(),
toolchain: ToolchainSummary::default(),
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
assert_ne!(dev_cfg.fingerprint, release_cfg.fingerprint);
}
#[test]
fn fingerprint_differs_when_toolchain_summary_changes() {
let mut tc_a = ToolchainSummary::default();
tc_a.tools.insert("cxx".to_owned(), "g++".to_owned());
let mut tc_b = ToolchainSummary::default();
tc_b.tools.insert("cxx".to_owned(), "clang++".to_owned());
let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: dev(),
toolchain: tc_a,
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: dev(),
toolchain: tc_b,
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
}
#[test]
fn fingerprint_differs_when_compiler_wrapper_changes() {
let no_wrapper = ToolchainSummary::default();
let with_wrapper = ToolchainSummary {
compiler_wrapper: Some(CompilerWrapperSummary {
kind: "ccache".into(),
spec: "ccache".into(),
source: "cli".into(),
version: Some("4.8.0".into()),
}),
..ToolchainSummary::default()
};
let cfg_a = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: dev(),
toolchain: no_wrapper,
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
let cfg_b = BuildConfiguration::resolve(BuildConfigurationInput {
package: "demo",
features: &Features::default(),
request: &SelectionRequest::default(),
profile: dev(),
toolchain: with_wrapper,
build_flags: ResolvedProfileFlags::default(),
})
.unwrap();
assert_ne!(cfg_a.fingerprint, cfg_b.fingerprint);
}
}