Skip to content

Commit 2732539

Browse files
committed
Auto merge of #8456 - ehuss:embedded-man, r=alexcrichton
Display embedded man pages for built-in commands. This changes `cargo help COMMAND` to display the man page for the given command. `cargo COMMAND --help` continues to show the basic clap output. The man pages are embedded in the executable in a compressed format. There's also a copy of the man pages in text format for platforms that do not have the `man` executable (like Windows). It is unfortunate to check in more pre-generated files. I hope in the future that the usage of asciidoc can be replaced with something else (possibly a custom markdown-based solution). cc #6104
2 parents e928120 + 0d6881c commit 2732539

39 files changed

+342
-373
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ features = [
106106
cargo-test-macro = { path = "crates/cargo-test-macro", version = "0.1.0" }
107107
cargo-test-support = { path = "crates/cargo-test-support", version = "0.1.0" }
108108

109+
[build-dependencies]
110+
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
111+
tar = { version = "0.4.26", default-features = false }
112+
109113
[[bin]]
110114
name = "cargo"
111115
test = false

build.rs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use flate2::{Compression, GzBuilder};
2+
use std::ffi::OsStr;
3+
use std::fs;
4+
use std::path::Path;
5+
6+
fn main() {
7+
compress_man();
8+
}
9+
10+
fn compress_man() {
11+
let out_path = Path::new(&std::env::var("OUT_DIR").unwrap()).join("man.tgz");
12+
let dst = fs::File::create(out_path).unwrap();
13+
let encoder = GzBuilder::new()
14+
.filename("man.tar")
15+
.write(dst, Compression::best());
16+
let mut ar = tar::Builder::new(encoder);
17+
18+
let mut add_files = |dir, extension| {
19+
for entry in fs::read_dir(dir).unwrap() {
20+
let path = entry.unwrap().path();
21+
if path.extension() != Some(extension) {
22+
continue;
23+
}
24+
println!("cargo:rerun-if-changed={}", path.display());
25+
ar.append_path_with_name(&path, path.file_name().unwrap())
26+
.unwrap();
27+
}
28+
};
29+
30+
add_files(Path::new("src/etc/man"), OsStr::new("1"));
31+
add_files(Path::new("src/doc/man/generated_txt"), OsStr::new("txt"));
32+
let encoder = ar.into_inner().unwrap();
33+
encoder.finish().unwrap();
34+
}

src/bin/cargo/cli.rs

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ pub fn main(config: &mut Config) -> CliResult {
1010
// CAUTION: Be careful with using `config` until it is configured below.
1111
// In general, try to avoid loading config values unless necessary (like
1212
// the [alias] table).
13+
14+
if commands::help::handle_embedded_help(config) {
15+
return Ok(());
16+
}
17+
1318
let args = match cli().get_matches_safe() {
1419
Ok(args) => args,
1520
Err(e) => {

src/bin/cargo/commands/bench.rs

+1-24
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,7 @@ pub fn cli() -> App {
4545
"Run all benchmarks regardless of failure",
4646
))
4747
.arg_unit_graph()
48-
.after_help(
49-
"\
50-
The benchmark filtering argument BENCHNAME and all the arguments following the
51-
two dashes (`--`) are passed to the benchmark binaries and thus to libtest
52-
(rustc's built in unit-test and micro-benchmarking framework). If you're
53-
passing arguments to both Cargo and the binary, the ones after `--` go to the
54-
binary, the ones before go to Cargo. For details about libtest's arguments see
55-
the output of `cargo bench -- --help`.
56-
57-
If the `--package` argument is given, then SPEC is a package ID specification
58-
which indicates which package should be benchmarked. If it is not given, then
59-
the current package is benchmarked. For more information on SPEC and its format,
60-
see the `cargo help pkgid` command.
61-
62-
All packages in the workspace are benchmarked if the `--workspace` flag is supplied. The
63-
`--workspace` flag is automatically assumed for a virtual manifest.
64-
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.
65-
66-
The `--jobs` argument affects the building of the benchmark executable but does
67-
not affect how many jobs are used when running the benchmarks.
68-
69-
Compilation can be customized with the `bench` profile in the manifest.
70-
",
71-
)
48+
.after_help("Run `cargo help bench` for more detailed information.\n")
7249
}
7350

7451
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/build.rs

+1-11
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,7 @@ pub fn cli() -> App {
4242
.arg_message_format()
4343
.arg_build_plan()
4444
.arg_unit_graph()
45-
.after_help(
46-
"\
47-
All packages in the workspace are built if the `--workspace` flag is supplied. The
48-
`--workspace` flag is automatically assumed for a virtual manifest.
49-
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.
50-
51-
Compilation can be configured via the use of profiles which are configured in
52-
the manifest. The default profile for this command is `dev`, but passing
53-
the --release flag will use the `release` profile instead.
54-
",
55-
)
45+
.after_help("Run `cargo help build` for more detailed information.\n")
5646
}
5747

5848
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/check.rs

+1-19
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,7 @@ pub fn cli() -> App {
3434
.arg_manifest_path()
3535
.arg_message_format()
3636
.arg_unit_graph()
37-
.after_help(
38-
"\
39-
If the `--package` argument is given, then SPEC is a package ID specification
40-
which indicates which package should be built. If it is not given, then the
41-
current package is built. For more information on SPEC and its format, see the
42-
`cargo help pkgid` command.
43-
44-
All packages in the workspace are checked if the `--workspace` flag is supplied. The
45-
`--workspace` flag is automatically assumed for a virtual manifest.
46-
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.
47-
48-
Compilation can be configured via the use of profiles which are configured in
49-
the manifest. The default profile for this command is `dev`, but passing
50-
the `--release` flag will use the `release` profile instead.
51-
52-
The `--profile test` flag can be used to check unit tests with the
53-
`#[cfg(test)]` attribute.
54-
",
55-
)
37+
.after_help("Run `cargo help check` for more detailed information.\n")
5638
}
5739

5840
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/clean.rs

+1-8
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,7 @@ pub fn cli() -> App {
1313
.arg_release("Whether or not to clean release artifacts")
1414
.arg_profile("Clean artifacts of the specified profile")
1515
.arg_doc("Whether or not to clean just the documentation directory")
16-
.after_help(
17-
"\
18-
If the `--package` argument is given, then SPEC is a package ID specification
19-
which indicates which package's artifacts should be cleaned out. If it is not
20-
given, then all packages' artifacts are removed. For more information on SPEC
21-
and its format, see the `cargo help pkgid` command.
22-
",
23-
)
16+
.after_help("Run `cargo help clean` for more detailed information.\n")
2417
}
2518

2619
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/doc.rs

+1-16
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,7 @@ pub fn cli() -> App {
3131
.arg_manifest_path()
3232
.arg_message_format()
3333
.arg_unit_graph()
34-
.after_help(
35-
"\
36-
By default the documentation for the local package and all dependencies is
37-
built. The output is all placed in `target/doc` in rustdoc's usual format.
38-
39-
All packages in the workspace are documented if the `--workspace` flag is
40-
supplied. The `--workspace` flag is automatically assumed for a virtual
41-
manifest. Note that `--exclude` has to be specified in conjunction with the
42-
`--workspace` flag.
43-
44-
If the `--package` argument is given, then SPEC is a package ID specification
45-
which indicates which package should be documented. If it is not given, then the
46-
current package is documented. For more information on SPEC and its format, see
47-
the `cargo help pkgid` command.
48-
",
49-
)
34+
.after_help("Run `cargo help doc` for more detailed information.\n")
5035
}
5136

5237
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/fetch.rs

+1-12
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,7 @@ pub fn cli() -> App {
99
.arg(opt("quiet", "No output printed to stdout").short("q"))
1010
.arg_manifest_path()
1111
.arg_target_triple("Fetch dependencies for the target triple")
12-
.after_help(
13-
"\
14-
If a lock file is available, this command will ensure that all of the Git
15-
dependencies and/or registries dependencies are downloaded and locally
16-
available. The network is never touched after a `cargo fetch` unless
17-
the lock file changes.
18-
19-
If the lock file is not available, then this is the equivalent of
20-
`cargo generate-lockfile`. A lock file is generated and dependencies are also
21-
all updated.
22-
",
23-
)
12+
.after_help("Run `cargo help fetch` for more detailed information.\n")
2413
}
2514

2615
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/fix.rs

+1-31
Original file line numberDiff line numberDiff line change
@@ -72,37 +72,7 @@ pub fn cli() -> App {
7272
.long("allow-staged")
7373
.help("Fix code even if the working directory has staged changes"),
7474
)
75-
.after_help(
76-
"\
77-
This Cargo subcommand will automatically take rustc's suggestions from
78-
diagnostics like warnings and apply them to your source code. This is intended
79-
to help automate tasks that rustc itself already knows how to tell you to fix!
80-
The `cargo fix` subcommand is also being developed for the Rust 2018 edition
81-
to provide code the ability to easily opt-in to the new edition without having
82-
to worry about any breakage.
83-
84-
Executing `cargo fix` will under the hood execute `cargo check`. Any warnings
85-
applicable to your crate will be automatically fixed (if possible) and all
86-
remaining warnings will be displayed when the check process is finished. For
87-
example if you'd like to prepare for the 2018 edition, you can do so by
88-
executing:
89-
90-
cargo fix --edition
91-
92-
which behaves the same as `cargo check --all-targets`. Similarly if you'd like
93-
to fix code for different platforms you can do:
94-
95-
cargo fix --edition --target x86_64-pc-windows-gnu
96-
97-
or if your crate has optional features:
98-
99-
cargo fix --edition --no-default-features --features foo
100-
101-
If you encounter any problems with `cargo fix` or otherwise have any questions
102-
or feature requests please don't hesitate to file an issue at
103-
https://github.com/rust-lang/cargo
104-
",
105-
)
75+
.after_help("Run `cargo help fix` for more detailed information.\n")
10676
}
10777

10878
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/generate_lockfile.rs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub fn cli() -> App {
77
.about("Generate the lockfile for a package")
88
.arg(opt("quiet", "No output printed to stdout").short("q"))
99
.arg_manifest_path()
10+
.after_help("Run `cargo help generate-lockfile` for more detailed information.\n")
1011
}
1112

1213
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

src/bin/cargo/commands/help.rs

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use crate::aliased_command;
2+
use cargo::util::errors::CargoResult;
3+
use cargo::util::paths::resolve_executable;
4+
use cargo::Config;
5+
use flate2::read::GzDecoder;
6+
use std::ffi::OsString;
7+
use std::io::Read;
8+
use std::io::Write;
9+
use std::path::Path;
10+
11+
const COMPRESSED_MAN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/man.tgz"));
12+
13+
/// Checks if the `help` command is being issued.
14+
///
15+
/// This runs before clap processing, because it needs to intercept the `help`
16+
/// command if a man page is available.
17+
///
18+
/// Returns `true` if a man page was displayed. In this case, Cargo should
19+
/// exit.
20+
pub fn handle_embedded_help(config: &Config) -> bool {
21+
match try_help(config) {
22+
Ok(true) => true,
23+
Ok(false) => false,
24+
Err(e) => {
25+
log::warn!("man failed: {:?}", e);
26+
false
27+
}
28+
}
29+
}
30+
31+
fn try_help(config: &Config) -> CargoResult<bool> {
32+
let mut args = std::env::args_os()
33+
.skip(1)
34+
.skip_while(|arg| arg.to_str().map_or(false, |s| s.starts_with('-')));
35+
if !args
36+
.next()
37+
.map_or(false, |arg| arg.to_str() == Some("help"))
38+
{
39+
return Ok(false);
40+
}
41+
let subcommand = match args.next() {
42+
Some(arg) => arg,
43+
None => return Ok(false),
44+
};
45+
let subcommand = match subcommand.to_str() {
46+
Some(s) => s,
47+
None => return Ok(false),
48+
};
49+
// Check if this is a built-in command (or alias);
50+
let subcommand = match check_alias(config, subcommand) {
51+
Some(s) => s,
52+
None => return Ok(false),
53+
};
54+
if resolve_executable(Path::new("man")).is_ok() {
55+
let man = match extract_man(&subcommand, "1") {
56+
Some(man) => man,
57+
None => return Ok(false),
58+
};
59+
write_and_spawn(&man, "man")?;
60+
} else {
61+
let txt = match extract_man(&subcommand, "txt") {
62+
Some(txt) => txt,
63+
None => return Ok(false),
64+
};
65+
if resolve_executable(Path::new("less")).is_ok() {
66+
write_and_spawn(&txt, "less")?;
67+
} else if resolve_executable(Path::new("more")).is_ok() {
68+
write_and_spawn(&txt, "more")?;
69+
} else {
70+
drop(std::io::stdout().write_all(&txt));
71+
}
72+
}
73+
Ok(true)
74+
}
75+
76+
/// Checks if the given subcommand is a built-in command (possibly via an alias).
77+
///
78+
/// Returns None if it is not a built-in command.
79+
fn check_alias(config: &Config, subcommand: &str) -> Option<String> {
80+
if super::builtin_exec(subcommand).is_some() {
81+
return Some(subcommand.to_string());
82+
}
83+
match aliased_command(config, subcommand) {
84+
Ok(Some(alias)) => {
85+
let alias = alias.into_iter().next()?;
86+
if super::builtin_exec(&alias).is_some() {
87+
Some(alias)
88+
} else {
89+
None
90+
}
91+
}
92+
_ => None,
93+
}
94+
}
95+
96+
/// Extracts the given man page from the compressed archive.
97+
///
98+
/// Returns None if the command wasn't found.
99+
fn extract_man(subcommand: &str, extension: &str) -> Option<Vec<u8>> {
100+
let extract_name = OsString::from(format!("cargo-{}.{}", subcommand, extension));
101+
let gz = GzDecoder::new(COMPRESSED_MAN);
102+
let mut ar = tar::Archive::new(gz);
103+
// Unwraps should be safe here, since this is a static archive generated
104+
// by our build script. It should never be an invalid format!
105+
for entry in ar.entries().unwrap() {
106+
let mut entry = entry.unwrap();
107+
let path = entry.path().unwrap();
108+
if path.file_name().unwrap() != extract_name {
109+
continue;
110+
}
111+
let mut result = Vec::new();
112+
entry.read_to_end(&mut result).unwrap();
113+
return Some(result);
114+
}
115+
None
116+
}
117+
118+
/// Write the contents of a man page to disk and spawn the given command to
119+
/// display it.
120+
fn write_and_spawn(contents: &[u8], command: &str) -> CargoResult<()> {
121+
let mut tmp = tempfile::Builder::new().prefix("cargo-man").tempfile()?;
122+
let f = tmp.as_file_mut();
123+
f.write_all(&contents)?;
124+
f.flush()?;
125+
let mut cmd = std::process::Command::new(command)
126+
.arg(tmp.path())
127+
.spawn()?;
128+
drop(cmd.wait());
129+
Ok(())
130+
}

src/bin/cargo/commands/init.rs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub fn cli() -> App {
99
.arg(Arg::with_name("path").default_value("."))
1010
.arg(opt("registry", "Registry to use").value_name("REGISTRY"))
1111
.arg_new_opts()
12+
.after_help("Run `cargo help init` for more detailed information.\n")
1213
}
1314

1415
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {

0 commit comments

Comments
 (0)