xtask/cmd/power_set/
utils.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Context;
4
5#[derive(Debug, Clone, serde_derive::Deserialize)]
6#[serde(default, deny_unknown_fields)]
7pub struct XTaskMetadata {
8    /// Allows you to provide a list of combinations that should be skipped.
9    /// Meaning, that these combinations will not be tested on supersets.
10    #[serde(alias = "skip-feature-sets")]
11    pub skip_feature_sets: BTreeSet<BTreeSet<String>>,
12    /// Allows you to skip optional dependencies.
13    #[serde(alias = "skip-optional-dependencies")]
14    pub skip_optional_dependencies: bool,
15    /// Allows you to provide a list of features that should be always included.
16    #[serde(alias = "always-include-features")]
17    pub always_include_features: BTreeSet<String>,
18    /// Allows you to provide the maximum number of features that should be tested.
19    #[serde(alias = "max-combination-size")]
20    pub max_combination_size: Option<usize>,
21    /// A set of features that are considered strictly additive meaning that they only
22    /// add new functionality and do not change the behavior of the crate.
23    #[serde(alias = "additive-features")]
24    pub additive_features: BTreeSet<String>,
25    /// A set of features that should be ignored when computing the power set.
26    #[serde(alias = "ignore-features")]
27    pub ignore_features: BTreeSet<String>,
28    /// Whether to skip the power set.
29    pub skip: bool,
30}
31
32impl Default for XTaskMetadata {
33    fn default() -> Self {
34        Self {
35            skip_feature_sets: Default::default(),
36            skip_optional_dependencies: true,
37            always_include_features: Default::default(),
38            max_combination_size: None,
39            additive_features: Default::default(),
40            ignore_features: Default::default(),
41            skip: false,
42        }
43    }
44}
45
46impl XTaskMetadata {
47    pub fn from_package(package: &cargo_metadata::Package) -> anyhow::Result<Self> {
48        let Some(metadata) = package.metadata.get("xtask").and_then(|v| v.get("powerset")) else {
49            return Ok(Self::default());
50        };
51
52        serde_json::from_value(metadata.clone()).context("xtask")
53    }
54}
55
56fn find_permutations<'a>(
57    initial_start: BTreeSet<&'a str>,
58    remaining: usize,
59    permutations: &mut BTreeSet<BTreeSet<&'a str>>,
60    viable_features: &BTreeMap<&'a str, BTreeSet<&'a str>>,
61    skip_feature_sets: &BTreeSet<BTreeSet<&'a str>>,
62) {
63    let mut stack = vec![(initial_start, remaining)];
64
65    while let Some((start, rem)) = stack.pop() {
66        if skip_feature_sets.iter().any(|s| s.is_subset(&start)) || !permutations.insert(start.clone()) || rem == 0 {
67            continue;
68        }
69
70        let flattened: BTreeSet<_> = start
71            .iter()
72            .flat_map(|f| viable_features[f].iter().chain(std::iter::once(f)))
73            .collect();
74
75        for (feature, deps) in viable_features.iter() {
76            if flattened.contains(feature) {
77                continue;
78            }
79
80            let mut new_start = start.clone();
81            new_start.insert(feature);
82            for dep in deps {
83                new_start.remove(dep);
84            }
85
86            if permutations.contains(&new_start) || skip_feature_sets.contains(&new_start) {
87                continue;
88            }
89
90            stack.push((new_start, rem.saturating_sub(1)));
91        }
92    }
93}
94
95fn flatten_features<'a>(
96    deps: impl IntoIterator<Item = &'a str>,
97    package_features: &BTreeMap<&'a str, Vec<&'a str>>,
98) -> BTreeSet<&'a str> {
99    let mut next: Vec<_> = deps.into_iter().collect();
100
101    let mut features = BTreeSet::new();
102    while let Some((dep, deps)) = next.pop().and_then(|d| Some((d, package_features.get(d)?))) {
103        if features.insert(dep) {
104            next.extend(deps);
105        }
106    }
107
108    features
109}
110
111pub fn test_package_features<'a>(
112    package: &'a cargo_metadata::Package,
113    added_features: impl IntoIterator<Item = &'a str>,
114    excluded_features: impl IntoIterator<Item = &'a str>,
115    xtask_metadata: &'a XTaskMetadata,
116) -> anyhow::Result<BTreeSet<BTreeSet<&'a str>>> {
117    if package.features.is_empty() {
118        return Ok(BTreeSet::new());
119    }
120
121    let mut always_included_features: BTreeSet<_> = xtask_metadata
122        .always_include_features
123        .iter()
124        .map(|f| f.as_str())
125        .chain(added_features)
126        .collect();
127
128    let skip_feature_sets: BTreeSet<_> = excluded_features
129        .into_iter()
130        .map(|f| BTreeSet::from_iter(std::iter::once(f)))
131        .chain(
132            xtask_metadata
133                .skip_feature_sets
134                .iter()
135                .map(|s| s.iter().map(|f| f.as_str()).collect()),
136        )
137        .collect();
138
139    let mut package_features: BTreeMap<&str, Vec<&str>> = package
140        .features
141        .iter()
142        .map(|(k, v)| (k.as_str(), v.iter().map(|f| f.as_str()).collect()))
143        .collect();
144
145    if xtask_metadata.skip_optional_dependencies {
146        let mut implicit_features = BTreeSet::new();
147        let mut used_deps = BTreeSet::new();
148
149        for (feature, deps) in package.features.iter() {
150            for dep in deps.iter().filter_map(|f| f.strip_prefix("dep:")) {
151                if dep == feature && deps.len() == 1 {
152                    implicit_features.insert(feature.as_str());
153                } else {
154                    used_deps.insert(dep);
155                }
156            }
157        }
158
159        for feature in implicit_features {
160            if used_deps.contains(&feature) {
161                continue;
162            }
163
164            package_features.remove(feature);
165        }
166    }
167
168    let mut viable_features = BTreeMap::new();
169    let mut additive_features = BTreeMap::new();
170
171    for (feature, deps) in package_features.iter() {
172        if xtask_metadata.ignore_features.contains(*feature) {
173            continue;
174        }
175
176        let flattened = flatten_features(deps.iter().copied(), &package_features);
177
178        if !xtask_metadata.additive_features.contains(*feature) {
179            viable_features.insert(*feature, flattened);
180        } else {
181            additive_features.insert(*feature, flattened);
182        }
183    }
184
185    // Remove features that are not in the package
186    always_included_features.retain(|f| package_features.contains_key(f));
187
188    // Non additive permutations are permutations that we need to find every
189    // combination of
190    let mut non_additive_permutations = BTreeSet::new();
191
192    // This finds all the combinations of features that are not additive
193    find_permutations(
194        always_included_features.clone(),
195        xtask_metadata.max_combination_size.unwrap_or(viable_features.len() + 1),
196        &mut non_additive_permutations,
197        &viable_features,
198        &skip_feature_sets,
199    );
200
201    // This finds all the combinations of features that are additive
202    // With additive features we do not need to find every combination, we just need
203    // to add the additive features to the non additive permutations
204
205    // This loop adds the additive features to the non additive permutations
206    // Example:
207    // - NON_ADDITIVE = [(A), (B), (A, B), ()]
208    // - ADDITIVE = [(C), (D), (E)]
209    // Result: [
210    //   (),
211    //   (A),
212    //   (B),
213    //   (A, B),
214    //   (A, C),
215    //   (A, C, D),
216    //   (A, C, D, E),
217    //   (B, C),
218    //   (B, C, D),
219    //   (B, C, D, E),
220    //   (A, B, C),
221    //   (A, B, C, D),
222    //   (A, B, C, D, E),
223    //   (C),
224    //   (D),
225    //   (E),
226    //   (C, D),
227    //   (C, D, E),
228    // ]
229    // To note: we do not test for combinations of the additive features. Such as
230    // (A, D, E).
231
232    let mut permutations = BTreeSet::new();
233    for mut permutation in non_additive_permutations {
234        let flattened: BTreeSet<_> = permutation
235            .clone()
236            .into_iter()
237            .flat_map(|f| viable_features[&f].iter().copied().chain(std::iter::once(f)))
238            .collect();
239
240        permutations.insert(permutation.clone());
241        for (feature, deps) in additive_features.iter() {
242            if flattened.contains(feature) {
243                continue;
244            }
245
246            permutation.insert(feature);
247            for dep in deps {
248                permutation.remove(dep);
249            }
250            permutations.insert(permutation.clone());
251        }
252    }
253
254    let flattened: BTreeSet<_> = always_included_features
255        .iter()
256        .flat_map(|f| viable_features[f].iter().chain(std::iter::once(f)))
257        .collect();
258
259    for feature in additive_features.keys() {
260        if flattened.contains(feature) {
261            continue;
262        }
263
264        let mut permutation = always_included_features.clone();
265        permutation.insert(feature);
266        permutations.insert(permutation);
267    }
268
269    Ok(permutations)
270}
271
272pub fn parse_features<'a>(
273    features: impl IntoIterator<Item = &'a str>,
274) -> (BTreeSet<&'a str>, BTreeMap<&'a str, BTreeSet<&'a str>>) {
275    let mut generic_features = BTreeSet::new();
276    let mut crate_features = BTreeMap::new();
277
278    for feature in features {
279        let mut splits = feature.splitn(2, '/');
280        let first = splits.next().unwrap();
281        if let Some(second) = splits.next() {
282            crate_features.entry(first).or_insert_with(BTreeSet::new).insert(second);
283        } else {
284            generic_features.insert(first);
285        }
286    }
287
288    (generic_features, crate_features)
289}