Skip to content

Commit 7d720ef

Browse files
committed
Auto merge of #8062 - ehuss:tree, r=alexcrichton
Add `cargo tree` command. This migrates [cargo-tree](https://github.com/sfackler/cargo-tree/) into Cargo as a built-in command. This is based on a recent master (https://github.com/sfackler/cargo-tree/tree/4108d216ec072d2e7c9befb4de32175f97a9dbc4), and should be mostly similar in functionality. There are a variety changes: * `--all-targets` renamed to `--no-filter-targets` to avoid confusion with the `--all-targets` flag used in other Cargo commands with a different meaning. * `--all`/`-a` renamed to `--no-dedupe` to avoid confusion with the `-all` flag which means "all workspace crates" in other Cargo commands. * `--duplicate` renamed to `--duplicates` (with alias), just a personal preference. * Added support for multiple roots (workspace support). * Added the `--graph-features` flag for including features in the graph (to "explain" why a feature is enabled). * Added `{f}` to format string to show features. * Handles new feature resolver. * Handles cyclical dev dependencies. * Added a test suite. * Dropped the dependency on petgraph, in favor of a simpler custom graph. Closes #7286.
2 parents 239f2bf + 7c40344 commit 7d720ef

31 files changed

+4752
-53
lines changed

src/bin/cargo/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub fn builtin() -> Vec<App> {
2727
rustdoc::cli(),
2828
search::cli(),
2929
test::cli(),
30+
tree::cli(),
3031
uninstall::cli(),
3132
update::cli(),
3233
vendor::cli(),
@@ -63,6 +64,7 @@ pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches<'_>) -> Cli
6364
"rustdoc" => rustdoc::exec,
6465
"search" => search::exec,
6566
"test" => test::exec,
67+
"tree" => tree::exec,
6668
"uninstall" => uninstall::exec,
6769
"update" => update::exec,
6870
"vendor" => vendor::exec,
@@ -99,6 +101,7 @@ pub mod rustc;
99101
pub mod rustdoc;
100102
pub mod search;
101103
pub mod test;
104+
pub mod tree;
102105
pub mod uninstall;
103106
pub mod update;
104107
pub mod vendor;

src/bin/cargo/commands/tree.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use crate::command_prelude::*;
2+
use anyhow::{bail, format_err};
3+
use cargo::core::dependency::DepKind;
4+
use cargo::ops::tree::{self, EdgeKind};
5+
use cargo::ops::Packages;
6+
use cargo::util::CargoResult;
7+
use std::collections::HashSet;
8+
use std::str::FromStr;
9+
10+
pub fn cli() -> App {
11+
subcommand("tree")
12+
.about("Display a tree visualization of a dependency graph")
13+
.arg(opt("quiet", "Suppress status messages").short("q"))
14+
.arg_manifest_path()
15+
.arg_package_spec_no_all(
16+
"Package to be used as the root of the tree",
17+
"Display the tree for all packages in the workspace",
18+
"Exclude specific workspace members",
19+
)
20+
.arg(Arg::with_name("all").long("all").short("a").hidden(true))
21+
.arg(
22+
Arg::with_name("all-targets")
23+
.long("all-targets")
24+
.hidden(true),
25+
)
26+
.arg_features()
27+
.arg_target_triple(
28+
"Filter dependencies matching the given target-triple (default host platform)",
29+
)
30+
.arg(
31+
Arg::with_name("no-dev-dependencies")
32+
.long("no-dev-dependencies")
33+
.hidden(true),
34+
)
35+
.arg(
36+
multi_opt(
37+
"edges",
38+
"KINDS",
39+
"The kinds of dependencies to display \
40+
(features, normal, build, dev, all, no-dev, no-build, no-normal)",
41+
)
42+
.short("e"),
43+
)
44+
.arg(
45+
optional_multi_opt(
46+
"invert",
47+
"SPEC",
48+
"Invert the tree direction and focus on the given package",
49+
)
50+
.short("i"),
51+
)
52+
.arg(Arg::with_name("no-indent").long("no-indent").hidden(true))
53+
.arg(
54+
Arg::with_name("prefix-depth")
55+
.long("prefix-depth")
56+
.hidden(true),
57+
)
58+
.arg(
59+
opt(
60+
"prefix",
61+
"Change the prefix (indentation) of how each entry is displayed",
62+
)
63+
.value_name("PREFIX")
64+
.possible_values(&["depth", "indent", "none"])
65+
.default_value("indent"),
66+
)
67+
.arg(opt(
68+
"no-dedupe",
69+
"Do not de-duplicate (repeats all shared dependencies)",
70+
))
71+
.arg(
72+
opt(
73+
"duplicates",
74+
"Show only dependencies which come in multiple versions (implies -i)",
75+
)
76+
.short("d")
77+
.alias("duplicate"),
78+
)
79+
.arg(
80+
opt("charset", "Character set to use in output: utf8, ascii")
81+
.value_name("CHARSET")
82+
.possible_values(&["utf8", "ascii"])
83+
.default_value("utf8"),
84+
)
85+
.arg(
86+
opt("format", "Format string used for printing dependencies")
87+
.value_name("FORMAT")
88+
.short("f")
89+
.default_value("{p}"),
90+
)
91+
}
92+
93+
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
94+
let prefix = if args.is_present("no-indent") {
95+
config
96+
.shell()
97+
.warn("the --no-indent flag has been changed to --prefix=none")?;
98+
"none"
99+
} else if args.is_present("prefix-depth") {
100+
config
101+
.shell()
102+
.warn("the --prefix-depth flag has been changed to --prefix=depth")?;
103+
"depth"
104+
} else {
105+
args.value_of("prefix").unwrap()
106+
};
107+
let prefix = tree::Prefix::from_str(prefix).map_err(|e| anyhow::anyhow!("{}", e))?;
108+
109+
if args.is_present("all") {
110+
return Err(format_err!(
111+
"The `cargo tree` --all flag has been changed to --no-dedupe.\n\
112+
If you are looking to display all workspace members, use the --workspace flag."
113+
)
114+
.into());
115+
}
116+
117+
let target = if args.is_present("all-targets") {
118+
config
119+
.shell()
120+
.warn("the --all-targets flag has been changed to --target=all")?;
121+
Some("all")
122+
} else {
123+
args.value_of("target")
124+
};
125+
let target = tree::Target::from_cli(target);
126+
127+
let edge_kinds = parse_edge_kinds(config, args)?;
128+
let graph_features = edge_kinds.contains(&EdgeKind::Feature);
129+
130+
let packages = args.packages_from_flags()?;
131+
let mut invert = args
132+
.values_of("invert")
133+
.map_or_else(|| Vec::new(), |is| is.map(|s| s.to_string()).collect());
134+
if args.is_present_with_zero_values("invert") {
135+
match &packages {
136+
Packages::Packages(ps) => {
137+
// Backwards compatibility with old syntax of `cargo tree -i -p foo`.
138+
invert.extend(ps.clone());
139+
}
140+
_ => {
141+
return Err(format_err!(
142+
"The `-i` flag requires a package name.\n\
143+
\n\
144+
The `-i` flag is used to inspect the reverse dependencies of a specific\n\
145+
package. It will invert the tree and display the packages that depend on the\n\
146+
given package.\n\
147+
\n\
148+
Note that in a workspace, by default it will only display the package's\n\
149+
reverse dependencies inside the tree of the workspace member in the current\n\
150+
directory. The --workspace flag can be used to extend it so that it will show\n\
151+
the package's reverse dependencies across the entire workspace. The -p flag\n\
152+
can be used to display the package's reverse dependencies only with the\n\
153+
subtree of the package given to -p.\n\
154+
"
155+
)
156+
.into());
157+
}
158+
}
159+
}
160+
161+
let ws = args.workspace(config)?;
162+
let charset = tree::Charset::from_str(args.value_of("charset").unwrap())
163+
.map_err(|e| anyhow::anyhow!("{}", e))?;
164+
let opts = tree::TreeOptions {
165+
features: values(args, "features"),
166+
all_features: args.is_present("all-features"),
167+
no_default_features: args.is_present("no-default-features"),
168+
packages,
169+
target,
170+
edge_kinds,
171+
invert,
172+
prefix,
173+
no_dedupe: args.is_present("no-dedupe"),
174+
duplicates: args.is_present("duplicates"),
175+
charset,
176+
format: args.value_of("format").unwrap().to_string(),
177+
graph_features,
178+
};
179+
180+
tree::build_and_print(&ws, &opts)?;
181+
Ok(())
182+
}
183+
184+
fn parse_edge_kinds(config: &Config, args: &ArgMatches<'_>) -> CargoResult<HashSet<EdgeKind>> {
185+
let mut kinds: Vec<&str> = args
186+
.values_of("edges")
187+
.map_or_else(|| Vec::new(), |es| es.flat_map(|e| e.split(',')).collect());
188+
if args.is_present("no-dev-dependencies") {
189+
config
190+
.shell()
191+
.warn("the --no-dev-dependencies flag has changed to -e=no-dev")?;
192+
kinds.push("no-dev");
193+
}
194+
if kinds.len() == 0 {
195+
kinds.extend(&["normal", "build", "dev"]);
196+
}
197+
198+
let mut result = HashSet::new();
199+
let insert_defaults = |result: &mut HashSet<EdgeKind>| {
200+
result.insert(EdgeKind::Dep(DepKind::Normal));
201+
result.insert(EdgeKind::Dep(DepKind::Build));
202+
result.insert(EdgeKind::Dep(DepKind::Development));
203+
};
204+
let unknown = |k| {
205+
bail!(
206+
"unknown edge kind `{}`, valid values are \
207+
\"normal\", \"build\", \"dev\", \
208+
\"no-normal\", \"no-build\", \"no-dev\", \
209+
\"features\", or \"all\"",
210+
k
211+
)
212+
};
213+
if kinds.iter().any(|k| k.starts_with("no-")) {
214+
insert_defaults(&mut result);
215+
for kind in &kinds {
216+
match *kind {
217+
"no-normal" => result.remove(&EdgeKind::Dep(DepKind::Normal)),
218+
"no-build" => result.remove(&EdgeKind::Dep(DepKind::Build)),
219+
"no-dev" => result.remove(&EdgeKind::Dep(DepKind::Development)),
220+
"features" => result.insert(EdgeKind::Feature),
221+
"normal" | "build" | "dev" | "all" => {
222+
bail!("`no-` dependency kinds cannot be mixed with other dependency kinds")
223+
}
224+
k => return unknown(k),
225+
};
226+
}
227+
return Ok(result);
228+
}
229+
for kind in &kinds {
230+
match *kind {
231+
"all" => {
232+
insert_defaults(&mut result);
233+
result.insert(EdgeKind::Feature);
234+
}
235+
"features" => {
236+
result.insert(EdgeKind::Feature);
237+
}
238+
"normal" => {
239+
result.insert(EdgeKind::Dep(DepKind::Normal));
240+
}
241+
"build" => {
242+
result.insert(EdgeKind::Dep(DepKind::Build));
243+
}
244+
"dev" => {
245+
result.insert(EdgeKind::Dep(DepKind::Development));
246+
}
247+
k => return unknown(k),
248+
}
249+
}
250+
if kinds.len() == 1 && kinds[0] == "features" {
251+
insert_defaults(&mut result);
252+
}
253+
Ok(result)
254+
}

src/cargo/core/compiler/build_config.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::core::compiler::{CompileKind, CompileTarget};
1+
use crate::core::compiler::CompileKind;
22
use crate::core::interning::InternedString;
33
use crate::util::ProcessBuilder;
44
use crate::util::{CargoResult, Config, RustfixDiagnosticServer};
@@ -45,22 +45,8 @@ impl BuildConfig {
4545
mode: CompileMode,
4646
) -> CargoResult<BuildConfig> {
4747
let cfg = config.build_config()?;
48-
let requested_kind = match requested_target {
49-
Some(s) => CompileKind::Target(CompileTarget::new(s)?),
50-
None => match &cfg.target {
51-
Some(val) => {
52-
let value = if val.raw_value().ends_with(".json") {
53-
let path = val.clone().resolve_path(config);
54-
path.to_str().expect("must be utf-8 in toml").to_string()
55-
} else {
56-
val.raw_value().to_string()
57-
};
58-
CompileKind::Target(CompileTarget::new(&value)?)
59-
}
60-
None => CompileKind::Host,
61-
},
62-
};
63-
48+
let requested_kind =
49+
CompileKind::from_requested_target(config, requested_target.as_deref())?;
6450
if jobs == Some(0) {
6551
anyhow::bail!("jobs must be at least 1")
6652
}

src/cargo/core/compiler/compile_kind.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::core::{InternedString, Target};
22
use crate::util::errors::{CargoResult, CargoResultExt};
3+
use crate::util::Config;
34
use serde::Serialize;
45
use std::path::Path;
56

@@ -39,6 +40,32 @@ impl CompileKind {
3940
CompileKind::Target(n) => CompileKind::Target(n),
4041
}
4142
}
43+
44+
/// Creates a new `CompileKind` based on the requested target.
45+
///
46+
/// If no target is given, this consults the config if the default is set.
47+
/// Otherwise returns `CompileKind::Host`.
48+
pub fn from_requested_target(
49+
config: &Config,
50+
target: Option<&str>,
51+
) -> CargoResult<CompileKind> {
52+
let kind = match target {
53+
Some(s) => CompileKind::Target(CompileTarget::new(s)?),
54+
None => match &config.build_config()?.target {
55+
Some(val) => {
56+
let value = if val.raw_value().ends_with(".json") {
57+
let path = val.clone().resolve_path(config);
58+
path.to_str().expect("must be utf-8 in toml").to_string()
59+
} else {
60+
val.raw_value().to_string()
61+
};
62+
CompileKind::Target(CompileTarget::new(&value)?)
63+
}
64+
None => CompileKind::Host,
65+
},
66+
};
67+
Ok(kind)
68+
}
4269
}
4370

