blob: a21030f0141f37061c2dc62f7856eaa98a5529ef [file] [log] [blame]
use anyhow::{anyhow, Context as _};
use cargo::core::shell::Shell;
use cargo::core::{features, CliUnstable};
use cargo::{self, drop_print, drop_println, CargoResult, CliResult, Config};
use clap::{builder::UnknownArgumentValueParser, 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 crate::util::is_rustup;
use cargo::core::features::HIDDEN;
use cargo::util::style;
pub fn main(config: &mut LazyConfig) -> CliResult {
let args = cli().try_get_matches()?;
// Update the process-level notion of cwd
// This must be completed before config is initialized
assert_eq!(config.is_init(), false);
if let Some(new_cwd) = args.get_one::<std::path::PathBuf>("directory") {
// This is a temporary hack. This cannot access `Config`, 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")?;
}
// 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") {
// 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!(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(());
}
};
let exec = Exec::infer(cmd)?;
config_configure(config, &expanded_args, subcommand_args, global_args, &exec)?;
super::init_git(config);
exec.exec(config, 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, sub_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) = 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(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(),
))?;
}
}
if commands::run::is_manifest_command(cmd) {
if config.cli_unstable().script {
return Ok((args, GlobalArgs::default()));
} else {
config.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().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,
exec: &Exec,
) -> CliResult {
let arg_target_dir = &subcommand_args.value_of_path("target-dir", config);
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.flag("quiet") || global_args.quiet;
if matches!(exec, 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 !config.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());
}
config.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()))
}
}
fn exec(self, config: &mut Config, subcommand_args: &ArgMatches) -> CliResult {
match self {
Self::Builtin(exec) => exec(config, subcommand_args),
Self::Manifest(cmd) => {
let ext_path = super::find_external_subcommand(config, &cmd);
if !config.cli_unstable().script && ext_path.is_some() {
config.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(config, subcommand_args)
} else {
let ext_args: Vec<OsString> = subcommand_args
.get_many::<OsString>("")
.unwrap_or_default()
.cloned()
.collect();
commands::run::exec_manifest_command(config, &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(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 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)
.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. Default location is $HOME/.cargo/bin
<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_quiet()
.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())
}
/// 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 }
}
/// Check whether the config is loaded
///
/// This is useful for asserts in case the environment needs to be setup before loading
pub fn is_init(&self) -> bool {
self.config.is_some()
}
/// 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();
}
#[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"
)
}
}