cargo_hackerman/
hack.rs

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    /// package id of the dependency we are adding
81    pub pid: Pid<'a>,
82
83    /// dependency type - dev or normal
84    pub ty: Ty,
85
86    /// Crate needs renaming
87    pub rename: bool,
88
89    /// Features to add
90    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        //tree.iter() {
103        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    /// all targets, normal and builds
119    AllTargets,
120    /// all targets, normal dependencies only
121    NormalOnly,
122    /// current target only
123    Target,
124    /// current target only, normal and build dependencies globally, dev dependencies for workspace
125    DevTarget,
126    NoDev,
127    MemberDev(Pid<'a>),
128}
129
130// we are doing 4 types of passes:
131// 1. everything for all the targets
132// 2. everything for this target - this is used to filter the first one
133// 3. starting from a workspace member, no dev
134// 4. starting from a workspace member, dev for that membe only
135
136fn 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        // last_edge.set(Some(e));
149        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]; // .unwrap();
169            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    //    dump(fg)?;
216    let mut changed = BTreeMap::new();
217    //    loop {
218    // First we collect all the named feats. The idea if some crate depends on
219    // the base feature (key) it should depend on all the named features of this
220    // crate (values).
221
222    // DetachedDepTree is used to avoid fighting the borrow checker.
223    // indices correspond to features in graph
224    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    // For reasons unknown cargo resolves dependencies for all the targets including those
233    // never be used. While we have to care about features added at this step - we can skip
234    // them for crates that never will be used - such as winapi on linux. second pass does
235    // that.
236    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        // all the "feature" nodes that belong to the workspace
254        let members_dfs_postorder = DfsPostOrder::new(&workspace_only_graph, fg.root)
255            .iter(&workspace_only_graph)
256            .collect::<Vec<_>>();
257
258        // only feature "roots" nodes, deduplicated
259        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        // For every workspace member we start collecting features it uses, similar to
288        // workspace_feats above
289
290        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        // at this point dep_feats contains all the normal features used by {member}.
331        // we'll use it to filter dep dependencies if any.
332        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            // DFS traverse of the current member and everything below it
346            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    // renames are needed when there's several dependencies from a member with the same name.
383    // there can be one, two or three of them.
384    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}