cargo/src/bin/cargo/cli.rs

522 lines
18 KiB
Rust

use anyhow::anyhow;
use cargo::core::shell::Shell;
use cargo::core::{features, CliUnstable};
use cargo::{self, drop_print, drop_println, CliResult, Config};
use clap::{Arg, ArgMatches};
use itertools::Itertools;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fmt::Write;
use super::commands;
use super::list_commands;
use crate::command_prelude::*;
use cargo::core::features::HIDDEN;
lazy_static::lazy_static! {
// Maps from commonly known external commands (not builtin to cargo) to their
// description, for the help page. Reserved for external subcommands that are
// core within the rust ecosystem (esp ones that might become internal in the future).
static ref KNOWN_EXTERNAL_COMMAND_DESCRIPTIONS: HashMap<&'static str, &'static str> = HashMap::from([
("clippy", "Checks a package to catch common mistakes and improve your Rust code."),
("fmt", "Formats all bin and lib files of the current crate using rustfmt."),
]);
}
pub fn main(config: &mut LazyConfig) -> CliResult {
let args = cli().try_get_matches()?;
// CAUTION: Be careful with using `config` until it is configured below.
// In general, try to avoid loading config values unless necessary (like
// the [alias] table).
let config = config.get_mut();
let (expanded_args, global_args) = expand_aliases(config, args, vec![])?;
if expanded_args
.get_one::<String>("unstable-features")
.map(String::as_str)
== Some("help")
{
let options = CliUnstable::help();
let non_hidden_options: Vec<(String, String)> = options
.iter()
.filter(|(_, help_message)| *help_message != HIDDEN)
.map(|(name, help)| (name.to_string(), help.to_string()))
.collect();
let longest_option = non_hidden_options
.iter()
.map(|(option_name, _)| option_name.len())
.max()
.unwrap_or(0);
let help_lines: Vec<String> = non_hidden_options
.iter()
.map(|(option_name, option_help_message)| {
let option_name_kebab_case = option_name.replace("_", "-");
let padding = " ".repeat(longest_option - option_name.len()); // safe to subtract
format!(
" -Z {}{} -- {}",
option_name_kebab_case, padding, option_help_message
)
})
.collect();
let joined = help_lines.join("\n");
drop_println!(
config,
"
Available unstable (nightly-only) flags:
{}
Run with 'cargo -Z [FLAG] [COMMAND]'",
joined
);
if !config.nightly_features_allowed {
drop_println!(
config,
"\nUnstable flags are only available on the nightly channel \
of Cargo, but this is the `{}` channel.\n\
{}",
features::channel(),
features::SEE_CHANNELS
);
}
drop_println!(
config,
"\nSee https://doc.rust-lang.org/nightly/cargo/reference/unstable.html \
for more information about these flags."
);
return Ok(());
}
let is_verbose = expanded_args.verbose() > 0;
if expanded_args.flag("version") {
let version = get_version_string(is_verbose);
drop_print!(config, "{}", version);
return Ok(());
}
if let Some(code) = expanded_args.get_one::<String>("explain") {
let mut procss = config.load_global_rustc(None)?.process();
procss.arg("--explain").arg(code).exec()?;
return Ok(());
}
if expanded_args.flag("list") {
drop_println!(config, "Installed Commands:");
for (name, command) in list_commands(config) {
let known_external_desc = KNOWN_EXTERNAL_COMMAND_DESCRIPTIONS.get(name.as_str());
match command {
CommandInfo::BuiltIn { about } => {
assert!(
known_external_desc.is_none(),
"KNOWN_EXTERNAL_COMMANDS shouldn't contain builtin \"{}\"",
name
);
let summary = about.unwrap_or_default();
let summary = summary.lines().next().unwrap_or(&summary); // display only the first line
drop_println!(config, " {:<20} {}", name, summary);
}
CommandInfo::External { path } => {
if let Some(desc) = known_external_desc {
drop_println!(config, " {:<20} {}", name, desc);
} else if is_verbose {
drop_println!(config, " {:<20} {}", name, path.display());
} else {
drop_println!(config, " {}", name);
}
}
CommandInfo::Alias { target } => {
drop_println!(
config,
" {:<20} alias: {}",
name,
target.iter().join(" ")
);
}
}
}
return Ok(());
}
let (cmd, subcommand_args) = match expanded_args.subcommand() {
Some((cmd, args)) => (cmd, args),
_ => {
// No subcommand provided.
cli().print_help()?;
return Ok(());
}
};
config_configure(config, &expanded_args, subcommand_args, global_args)?;
super::init_git_transports(config);
execute_subcommand(config, cmd, subcommand_args)
}
pub fn get_version_string(is_verbose: bool) -> String {
let version = cargo::version();
let mut version_string = format!("cargo {}\n", version);
if is_verbose {
version_string.push_str(&format!("release: {}\n", version.version));
if let Some(ref ci) = version.commit_info {
version_string.push_str(&format!("commit-hash: {}\n", ci.commit_hash));
version_string.push_str(&format!("commit-date: {}\n", ci.commit_date));
}
writeln!(version_string, "host: {}", env!("RUST_HOST_TARGET")).unwrap();
add_libgit2(&mut version_string);
add_curl(&mut version_string);
add_ssl(&mut version_string);
writeln!(version_string, "os: {}", os_info::get()).unwrap();
}
version_string
}
fn add_libgit2(version_string: &mut String) {
let git2_v = git2::Version::get();
let lib_v = git2_v.libgit2_version();
let vendored = if git2_v.vendored() {
format!("vendored")
} else {
format!("system")
};
writeln!(
version_string,
"libgit2: {}.{}.{} (sys:{} {})",
lib_v.0,
lib_v.1,
lib_v.2,
git2_v.crate_version(),
vendored
)
.unwrap();
}
fn add_curl(version_string: &mut String) {
let curl_v = curl::Version::get();
let vendored = if curl_v.vendored() {
format!("vendored")
} else {
format!("system")
};
writeln!(
version_string,
"libcurl: {} (sys:{} {} ssl:{})",
curl_v.version(),
curl_sys::rust_crate_version(),
vendored,
curl_v.ssl_version().unwrap_or("none")
)
.unwrap();
}
fn add_ssl(version_string: &mut String) {
#[cfg(feature = "openssl")]
{
writeln!(version_string, "ssl: {}", openssl::version::version()).unwrap();
}
#[cfg(not(feature = "openssl"))]
{
let _ = version_string; // Silence unused warning.
}
}
/// Expands aliases recursively to collect all the command line arguments.
///
/// [`GlobalArgs`] need to be extracted before expanding aliases because the
/// clap code for extracting a subcommand discards global options
/// (appearing before the subcommand).
fn expand_aliases(
config: &mut Config,
args: ArgMatches,
mut already_expanded: Vec<String>,
) -> Result<(ArgMatches, GlobalArgs), CliError> {
if let Some((cmd, args)) = args.subcommand() {
let exec = commands::builtin_exec(cmd);
let aliased_cmd = super::aliased_command(config, cmd);
match (exec, aliased_cmd) {
(Some(_), Ok(Some(_))) => {
// User alias conflicts with a built-in subcommand
config.shell().warn(format!(
"user-defined alias `{}` is ignored, because it is shadowed by a built-in command",
cmd,
))?;
}
(Some(_), Ok(None) | Err(_)) => {
// Here we ignore errors from aliasing as we already favor built-in command,
// and alias doesn't involve in this context.
if let Some(values) = args.get_many::<OsString>("") {
// Command is built-in and is not conflicting with alias, but contains ignored values.
return Err(anyhow::format_err!(
"\
trailing arguments after built-in command `{}` are unsupported: `{}`
To pass the arguments to the subcommand, remove `--`",
cmd,
values.map(|s| s.to_string_lossy()).join(" "),
)
.into());
}
}
(None, Ok(None)) => {}
(None, Ok(Some(alias))) => {
// Check if this alias is shadowing an external subcommand
// (binary of the form `cargo-<subcommand>`)
// Currently this is only a warning, but after a transition period this will become
// a hard error.
if let Some(path) = super::find_external_subcommand(config, cmd) {
config.shell().warn(format!(
"\
user-defined alias `{}` is shadowing an external subcommand found at: `{}`
This was previously accepted but is being phased out; it will become a hard error in a future release.
For more information, see issue #10049 <https://github.com/rust-lang/cargo/issues/10049>.",
cmd,
path.display(),
))?;
}
let mut alias = alias
.into_iter()
.map(|s| OsString::from(s))
.collect::<Vec<_>>();
alias.extend(args.get_many::<OsString>("").unwrap_or_default().cloned());
// new_args strips out everything before the subcommand, so
// capture those global options now.
// Note that an alias to an external command will not receive
// these arguments. That may be confusing, but such is life.
let global_args = GlobalArgs::new(args);
let new_args = cli().no_binary_name(true).try_get_matches_from(alias)?;
let new_cmd = new_args.subcommand_name().expect("subcommand is required");
already_expanded.push(cmd.to_string());
if already_expanded.contains(&new_cmd.to_string()) {
// Crash if the aliases are corecursive / unresolvable
return Err(anyhow!(
"alias {} has unresolvable recursive definition: {} -> {}",
already_expanded[0],
already_expanded.join(" -> "),
new_cmd,
)
.into());
}
let (expanded_args, _) = expand_aliases(config, new_args, already_expanded)?;
return Ok((expanded_args, global_args));
}
(None, Err(e)) => return Err(e.into()),
}
};
Ok((args, GlobalArgs::default()))
}
fn config_configure(
config: &mut Config,
args: &ArgMatches,
subcommand_args: &ArgMatches,
global_args: GlobalArgs,
) -> CliResult {
let arg_target_dir = &subcommand_args.value_of_path("target-dir", config);
let verbose = global_args.verbose + args.verbose();
// quiet is unusual because it is redefined in some subcommands in order
// to provide custom help text.
let quiet = args.flag("quiet") || subcommand_args.flag("quiet") || global_args.quiet;
let global_color = global_args.color; // Extract so it can take reference.
let color = args
.get_one::<String>("color")
.map(String::as_str)
.or_else(|| global_color.as_deref());
let frozen = args.flag("frozen") || global_args.frozen;
let locked = args.flag("locked") || global_args.locked;
let offline = args.flag("offline") || global_args.offline;
let mut unstable_flags = global_args.unstable_flags;
if let Some(values) = args.get_many::<String>("unstable-features") {
unstable_flags.extend(values.cloned());
}
let mut config_args = global_args.config_args;
if let Some(values) = args.get_many::<String>("config") {
config_args.extend(values.cloned());
}
config.configure(
verbose,
quiet,
color,
frozen,
locked,
offline,
arg_target_dir,
&unstable_flags,
&config_args,
)?;
Ok(())
}
fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatches) -> CliResult {
if let Some(exec) = commands::builtin_exec(cmd) {
return exec(config, subcommand_args);
}
let mut ext_args: Vec<&OsStr> = vec![OsStr::new(cmd)];
ext_args.extend(
subcommand_args
.get_many::<OsString>("")
.unwrap_or_default()
.map(OsString::as_os_str),
);
super::execute_external_subcommand(config, cmd, &ext_args)
}
#[derive(Default)]
struct GlobalArgs {
verbose: u32,
quiet: bool,
color: Option<String>,
frozen: bool,
locked: bool,
offline: bool,
unstable_flags: Vec<String>,
config_args: Vec<String>,
}
impl GlobalArgs {
fn new(args: &ArgMatches) -> GlobalArgs {
GlobalArgs {
verbose: args.verbose(),
quiet: args.flag("quiet"),
color: args.get_one::<String>("color").cloned(),
frozen: args.flag("frozen"),
locked: args.flag("locked"),
offline: args.flag("offline"),
unstable_flags: args
.get_many::<String>("unstable-features")
.unwrap_or_default()
.cloned()
.collect(),
config_args: args
.get_many::<String>("config")
.unwrap_or_default()
.cloned()
.collect(),
}
}
}
pub fn cli() -> Command {
let is_rustup = std::env::var_os("RUSTUP_HOME").is_some();
let usage = if is_rustup {
"cargo [+toolchain] [OPTIONS] [COMMAND]"
} else {
"cargo [OPTIONS] [COMMAND]"
};
Command::new("cargo")
.allow_external_subcommands(true)
// Doesn't mix well with our list of common cargo commands. See clap-rs/clap#3108 for
// opening clap up to allow us to style our help template
.disable_colored_help(true)
// Provide a custom help subcommand for calling into man pages
.disable_help_subcommand(true)
.override_usage(usage)
.help_template(
"\
Rust's package manager
Usage: {usage}
Options:
{options}
Some common cargo commands are (see all commands with --list):
build, b Compile the current package
check, c Analyze the current package and report errors, but don't build object files
clean Remove the target directory
doc, d Build this package's and its dependencies' documentation
new Create a new cargo package
init Create a new cargo package in an existing directory
add Add dependencies to a manifest file
run, r Run a binary or example of the local package
test, t Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this package to the registry
install Install a Rust binary. Default location is $HOME/.cargo/bin
uninstall Uninstall a Rust binary
See 'cargo help <command>' for more information on a specific command.\n",
)
.arg(flag("version", "Print version info and exit").short('V'))
.arg(flag("list", "List installed commands"))
.arg(opt("explain", "Run `rustc --explain CODE`").value_name("CODE"))
.arg(
opt(
"verbose",
"Use verbose output (-vv very verbose/build.rs output)",
)
.short('v')
.action(ArgAction::Count)
.global(true),
)
.arg_quiet()
.arg(
opt("color", "Coloring: auto, always, never")
.value_name("WHEN")
.global(true),
)
.arg(flag("frozen", "Require Cargo.lock and cache are up to date").global(true))
.arg(flag("locked", "Require Cargo.lock is up to date").global(true))
.arg(flag("offline", "Run without accessing the network").global(true))
.arg(multi_opt("config", "KEY=VALUE", "Override a configuration value").global(true))
.arg(
Arg::new("unstable-features")
.help("Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details")
.short('Z')
.value_name("FLAG")
.action(ArgAction::Append)
.global(true),
)
.subcommands(commands::builtin())
}
/// Delay loading [`Config`] until access.
///
/// In the common path, the [`Config`] is dependent on CLI parsing and shouldn't be loaded until
/// after that is done but some other paths (like fix or earlier errors) might need access to it,
/// so this provides a way to share the instance and the implementation across these different
/// accesses.
pub struct LazyConfig {
config: Option<Config>,
}
impl LazyConfig {
pub fn new() -> Self {
Self { config: None }
}
/// Get the config, loading it if needed
///
/// On error, the process is terminated
pub fn get(&mut self) -> &Config {
self.get_mut()
}
/// Get the config, loading it if needed
///
/// On error, the process is terminated
pub fn get_mut(&mut self) -> &mut Config {
self.config.get_or_insert_with(|| match Config::default() {
Ok(cfg) => cfg,
Err(e) => {
let mut shell = Shell::new();
cargo::exit_with_error(e.into(), &mut shell)
}
})
}
}
#[test]
fn verify_cli() {
cli().debug_assert();
}