1#![allow(clippy::similar_names)]
2
3use crate::{
4 feat_graph::{Feat, FeatGraph, Pid},
5 metadata::DepKindInfo,
6 source::ChangePackage,
7 toml::set_dependencies,
8};
9use cargo_metadata::Metadata;
10use cargo_platform::Cfg;
11use petgraph::{
12 graph::NodeIndex,
13 visit::{Dfs, DfsPostOrder, EdgeFiltered, EdgeRef, NodeFiltered, VisitMap, Walker},
14};
15use std::collections::{BTreeMap, BTreeSet};
16use tracing::{debug, info, trace, warn};
17
18fn force_config(var: &mut bool, name: &str, meta: &serde_json::Value) -> Option<()> {
19 *var = meta.get("hackerman")?.get(name)?.as_bool()?;
20 Some(())
21}
22
23pub fn hack(
24 dry: bool,
25 mut lock: bool,
26 mut no_dev: bool,
27 meta: &Metadata,
28 triplets: Vec<&str>,
29 cfgs: Vec<Cfg>,
30) -> anyhow::Result<bool> {
31 force_config(&mut lock, "lock", &meta.workspace_metadata);
32 force_config(&mut no_dev, "no-dev", &meta.workspace_metadata);
33
34 let mut fg = FeatGraph::init(meta, triplets, cfgs)?;
35 let changeset = get_changeset(&mut fg, no_dev)?;
36 let has_changes = !changeset.is_empty();
37
38 if dry {
39 if changeset.is_empty() {
40 println!("Features are unified as is");
41 return Ok(false);
42 }
43 println!("Hackerman would like to set those features for following packets:");
44 }
45
46 for (member, changes) in changeset {
47 let mut changeset = changes
48 .into_iter()
49 .map(|change| ChangePackage::make(member, change))
50 .collect::<anyhow::Result<Vec<_>>>()?;
51
52 if dry {
53 changeset.sort_by(|a, b| a.name.cmp(&b.name));
54 let path = &member.package().manifest_path;
55 println!("{path}");
56 for change in changeset {
57 let t = match change.ty {
58 Ty::Dev => "dev ",
59 Ty::Norm => "",
60 };
61 println!(
62 "\t{} {} {}: {t}{:?}",
63 change.name, change.version, change.source, change.feats
64 );
65 }
66 } else {
67 let path = &member.package().manifest_path;
68 set_dependencies(path, lock, &changeset)?;
69 }
70 }
71
72 if dry && has_changes {
73 anyhow::bail!("Features are not unified");
74 }
75
76 Ok(has_changes)
77}
78
79pub struct FeatChange<'a> {
80 pub pid: Pid<'a>,
82
83 pub ty: Ty,
85
86 pub rename: bool,
88
89 pub features: BTreeSet<String>,
91}
92
93type FeatChanges<'a> = BTreeMap<Pid<'a>, Vec<FeatChange<'a>>>;
94type DetachedDepTree = BTreeMap<NodeIndex, BTreeSet<NodeIndex>>;
95
96fn show_detached_dep_tree(tree: &DetachedDepTree, fg: &FeatGraph) -> &'static str {
97 let mut t = tree.iter().collect::<Vec<_>>();
98
99 t.sort_by(|a, b| fg.features[*a.0].fid().cmp(&fg.features[*b.0].fid()));
100
101 for (&package, feats) in t {
102 let package = fg.features[package];
104 print!("{package}\n\t");
105 for feature in feats.iter().copied() {
106 let feature = fg.features[feature];
107 let fid = feature.fid().unwrap();
108 assert_eq!(package.fid().unwrap().pid, fid.pid);
109 print!("{} ", fid.dep);
110 }
111 println!();
112 }
113 ""
114}
115
116#[derive(Debug, Clone, Copy)]
117pub enum Collect<'a> {
118 AllTargets,
120 NormalOnly,
122 Target,
124 DevTarget,
126 NoDev,
127 MemberDev(Pid<'a>),
128}
129
130fn collect_features_from<M>(
137 dfs: &mut Dfs<NodeIndex, M>,
138 fg: &FeatGraph,
139 to: &mut DetachedDepTree,
140 filter: Collect,
141) where
142 M: VisitMap<NodeIndex>,
143{
144 let mut to_visit = Vec::new();
145 let mut added = BTreeSet::new();
146
147 let g = EdgeFiltered::from_fn(&fg.features, |e| {
148 match filter {
150 Collect::AllTargets => true,
151 Collect::Target | Collect::NoDev | Collect::DevTarget | Collect::MemberDev(_) => e
152 .weight()
153 .satisfies(fg.features[e.source()], filter, &fg.platforms, &fg.cfgs),
154 Collect::NormalOnly => e.weight().is_normal(),
155 }
156 });
157
158 loop {
159 while let Some(ix) = dfs.next(&g) {
160 if let Some(fid) = fg.features[ix].fid() {
161 if let Some(parent) = fg.fid_cache.get(&fid.get_base()) {
162 to.entry(*parent).or_default().insert(ix);
163 }
164 }
165 }
166 for t in fg.triggers.iter() {
167 let package = fg.fid_cache[&t.package.base().get_base()];
168 let feature = fg.fid_cache[&t.feature]; let weak_dep = fg.fid_cache[&t.weak_dep];
170 let weak_feat = fg.fid_cache[&t.weak_feat];
171
172 if let Some(dep) = to.get(&package) {
173 if dep.contains(&feature) && dep.contains(&weak_dep) && added.insert(weak_feat) {
174 to_visit.push(weak_feat);
175 }
176 }
177 }
178
179 if let Some(next) = to_visit.pop() {
180 dfs.move_to(next);
181 } else {
182 break;
183 }
184 }
185}
186
187#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
188pub enum Ty {
189 Dev,
190 Norm,
191}
192
193impl Ty {
194 #[must_use]
195 pub const fn table_name(&self) -> &'static str {
196 match self {
197 Ty::Dev => "dev-dependencies",
198 Ty::Norm => "dependencies",
199 }
200 }
201}
202
203impl std::fmt::Display for Ty {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 match self {
206 Ty::Dev => f.write_str("dev"),
207 Ty::Norm => f.write_str("norm"),
208 }
209 }
210}
211
212pub fn get_changeset<'a>(fg: &mut FeatGraph<'a>, no_dev: bool) -> anyhow::Result<FeatChanges<'a>> {
213 info!("==== Calculating changeset for hack");
214
215 let mut changed = BTreeMap::new();
217 let mut raw_workspace_feats: DetachedDepTree = BTreeMap::new();
225 collect_features_from(
226 &mut Dfs::new(&fg.features, fg.root),
227 fg,
228 &mut raw_workspace_feats,
229 Collect::NormalOnly,
230 );
231
232 let mut filtered_workspace_feats = BTreeMap::new();
237 collect_features_from(
238 &mut Dfs::new(&fg.features, fg.root),
239 fg,
240 &mut filtered_workspace_feats,
241 Collect::Target,
242 );
243 raw_workspace_feats.retain(|k, _| filtered_workspace_feats.contains_key(k));
244
245 info!(
246 "Accumulated workspace dependencies{}",
247 show_detached_dep_tree(&raw_workspace_feats, fg)
248 );
249 let members = {
250 let workspace_only_graph =
251 NodeFiltered::from_fn(&fg.features, |node| fg.features[node].is_workspace());
252
253 let members_dfs_postorder = DfsPostOrder::new(&workspace_only_graph, fg.root)
255 .iter(&workspace_only_graph)
256 .collect::<Vec<_>>();
257
258 let mut res = Vec::new();
260 let mut seen = BTreeSet::new();
261 for member in members_dfs_postorder {
262 if let Some(pid) = fg.features[member].pid() {
263 if seen.contains(&pid) {
264 continue;
265 }
266 seen.insert(pid);
267
268 let package = pid.package();
269 let fid = if package.features.contains_key("default") {
270 pid.named("default")
271 } else {
272 pid.base()
273 };
274 if let Some(&ix) = fg.fid_cache.get(&fid) {
275 res.push((pid, ix));
276 } else {
277 warn!("unknown base in workspace: {fid:?}?");
278 }
279 }
280 }
281 res
282 };
283
284 for (member, member_ix) in members.iter().copied() {
285 info!("==== Checking {member:?}");
286
287 let mut dfs = Dfs::new(&fg.features, member_ix);
291 let mut deps_feats = BTreeMap::new();
292 'dependency: loop {
293 collect_features_from(&mut dfs, fg, &mut deps_feats, Collect::NoDev);
294
295 debug!(
296 "Accumulated deps for {:?} are as following:{}",
297 member.package().name,
298 show_detached_dep_tree(&deps_feats, fg),
299 );
300
301 for (&dep, feats) in &deps_feats {
302 if let Some(ws_feats) = raw_workspace_feats.get(&dep) {
303 if ws_feats != feats {
304 if let Some(&missing_feat) = ws_feats.difference(feats).next() {
305 info!("\t{member:?} lacks {}", fg.features[missing_feat]);
306
307 changed
308 .entry(member)
309 .or_insert_with(BTreeMap::default)
310 .insert((Ty::Norm, dep), ws_feats.clone());
311
312 let new_dep =
313 fg.add_edge(member_ix, missing_feat, false, DepKindInfo::NORMAL)?;
314 dfs.move_to(new_dep);
315
316 trace!("Performing one more iteration on {member:?}");
317 continue 'dependency;
318 }
319 }
320 }
321 }
322
323 break;
324 }
325
326 if no_dev {
327 continue;
328 }
329
330 if !member
333 .package()
334 .dependencies
335 .iter()
336 .any(|d| d.kind == cargo_metadata::DependencyKind::Development)
337 {
338 debug!("No dev dependencies for {member:?}, skipping");
339 continue;
340 }
341
342 let mut dfs = Dfs::new(&fg.features, member_ix);
343 let mut dev_feats = BTreeMap::new();
344 'dev_dependency: loop {
345 collect_features_from(&mut dfs, fg, &mut dev_feats, Collect::MemberDev(member));
347
348 dev_feats.retain(|key, _val| filtered_workspace_feats.contains_key(key));
349
350 debug!(
351 "Accumulated dev deps for {:?} are as following:{}",
352 member.package().name,
353 show_detached_dep_tree(&dev_feats, fg),
354 );
355
356 for (&dep, feats) in &dev_feats {
357 if let Some(ws_feats) = raw_workspace_feats.get(&dep) {
358 if ws_feats != feats {
359 if let Some(&missing_feat) = ws_feats.difference(feats).next() {
360 debug!("\t{member:?} lacks dev {}", fg.features[missing_feat]);
361
362 changed
363 .entry(member)
364 .or_insert_with(BTreeMap::default)
365 .insert((Ty::Dev, dep), ws_feats.clone());
366
367 let new_dep =
368 fg.add_edge(member_ix, missing_feat, false, DepKindInfo::DEV)?;
369 dfs.move_to(new_dep);
370
371 trace!("Performing one more dev iteration on {member:?}");
372 continue 'dev_dependency;
373 }
374 }
375 }
376 }
377
378 break;
379 }
380 }
381
382 let mut renames = BTreeMap::new();
385 for package in &fg.workspace_members {
386 use std::cell::RefCell;
387 let mut deps = BTreeMap::new();
388 let cell = RefCell::new(&mut deps);
389 let package_index = match fg.fid_cache.get(&package.root()) {
390 Some(ix) => ix,
391 None => continue,
392 };
393 let g = EdgeFiltered::from_fn(&fg.features, |edge| {
394 if fg.features[edge.target()].pid() == Some(*package) {
395 true
396 } else {
397 if let Some(dep) = fg.features[edge.target()].pid() {
398 let dep = dep.package();
399 cell.borrow_mut()
400 .entry(dep.name.clone())
401 .or_insert_with(BTreeSet::new)
402 .insert(&dep.id);
403 }
404 false
405 }
406 });
407
408 let mut dfs = Dfs::new(&g, *package_index);
409 while dfs.next(&g).is_some() {}
410 deps.retain(|_key, val| val.len() > 1);
411 for (dep, _versions) in deps {
412 renames
413 .entry(*package)
414 .or_insert_with(BTreeSet::new)
415 .insert(dep);
416 }
417 }
418
419 Ok(changed
420 .into_iter()
421 .map(|(pid, deps)| {
422 let feats = deps
423 .into_iter()
424 .filter_map(|((ty, dep_pid), feats)| {
425 let package = fg.features[dep_pid].fid()?.pid;
426 let feats = feats
427 .iter()
428 .filter_map(|f| match fg.features[*f].fid()?.dep {
429 Feat::Base => None,
430 Feat::Named(name) => Some(name.to_string()),
431 })
432 .collect::<BTreeSet<_>>();
433 let rename = renames
434 .get(&pid)
435 .map_or(false, |names| names.contains(&package.package().name));
436 Some(FeatChange {
437 pid: package,
438 ty,
439 rename,
440 features: feats,
441 })
442 })
443 .collect::<Vec<_>>();
444 (pid, feats)
445 })
446 .collect::<BTreeMap<_, _>>())
447}