xtask/cmd/power_set/
mod.rs1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Context;
4
5mod utils;
6
7use utils::{XTaskMetadata, parse_features, test_package_features};
8
9use crate::cmd::IGNORED_PACKAGES;
10use crate::utils::{cargo_cmd, comma_delimited};
11
12#[derive(Debug, Clone, clap::Parser)]
13pub struct PowerSet {
14 #[clap(long, value_delimiter = ',')]
15 #[clap(alias = "feature")]
16 features: Vec<String>,
18 #[clap(long, value_delimiter = ',')]
19 #[clap(alias = "exclude-feature")]
20 exclude_features: Vec<String>,
22 #[clap(long, short, value_delimiter = ',')]
23 #[clap(alias = "package")]
24 packages: Vec<String>,
26 #[clap(long, short, value_delimiter = ',')]
27 #[clap(alias = "exclude-package")]
28 exclude_packages: Vec<String>,
30 #[clap(long, default_value = "0")]
31 skip: usize,
33 #[clap(long, default_value = "true")]
34 fail_fast: bool,
36 #[clap(long, default_value = "target/power-set")]
37 target_dir: String,
39 #[clap(long, action = clap::ArgAction::SetTrue)]
40 no_override_target_dir: bool,
42 #[clap(name = "command", default_value = "clippy")]
43 command: String,
45 #[clap(long, short, action = clap::ArgAction::SetTrue)]
46 #[clap(alias = "dry-run")]
47 dry_run: bool,
49 #[clap(last = true)]
50 args: Vec<String>,
52}
53
54impl PowerSet {
55 pub fn run(self) -> anyhow::Result<()> {
56 let start = std::time::Instant::now();
57
58 let metadata = crate::utils::metadata()?;
59
60 let mut tests = BTreeMap::new();
61
62 let features = self.features.into_iter().map(|f| f.to_lowercase()).collect::<BTreeSet<_>>();
63
64 let (added_global_features, added_package_features) = parse_features(features.iter().map(|f| f.as_str()));
65 let (excluded_global_features, excluded_package_features) =
66 parse_features(self.exclude_features.iter().map(|f| f.as_str()));
67
68 let ignored_packages = self
69 .exclude_packages
70 .into_iter()
71 .chain(IGNORED_PACKAGES.iter().map(|p| p.to_string()))
72 .map(|p| p.to_lowercase())
73 .collect::<BTreeSet<_>>();
74 let packages = self.packages.into_iter().map(|p| p.to_lowercase()).collect::<BTreeSet<_>>();
75
76 let xtask_metadata = metadata
77 .workspace_packages()
78 .iter()
79 .map(|p| {
80 XTaskMetadata::from_package(p).with_context(|| format!("failed to get metadata for package {}", p.name))
81 })
82 .collect::<anyhow::Result<Vec<_>>>()?;
83
84 for (package, xtask_metadata) in metadata.workspace_packages().iter().zip(xtask_metadata.iter()) {
86 if ignored_packages.contains(&package.name.to_lowercase())
87 || !(packages.is_empty() || packages.contains(&package.name.to_lowercase()))
88 || xtask_metadata.skip
89 {
90 continue;
91 }
92
93 let added_features = added_package_features
94 .get(package.name.as_str())
95 .into_iter()
96 .flatten()
97 .chain(added_global_features.iter())
98 .copied()
99 .filter(|s| package.features.contains_key(*s));
100 let excluded_features = excluded_package_features
101 .get(package.name.as_str())
102 .into_iter()
103 .flatten()
104 .chain(excluded_global_features.iter())
105 .copied()
106 .filter(|s| package.features.contains_key(*s));
107
108 let features = test_package_features(package, added_features, excluded_features, xtask_metadata)
109 .with_context(|| package.name.clone())?;
110
111 tests.insert(package.name.as_str(), features);
112 }
113
114 let total = tests.values().map(|s| s.len()).sum::<usize>();
115
116 if self.dry_run {
117 println!("dry run: {} packages with a total of {} feature sets", tests.len(), total);
118
119 for (package, sets) in tests.iter() {
120 println!("dry run: {} with {} feature sets: {:#?}", package, sets.len(), sets);
121 }
122
123 return Ok(());
124 }
125
126 let mut i = 0;
127 let mut failed = Vec::new();
128
129 for (package, power_set) in tests.iter() {
130 for features in power_set.iter() {
131 if i < self.skip {
132 i += 1;
133 continue;
134 }
135
136 let mut cmd = cargo_cmd();
137 cmd.arg(&self.command);
138 cmd.arg("--no-default-features");
139 if !features.is_empty() {
140 cmd.arg("--features").arg(comma_delimited(features.iter()));
141 }
142 cmd.arg("--package").arg(package);
143
144 if !self.no_override_target_dir {
145 cmd.arg("--target-dir").arg(&self.target_dir);
146 }
147
148 cmd.args(&self.args);
149
150 println!("executing {cmd:?} ({i}/{total})");
151
152 if !cmd.status()?.success() {
153 failed.push((*package, features));
154 if self.fail_fast {
155 anyhow::bail!(
156 "failed to execute command for package {} with features {:?} after {:?}",
157 package,
158 features,
159 start.elapsed()
160 );
161 }
162 }
163
164 i += 1;
165 }
166 }
167
168 if !failed.is_empty() {
169 eprintln!("failed to execute command for the following:");
170 for (package, features) in failed {
171 eprintln!(" {package} with features {features:?}");
172 }
173
174 anyhow::bail!("failed to execute command for some packages after {:?}", start.elapsed());
175 }
176
177 println!("all commands executed successfully after {:?}", start.elapsed());
178
179 Ok(())
180 }
181}