4471
impl serde::ser::Serialize for CompileKind {

src/cargo/core/profiles.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -998,11 +998,7 @@ impl UnitFor {
998998
}
999999

10001000
pub(crate) fn map_to_features_for(&self) -> FeaturesFor {
1001-
if self.is_for_host_features() {
1002-
FeaturesFor::HostDep
1003-
} else {
1004-
FeaturesFor::NormalOrDev
1005-
}
1001+
FeaturesFor::from_for_host(self.is_for_host_features())
10061002
}
10071003
}
10081004

src/cargo/core/resolver/features.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,23 @@ pub enum HasDevUnits {
9292
}
9393

9494
/// Flag to indicate if features are requested for a build dependency or not.
95-
#[derive(Debug, PartialEq)]
95+
#[derive(Copy, Clone, Debug, PartialEq)]
9696
pub enum FeaturesFor {
9797
NormalOrDev,
9898
/// Build dependency or proc-macro.
9999
HostDep,
100100
}
101101

102+
impl FeaturesFor {
103+
pub fn from_for_host(for_host: bool) -> FeaturesFor {
104+
if for_host {
105+
FeaturesFor::HostDep
106+
} else {
107+
FeaturesFor::NormalOrDev
108+
}
109+
}
110+
}
111+
102112
impl FeatureOpts {
103113
fn new(config: &Config, has_dev_units: HasDevUnits) -> CargoResult<FeatureOpts> {
104114
let mut opts = FeatureOpts::default();

src/cargo/core/resolver/resolve.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@ unable to verify that `{0}` is the same as when the lockfile was generated
305305
PackageIdSpec::query_str(spec, self.iter())
306306
}
307307

308+
pub fn specs_to_ids(&self, specs: &[PackageIdSpec]) -> CargoResult<Vec<PackageId>> {
309+
specs.iter().map(|s| s.query(self.iter())).collect()
310+
}
311+
308312
pub fn unused_patches(&self) -> &[PackageId] {
309313
&self.unused_patches
310314
}

0 commit comments

Comments
 (0)