blob: fc5896065605fd57dcf8360f2243d0b597bf2479 [file] [log] [blame]
use anyhow::{anyhow, Context as _};
use cargo::core::{features, CliUnstable};
use cargo::util::context::TermConfig;
use cargo::{drop_print, drop_println, CargoResult};
use clap::builder::UnknownArgumentValueParser;
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 crate::util::is_rustup;
use cargo::core::shell::ColorChoice;
use cargo::util::style;
#[tracing::instrument(skip_all)]
pub fn main(gctx: &mut GlobalContext) -> CliResult {
// 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 args = cli(gctx).try_get_matches()?;
// Update the process-level notion of cwd
if let Some(new_cwd) = args.get_one::<std::path::PathBuf>("directory") {
// This is a temporary hack.
// This cannot access `GlobalContext`, so this is a bit messy.
// This does not properly parse `-Z` flags that appear after the subcommand.
// The error message is not as helpful as the standard one.
let nightly_features_allowed = matches!(&*features::channel(), "nightly" | "dev");
if !nightly_features_allowed
|| (nightly_features_allowed
&& !args
.get_many("unstable-features")
.map(|mut z| z.any(|value: &String| value == "unstable-options"))
.unwrap_or(false))
{
return Err(anyhow::format_err!(
"the `-C` flag is unstable, \
pass `-Z unstable-options` on the nightly channel to enable it"
)
.into());
}
std::env::set_current_dir(&new_cwd).context("could not change to requested directory")?;
gctx.reload_cwd()?;
}
let (expanded_args, global_args) = expand_aliases(gctx, args, vec![])?;
let is_verbose = expanded_args.verbose() > 0;
if expanded_args
.get_one::<String>("unstable-features")
.map(String::as_str)
== Some("help")
{
// Don't let config errors get in the way of parsing arguments
let _ = configure_gctx(gctx, &expanded_args, None, global_args, None);
print_zhelp(gctx);
} else if expanded_args.flag("version") {
// Don't let config errors get in the way of parsing arguments
let _ = configure_gctx(gctx, &expanded_args, None, global_args, None);
let version = get_version_string(is_verbose);
drop_print!(gctx, "{}", version);
} else if let Some(code) = expanded_args.get_one::<String>("explain") {
// Don't let config errors get in the way of parsing arguments
let _ = configure_gctx(gctx, &expanded_args, None, global_args, None);
let mut procss = gctx.load_global_rustc(None)?.process();
procss.arg("--explain").arg(code).exec()?;
} else if expanded_args.flag("list") {
// Don't let config errors get in the way of parsing arguments
let _ = configure_gctx(gctx, &expanded_args, None, global_args, None);
print_list(gctx, is_verbose);
} else {
let (cmd, subcommand_args) = match expanded_args.subcommand() {
Some((cmd, args)) => (cmd, args),
_ => {
// No subcommand provided.
cli(gctx).print_help()?;
return Ok(());
}
};
let exec = Exec::infer(cmd)?;
configure_gctx(
gctx,
&expanded_args,
Some(subcommand_args),
global_args,
Some(&exec),
)?;
super::init_git(gctx);
exec.exec(gctx, subcommand_args)?;
}
Ok(())
}
fn print_zhelp(gctx: &GlobalContext) {
let header = style::HEADER;
let literal = style::LITERAL;
let placeholder = style::PLACEHOLDER;
let options = CliUnstable::help();
let max_length = options
.iter()
.filter(|(_, help)| help.is_some())
.map(|(option_name, _)| option_name.len())
.max()
.unwrap_or(0);
let z_flags = options
.iter()
.filter(|(_, help)| help.is_some())
.map(|(opt, help)| {
let opt = opt.replace("_", "-");
let help = help.unwrap();
format!(" {literal}-Z {opt:<max_length$}{literal:#} {help}")
})
.join("\n");
drop_println!(
gctx,
"\
{header}Available unstable (nightly-only) flags:{header:#}
{z_flags}
Run with `{literal}cargo -Z{literal:#} {placeholder}[FLAG] [COMMAND]{placeholder:#}`",
);
if !gctx.nightly_features_allowed {
drop_println!(
gctx,
"\nUnstable flags are only available on the nightly channel \
of Cargo, but this is the `{}` channel.\n\
{}",
features::channel(),
features::SEE_CHANNELS
);
}
drop_println!(
gctx,
"\nSee https://doc.rust-lang.org/nightly/cargo/reference/unstable.html \
for more information about these flags."
);
}
fn print_list(gctx: &GlobalContext, is_verbose: bool) {
// 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).
let known_external_command_descriptions = 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.",
),
]);
drop_println!(
gctx,
color_print::cstr!("<green,bold>Installed Commands:</>")
);
for (name, command) in list_commands(gctx) {
let known_external_desc = known_external_command_descriptions.get(name.as_str());
let literal = style::LITERAL;
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!(gctx, " {literal}{name:<20}{literal:#} {summary}");
}
CommandInfo::External { path } => {
if let Some(desc) = known_external_desc {
drop_println!(gctx, " {literal}{name:<20}{literal:#} {desc}");
} else if is_verbose {
drop_println!(
gctx,
" {literal}{name:<20}{literal:#} {}",
path.display()
);
} else {
drop_println!(gctx, " {literal}{name}{literal:#}");
}
}
CommandInfo::Alias { target } => {
drop_println!(
gctx,
" {literal}{name:<20}{literal:#} alias: {}",
target.iter().join(" ")
);
}
}
}
}
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).
#[tracing::instrument(skip_all)]
fn expand_aliases(
gctx: &mut GlobalContext,
args: ArgMatches,
mut already_expanded: Vec<String>,
) -> Result<(ArgMatches, GlobalArgs), CliError> {
if let Some((cmd, sub_args)) = args.subcommand() {
let exec = commands::builtin_exec(cmd);
let aliased_cmd = super::aliased_command(gctx, cmd);
match (exec, aliased_cmd) {
(Some(_), Ok(Some(_))) => {
// User alias conflicts with a built-in subcommand
gctx.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) = sub_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 a user-defined 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 super::builtin_aliases_execs(cmd).is_none() {
if let Some(path) = super::find_external_subcommand(gctx, cmd) {
gctx.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(),
))?;
}
}
if commands::run::is_manifest_command(cmd) {
if gctx.cli_unstable().script {
return Ok((args, GlobalArgs::default()));
} else {
gctx.shell().warn(format_args!(
"\
user-defined alias `{cmd}` has the appearance of a manifest-command
This was previously accepted but will be phased out when `-Zscript` is stabilized.
For more information, see issue #12207 <https://github.com/rust-lang/cargo/issues/12207>."
))?;
}
}
let mut alias = alias
.into_iter()
.map(|s| OsString::from(s))
.collect::<Vec<_>>();
alias.extend(
sub_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(sub_args);
let new_args = cli(gctx).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(gctx, new_args, already_expanded)?;
return Ok((expanded_args, global_args));
}
(None, Err(e)) => return Err(e.into()),
}
};
Ok((args, GlobalArgs::default()))
}
#[tracing::instrument(skip_all)]
fn configure_gctx(
gctx: &mut GlobalContext,
args: &ArgMatches,
subcommand_args: Option<&ArgMatches>,
global_args: GlobalArgs,
exec: Option<&Exec>,
) -> CliResult {
let arg_target_dir = &subcommand_args.and_then(|a| a.value_of_path("target-dir", gctx));
let mut verbose = global_args.verbose + args.verbose();
// quiet is unusual because it is redefined in some subcommands in order
// to provide custom help text.
let mut quiet = args.flag("quiet")
|| subcommand_args.map(|a| a.flag("quiet")).unwrap_or_default()
|| global_args.quiet;
if matches!(exec, Some(Exec::Manifest(_))) && !quiet {
// Verbosity is shifted quieter for `Exec::Manifest` as it is can be used as if you ran
// `cargo install` and we especially shouldn't pollute programmatic output.
//
// For now, interactive output has the same default output as `cargo run` but that is
// subject to change.
if let Some(lower) = verbose.checked_sub(1) {
verbose = lower;
} else if !gctx.shell().is_err_tty() {
// Don't pollute potentially-scripted output
quiet = true;
}
}
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());
}
gctx.configure(
verbose,
quiet,
color,
frozen,
locked,
offline,
arg_target_dir,
&unstable_flags,
&config_args,
)?;
Ok(())
}
enum Exec {
Builtin(commands::Exec),
Manifest(String),
External(String),
}
impl Exec {
/// Precedence isn't the most obvious from this function because
/// - Some is determined by `expand_aliases`
/// - Some is enforced by `avoid_ambiguity_between_builtins_and_manifest_commands`
///
/// In actuality, it is:
/// 1. built-ins xor manifest-command
/// 2. aliases
/// 3. external subcommands
fn infer(cmd: &str) -> CargoResult<Self> {
if let Some(exec) = commands::builtin_exec(cmd) {
Ok(Self::Builtin(exec))
} else if commands::run::is_manifest_command(cmd) {
Ok(Self::Manifest(cmd.to_owned()))
} else {
Ok(Self::External(cmd.to_owned()))
}
}
#[tracing::instrument(skip_all)]
fn exec(self, gctx: &mut GlobalContext, subcommand_args: &ArgMatches) -> CliResult {
match self {
Self::Builtin(exec) => exec(gctx, subcommand_args),
Self::Manifest(cmd) => {
let ext_path = super::find_external_subcommand(gctx, &cmd);
if !gctx.cli_unstable().script && ext_path.is_some() {
gctx.shell().warn(format_args!(
"\
external subcommand `{cmd}` has the appearance of a manifest-command
This was previously accepted but will be phased out when `-Zscript` is stabilized.
For more information, see issue #12207 <https://github.com/rust-lang/cargo/issues/12207>.",
))?;
Self::External(cmd).exec(gctx, subcommand_args)
} else {
let ext_args: Vec<OsString> = subcommand_args
.get_many::<OsString>("")
.unwrap_or_default()
.cloned()
.collect();
commands::run::exec_manifest_command(gctx, &cmd, &ext_args)
}
}
Self::External(cmd) => {
let mut ext_args = vec![OsStr::new(&cmd)];
ext_args.extend(
subcommand_args
.get_many::<OsString>("")
.unwrap_or_default()
.map(OsString::as_os_str),
);
super::execute_external_subcommand(gctx, &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(),
}
}
}
#[tracing::instrument(skip_all)]
pub fn cli(gctx: &GlobalContext) -> Command {
// Don't let config errors get in the way of parsing arguments
let term = gctx.get::<TermConfig>("term").unwrap_or_default();
let color = term
.color
.and_then(|c| c.parse().ok())
.unwrap_or(ColorChoice::CargoAuto);
let color = match color {
ColorChoice::Always => clap::ColorChoice::Always,
ColorChoice::Never => clap::ColorChoice::Never,
ColorChoice::CargoAuto => clap::ColorChoice::Auto,
};
let usage = if is_rustup() {
color_print::cstr!("<cyan,bold>cargo</> <cyan>[+toolchain] [OPTIONS] [COMMAND]</>\n <cyan,bold>cargo</> <cyan>[+toolchain] [OPTIONS]</> <cyan,bold>-Zscript</> <cyan><<MANIFEST_RS>> [ARGS]...</>")
} else {
color_print::cstr!("<cyan,bold>cargo</> <cyan>[OPTIONS] [COMMAND]</>\n <cyan,bold>cargo</> <cyan>[OPTIONS]</> <cyan,bold>-Zscript</> <cyan><<MANIFEST_RS>> [ARGS]...</>")
};
let styles = {
clap::builder::styling::Styles::styled()
.header(style::HEADER)
.usage(style::USAGE)
.literal(style::LITERAL)
.placeholder(style::PLACEHOLDER)
.error(style::ERROR)
.valid(style::VALID)
.invalid(style::INVALID)
};
Command::new("cargo")
// Subcommands all count their args' display order independently (from 0),
// which makes their args interspersed with global args. This puts global args last.
//
// We also want these to come before auto-generated `--help`
.next_display_order(800)
.allow_external_subcommands(true)
.color(color)
.styles(styles)
// Provide a custom help subcommand for calling into man pages
.disable_help_subcommand(true)
.override_usage(usage)
.help_template(color_print::cstr!(
"\
Rust's package manager
<green,bold>Usage:</> {usage}
<green,bold>Options:</>
{options}
<green,bold>Commands:</>
<cyan,bold>build</>, <cyan,bold>b</> Compile the current package
<cyan,bold>check</>, <cyan,bold>c</> Analyze the current package and report errors, but don't build object files
<cyan,bold>clean</> Remove the target directory
<cyan,bold>doc</>, <cyan,bold>d</> Build this package's and its dependencies' documentation
<cyan,bold>new</> Create a new cargo package
<cyan,bold>init</> Create a new cargo package in an existing directory
<cyan,bold>add</> Add dependencies to a manifest file
<cyan,bold>remove</> Remove dependencies from a manifest file
<cyan,bold>run</>, <cyan,bold>r</> Run a binary or example of the local package
<cyan,bold>test</>, <cyan,bold>t</> Run the tests
<cyan,bold>bench</> Run the benchmarks
<cyan,bold>update</> Update dependencies listed in Cargo.lock
<cyan,bold>search</> Search registry for crates
<cyan,bold>publish</> Package and upload this package to the registry
<cyan,bold>install</> Install a Rust binary
<cyan,bold>uninstall</> Uninstall a Rust binary
<cyan>...</> See all commands with <cyan,bold>--list</>
See '<cyan,bold>cargo help</> <cyan><<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",
"Provide a detailed explanation of a rustc error message",
)
.value_name("CODE"),
)
.arg(
opt(
"verbose",
"Use verbose output (-vv very verbose/build.rs output)",
)
.short('v')
.action(ArgAction::Count)
.global(true),
)
.arg(flag("quiet", "Do not print cargo log messages").short('q').global(true))
.arg(
opt("color", "Coloring: auto, always, never")
.value_name("WHEN")
.global(true),
)
.arg(
Arg::new("directory")
.help("Change to DIRECTORY before doing anything (nightly-only)")
.short('C')
.value_name("DIRECTORY")
.value_hint(clap::ValueHint::DirPath)
.value_parser(clap::builder::ValueParser::path_buf()),
)
.arg(
flag("frozen", "Require Cargo.lock and cache are up to date")
.help_heading(heading::MANIFEST_OPTIONS)
.global(true),
)
.arg(
flag("locked", "Require Cargo.lock is up to date")
.help_heading(heading::MANIFEST_OPTIONS)
.global(true),
)
.arg(
flag("offline", "Run without accessing the network")
.help_heading(heading::MANIFEST_OPTIONS)
.global(true),
)
// Better suggestion for the unsupported short config flag.
.arg( Arg::new("unsupported-short-config-flag")
.help("")
.short('c')
.value_parser(UnknownArgumentValueParser::suggest_arg("--config"))
.action(ArgAction::SetTrue)
.global(true)
.hide(true))
.arg(multi_opt("config", "KEY=VALUE", "Override a configuration value").global(true))
// Better suggestion for the unsupported lowercase unstable feature flag.
.arg( Arg::new("unsupported-lowercase-unstable-feature-flag")
.help("")
.short('z')
.value_parser(UnknownArgumentValueParser::suggest_arg("-Z"))
.action(ArgAction::SetTrue)
.global(true)
.hide(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())
}
#[test]
fn verify_cli() {
let gctx = GlobalContext::default().unwrap();
cli(&gctx).debug_assert();
}
#[test]
fn avoid_ambiguity_between_builtins_and_manifest_commands() {
for cmd in commands::builtin() {
let name = cmd.get_name();
assert!(
!commands::run::is_manifest_command(&name),
"built-in command {name} is ambiguous with manifest-commands"
)
}
}