diff options
Diffstat (limited to '')
41 files changed, 4115 insertions, 0 deletions
diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs new file mode 100644 index 0000000..2d0107f --- /dev/null +++ b/src/bin/cargo/cli.rs @@ -0,0 +1,563 @@ +use anyhow::{anyhow, Context as _}; +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()?; + + // 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") { + 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(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 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(), + ))?; + } + } + + 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 + remove Remove dependencies from 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( + 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").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 } + } + + /// 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(); +} diff --git a/src/bin/cargo/commands/add.rs b/src/bin/cargo/commands/add.rs new file mode 100644 index 0000000..39f0e18 --- /dev/null +++ b/src/bin/cargo/commands/add.rs @@ -0,0 +1,362 @@ +use cargo::sources::CRATES_IO_REGISTRY; +use cargo::util::print_available_packages; +use indexmap::IndexMap; +use indexmap::IndexSet; + +use cargo::core::dependency::DepKind; +use cargo::core::FeatureValue; +use cargo::ops::cargo_add::add; +use cargo::ops::cargo_add::AddOptions; +use cargo::ops::cargo_add::DepOp; +use cargo::ops::resolve_ws; +use cargo::util::command_prelude::*; +use cargo::util::interning::InternedString; +use cargo::util::toml_mut::manifest::DepTable; +use cargo::CargoResult; + +pub fn cli() -> Command { + clap::Command::new("add") + .about("Add dependencies to a Cargo.toml manifest file") + .override_usage( + "\ + cargo add [OPTIONS] <DEP>[@<VERSION>] ... + cargo add [OPTIONS] --path <PATH> ... + cargo add [OPTIONS] --git <URL> ..." + ) + .after_help("Run `cargo help add` for more detailed information.\n") + .group(clap::ArgGroup::new("selected").multiple(true).required(true)) + .args([ + clap::Arg::new("crates") + .value_name("DEP_ID") + .num_args(0..) + .help("Reference to a package to add as a dependency") + .long_help( + "Reference to a package to add as a dependency + +You can reference a package by: +- `<name>`, like `cargo add serde` (latest version will be used) +- `<name>@<version-req>`, like `cargo add serde@1` or `cargo add serde@=1.0.38`" + ) + .group("selected"), + flag("no-default-features", + "Disable the default features"), + flag("default-features", + "Re-enable the default features") + .overrides_with("no-default-features"), + clap::Arg::new("features") + .short('F') + .long("features") + .value_name("FEATURES") + .action(ArgAction::Append) + .help("Space or comma separated list of features to activate"), + flag("optional", + "Mark the dependency as optional") + .long_help("Mark the dependency as optional + +The package name will be exposed as feature of your crate.") + .conflicts_with("dev"), + flag("no-optional", + "Mark the dependency as required") + .long_help("Mark the dependency as required + +The package will be removed from your features.") + .conflicts_with("dev") + .overrides_with("optional"), + clap::Arg::new("rename") + .long("rename") + .action(ArgAction::Set) + .value_name("NAME") + .help("Rename the dependency") + .long_help("Rename the dependency + +Example uses: +- Depending on multiple versions of a crate +- Depend on crates with the same name from different registries"), + ]) + .arg_manifest_path() + .arg_package("Package to modify") + .arg_quiet() + .arg_dry_run("Don't actually write the manifest") + .next_help_heading("Source") + .args([ + clap::Arg::new("path") + .long("path") + .action(ArgAction::Set) + .value_name("PATH") + .help("Filesystem path to local crate to add") + .group("selected") + .conflicts_with("git"), + clap::Arg::new("git") + .long("git") + .action(ArgAction::Set) + .value_name("URI") + .help("Git repository location") + .long_help("Git repository location + +Without any other information, cargo will use latest commit on the main branch.") + .group("selected"), + clap::Arg::new("branch") + .long("branch") + .action(ArgAction::Set) + .value_name("BRANCH") + .help("Git branch to download the crate from") + .requires("git") + .group("git-ref"), + clap::Arg::new("tag") + .long("tag") + .action(ArgAction::Set) + .value_name("TAG") + .help("Git tag to download the crate from") + .requires("git") + .group("git-ref"), + clap::Arg::new("rev") + .long("rev") + .action(ArgAction::Set) + .value_name("REV") + .help("Git reference to download the crate from") + .long_help("Git reference to download the crate from + +This is the catch all, handling hashes to named references in remote repositories.") + .requires("git") + .group("git-ref"), + clap::Arg::new("registry") + .long("registry") + .action(ArgAction::Set) + .value_name("NAME") + .help("Package registry for this dependency"), + ]) + .next_help_heading("Section") + .args([ + flag("dev", + "Add as development dependency") + .long_help("Add as development dependency + +Dev-dependencies are not used when compiling a package for building, but are used for compiling tests, examples, and benchmarks. + +These dependencies are not propagated to other packages which depend on this package.") + .group("section"), + flag("build", + "Add as build dependency") + .long_help("Add as build dependency + +Build-dependencies are the only dependencies available for use by build scripts (`build.rs` files).") + .group("section"), + clap::Arg::new("target") + .long("target") + .action(ArgAction::Set) + .value_name("TARGET") + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .help("Add as dependency to the given target platform") + ]) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let dry_run = args.dry_run(); + let section = parse_section(args); + + let ws = args.workspace(config)?; + + if args.is_present_with_zero_values("package") { + print_available_packages(&ws)?; + } + + let packages = args.packages_from_flags()?; + let packages = packages.get_packages(&ws)?; + let spec = match packages.len() { + 0 => { + return Err(CliError::new( + anyhow::format_err!( + "no packages selected to modify. Please specify one with `-p <PKGID>`" + ), + 101, + )); + } + 1 => packages[0], + _ => { + let names = packages.iter().map(|p| p.name()).collect::<Vec<_>>(); + return Err(CliError::new( + anyhow::format_err!( + "`cargo add` could not determine which package to modify. \ + Use the `--package` option to specify a package. \n\ + available packages: {}", + names.join(", ") + ), + 101, + )); + } + }; + + let dependencies = parse_dependencies(config, args)?; + + let options = AddOptions { + config, + spec, + dependencies, + section, + dry_run, + }; + add(&ws, &options)?; + + if !dry_run { + // Reload the workspace since we've changed dependencies + let ws = args.workspace(config)?; + resolve_ws(&ws)?; + } + + Ok(()) +} + +fn parse_dependencies(config: &Config, matches: &ArgMatches) -> CargoResult<Vec<DepOp>> { + let path = matches.get_one::<String>("path"); + let git = matches.get_one::<String>("git"); + let branch = matches.get_one::<String>("branch"); + let rev = matches.get_one::<String>("rev"); + let tag = matches.get_one::<String>("tag"); + let rename = matches.get_one::<String>("rename"); + let registry = match matches.registry(config)? { + Some(reg) if reg == CRATES_IO_REGISTRY => None, + reg => reg, + }; + let default_features = default_features(matches); + let optional = optional(matches); + + let mut crates = matches + .get_many::<String>("crates") + .into_iter() + .flatten() + .map(|c| (Some(c.clone()), None)) + .collect::<IndexMap<_, _>>(); + let mut infer_crate_name = false; + if crates.is_empty() { + if path.is_some() || git.is_some() { + crates.insert(None, None); + infer_crate_name = true; + } else { + unreachable!("clap should ensure we have some source selected"); + } + } + for feature in matches + .get_many::<String>("features") + .into_iter() + .flatten() + .map(String::as_str) + .flat_map(parse_feature) + { + let parsed_value = FeatureValue::new(InternedString::new(feature)); + match parsed_value { + FeatureValue::Feature(_) => { + if 1 < crates.len() { + let candidates = crates + .keys() + .map(|c| { + format!( + "`{}/{}`", + c.as_deref().expect("only none when there is 1"), + feature + ) + }) + .collect::<Vec<_>>(); + anyhow::bail!("feature `{feature}` must be qualified by the dependency it's being activated for, like {}", candidates.join(", ")); + } + crates + .first_mut() + .expect("always at least one crate") + .1 + .get_or_insert_with(IndexSet::new) + .insert(feature.to_owned()); + } + FeatureValue::Dep { .. } => { + anyhow::bail!("feature `{feature}` is not allowed to use explicit `dep:` syntax",) + } + FeatureValue::DepFeature { + dep_name, + dep_feature, + .. + } => { + if infer_crate_name { + anyhow::bail!("`{feature}` is unsupported when inferring the crate name, use `{dep_feature}`"); + } + if dep_feature.contains('/') { + anyhow::bail!("multiple slashes in feature `{feature}` is not allowed"); + } + crates.get_mut(&Some(dep_name.as_str().to_owned())).ok_or_else(|| { + anyhow::format_err!("feature `{dep_feature}` activated for crate `{dep_name}` but the crate wasn't specified") + })? + .get_or_insert_with(IndexSet::new) + .insert(dep_feature.as_str().to_owned()); + } + } + } + + let mut deps: Vec<DepOp> = Vec::new(); + for (crate_spec, features) in crates { + let dep = DepOp { + crate_spec, + rename: rename.map(String::from), + features, + default_features, + optional, + registry: registry.clone(), + path: path.map(String::from), + git: git.map(String::from), + branch: branch.map(String::from), + rev: rev.map(String::from), + tag: tag.map(String::from), + }; + deps.push(dep); + } + + if deps.len() > 1 && rename.is_some() { + anyhow::bail!("cannot specify multiple crates with `--rename`"); + } + + Ok(deps) +} + +fn default_features(matches: &ArgMatches) -> Option<bool> { + resolve_bool_arg( + matches.flag("default-features"), + matches.flag("no-default-features"), + ) +} + +fn optional(matches: &ArgMatches) -> Option<bool> { + resolve_bool_arg(matches.flag("optional"), matches.flag("no-optional")) +} + +fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> { + match (yes, no) { + (true, false) => Some(true), + (false, true) => Some(false), + (false, false) => None, + (_, _) => unreachable!("clap should make this impossible"), + } +} + +fn parse_section(matches: &ArgMatches) -> DepTable { + let kind = if matches.flag("dev") { + DepKind::Development + } else if matches.flag("build") { + DepKind::Build + } else { + DepKind::Normal + }; + + let mut table = DepTable::new().set_kind(kind); + + if let Some(target) = matches.get_one::<String>("target") { + assert!(!target.is_empty(), "Target specification may not be empty"); + table = table.set_target(target); + } + + table +} + +/// Split feature flag list +fn parse_feature(feature: &str) -> impl Iterator<Item = &str> { + // Not re-using `CliFeatures` because it uses a BTreeSet and loses user's ordering + feature + .split_whitespace() + .flat_map(|s| s.split(',')) + .filter(|s| !s.is_empty()) +} diff --git a/src/bin/cargo/commands/bench.rs b/src/bin/cargo/commands/bench.rs new file mode 100644 index 0000000..3739d88 --- /dev/null +++ b/src/bin/cargo/commands/bench.rs @@ -0,0 +1,77 @@ +use crate::command_prelude::*; +use cargo::ops::{self, TestOptions}; + +pub fn cli() -> Command { + subcommand("bench") + .about("Execute all benchmarks of a local package") + .arg_quiet() + .arg( + Arg::new("BENCHNAME") + .action(ArgAction::Set) + .help("If specified, only run benches containing this string in their names"), + ) + .arg( + Arg::new("args") + .help("Arguments for the bench binary") + .num_args(0..) + .last(true), + ) + .arg_targets_all( + "Benchmark only this package's library", + "Benchmark only the specified binary", + "Benchmark all binaries", + "Benchmark only the specified example", + "Benchmark all examples", + "Benchmark only the specified test target", + "Benchmark all tests", + "Benchmark only the specified bench target", + "Benchmark all benches", + "Benchmark all targets", + ) + .arg(flag("no-run", "Compile, but don't run benchmarks")) + .arg_package_spec( + "Package to run benchmarks for", + "Benchmark all packages in the workspace", + "Exclude packages from the benchmark", + ) + .arg_jobs() + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_ignore_rust_version() + .arg_message_format() + .arg(flag( + "no-fail-fast", + "Run all benchmarks regardless of failure", + )) + .arg_unit_graph() + .arg_timings() + .after_help("Run `cargo help bench` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let mut compile_opts = args.compile_options( + config, + CompileMode::Bench, + Some(&ws), + ProfileChecking::Custom, + )?; + + compile_opts.build_config.requested_profile = + args.get_profile_name(config, "bench", ProfileChecking::Custom)?; + + let ops = TestOptions { + no_run: args.flag("no-run"), + no_fail_fast: args.flag("no-fail-fast"), + compile_opts, + }; + + let bench_args = args.get_one::<String>("BENCHNAME").into_iter(); + let bench_args = bench_args.chain(args.get_many::<String>("args").unwrap_or_default()); + let bench_args = bench_args.map(String::as_str).collect::<Vec<_>>(); + + ops::run_benches(&ws, &ops, &bench_args) +} diff --git a/src/bin/cargo/commands/build.rs b/src/bin/cargo/commands/build.rs new file mode 100644 index 0000000..a78da38 --- /dev/null +++ b/src/bin/cargo/commands/build.rs @@ -0,0 +1,73 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("build") + // subcommand aliases are handled in aliased_command() + // .alias("b") + .about("Compile a local package and all of its dependencies") + .arg_quiet() + .arg_package_spec( + "Package to build (see `cargo help pkgid`)", + "Build all packages in the workspace", + "Exclude packages from the build", + ) + .arg_jobs() + .arg_targets_all( + "Build only this package's library", + "Build only the specified binary", + "Build all binaries", + "Build only the specified example", + "Build all examples", + "Build only the specified test target", + "Build all tests", + "Build only the specified bench target", + "Build all benches", + "Build all targets", + ) + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg( + opt( + "out-dir", + "Copy final artifacts to this directory (unstable)", + ) + .value_name("PATH"), + ) + .arg_manifest_path() + .arg_ignore_rust_version() + .arg_message_format() + .arg_build_plan() + .arg_unit_graph() + .arg_future_incompat_report() + .arg_timings() + .after_help("Run `cargo help build` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let mut compile_opts = args.compile_options( + config, + CompileMode::Build, + Some(&ws), + ProfileChecking::Custom, + )?; + + if let Some(out_dir) = args.value_of_path("out-dir", config) { + compile_opts.build_config.export_dir = Some(out_dir); + } else if let Some(out_dir) = config.build_config()?.out_dir.as_ref() { + let out_dir = out_dir.resolve_path(config); + compile_opts.build_config.export_dir = Some(out_dir); + } + if compile_opts.build_config.export_dir.is_some() { + config + .cli_unstable() + .fail_if_stable_opt("--out-dir", 6790)?; + } + ops::compile(&ws, &compile_opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/check.rs b/src/bin/cargo/commands/check.rs new file mode 100644 index 0000000..c9f6e0b --- /dev/null +++ b/src/bin/cargo/commands/check.rs @@ -0,0 +1,56 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("check") + // subcommand aliases are handled in aliased_command() + // .alias("c") + .about("Check a local package and all of its dependencies for errors") + .arg_quiet() + .arg_package_spec( + "Package(s) to check", + "Check all packages in the workspace", + "Exclude packages from the check", + ) + .arg_jobs() + .arg_targets_all( + "Check only this package's library", + "Check only the specified binary", + "Check all binaries", + "Check only the specified example", + "Check all examples", + "Check only the specified test target", + "Check all tests", + "Check only the specified bench target", + "Check all benches", + "Check all targets", + ) + .arg_release("Check artifacts in release mode, with optimizations") + .arg_profile("Check artifacts with the specified profile") + .arg_features() + .arg_target_triple("Check for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_ignore_rust_version() + .arg_message_format() + .arg_unit_graph() + .arg_future_incompat_report() + .arg_timings() + .after_help("Run `cargo help check` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + // This is a legacy behavior that causes `cargo check` to pass `--test`. + let test = matches!( + args.get_one::<String>("profile").map(String::as_str), + Some("test") + ); + let mode = CompileMode::Check { test }; + let compile_opts = + args.compile_options(config, mode, Some(&ws), ProfileChecking::LegacyTestOnly)?; + + ops::compile(&ws, &compile_opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs new file mode 100644 index 0000000..162461c --- /dev/null +++ b/src/bin/cargo/commands/clean.rs @@ -0,0 +1,37 @@ +use crate::command_prelude::*; + +use cargo::ops::{self, CleanOptions}; +use cargo::util::print_available_packages; + +pub fn cli() -> Command { + subcommand("clean") + .about("Remove artifacts that cargo has generated in the past") + .arg_quiet() + .arg_package_spec_simple("Package to clean artifacts for") + .arg_manifest_path() + .arg_target_triple("Target triple to clean output for") + .arg_target_dir() + .arg_release("Whether or not to clean release artifacts") + .arg_profile("Clean artifacts of the specified profile") + .arg_doc("Whether or not to clean just the documentation directory") + .after_help("Run `cargo help clean` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + + if args.is_present_with_zero_values("package") { + print_available_packages(&ws)?; + } + + let opts = CleanOptions { + config, + spec: values(args, "package"), + targets: args.targets(), + requested_profile: args.get_profile_name(config, "dev", ProfileChecking::Custom)?, + profile_specified: args.contains_id("profile") || args.flag("release"), + doc: args.flag("doc"), + }; + ops::clean(&ws, &opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/config.rs b/src/bin/cargo/commands/config.rs new file mode 100644 index 0000000..84c5e92 --- /dev/null +++ b/src/bin/cargo/commands/config.rs @@ -0,0 +1,55 @@ +use crate::command_prelude::*; +use cargo::ops::cargo_config; + +pub fn cli() -> Command { + subcommand("config") + .about("Inspect configuration values") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + subcommand("get") + .arg( + Arg::new("key") + .action(ArgAction::Set) + .help("The config key to display"), + ) + .arg( + opt("format", "Display format") + .value_parser(cargo_config::ConfigFormat::POSSIBLE_VALUES) + .default_value("toml"), + ) + .arg(flag( + "show-origin", + "Display where the config value is defined", + )) + .arg( + opt("merged", "Whether or not to merge config values") + .value_parser(["yes", "no"]) + .default_value("yes"), + ), + ) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + config + .cli_unstable() + .fail_if_stable_command(config, "config", 9301)?; + match args.subcommand() { + Some(("get", args)) => { + let opts = cargo_config::GetOptions { + key: args.get_one::<String>("key").map(String::as_str), + format: args.get_one::<String>("format").unwrap().parse()?, + show_origin: args.flag("show-origin"), + merged: args.get_one::<String>("merged").map(String::as_str) == Some("yes"), + }; + cargo_config::get(config, &opts)?; + } + Some((cmd, _)) => { + unreachable!("unexpected command {}", cmd) + } + None => { + unreachable!("unexpected command") + } + } + Ok(()) +} diff --git a/src/bin/cargo/commands/doc.rs b/src/bin/cargo/commands/doc.rs new file mode 100644 index 0000000..932058a --- /dev/null +++ b/src/bin/cargo/commands/doc.rs @@ -0,0 +1,61 @@ +use crate::command_prelude::*; + +use cargo::ops::{self, DocOptions}; + +pub fn cli() -> Command { + subcommand("doc") + // subcommand aliases are handled in aliased_command() + // .alias("d") + .about("Build a package's documentation") + .arg_quiet() + .arg(flag( + "open", + "Opens the docs in a browser after the operation", + )) + .arg_package_spec( + "Package to document", + "Document all packages in the workspace", + "Exclude packages from the build", + ) + .arg(flag( + "no-deps", + "Don't build documentation for dependencies", + )) + .arg(flag("document-private-items", "Document private items")) + .arg_jobs() + .arg_targets_lib_bin_example( + "Document only this package's library", + "Document only the specified binary", + "Document all binaries", + "Document only the specified example", + "Document all examples", + ) + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_message_format() + .arg_ignore_rust_version() + .arg_unit_graph() + .arg_timings() + .after_help("Run `cargo help doc` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let mode = CompileMode::Doc { + deps: !args.flag("no-deps"), + }; + let mut compile_opts = + args.compile_options(config, mode, Some(&ws), ProfileChecking::Custom)?; + compile_opts.rustdoc_document_private_items = args.flag("document-private-items"); + + let doc_opts = DocOptions { + open_result: args.flag("open"), + compile_opts, + }; + ops::doc(&ws, &doc_opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/fetch.rs b/src/bin/cargo/commands/fetch.rs new file mode 100644 index 0000000..2fbbc47 --- /dev/null +++ b/src/bin/cargo/commands/fetch.rs @@ -0,0 +1,24 @@ +use crate::command_prelude::*; + +use cargo::ops; +use cargo::ops::FetchOptions; + +pub fn cli() -> Command { + subcommand("fetch") + .about("Fetch dependencies of a package from the network") + .arg_quiet() + .arg_manifest_path() + .arg_target_triple("Fetch dependencies for the target triple") + .after_help("Run `cargo help fetch` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + + let opts = FetchOptions { + config, + targets: args.targets(), + }; + let _ = ops::fetch(&ws, &opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/fix.rs b/src/bin/cargo/commands/fix.rs new file mode 100644 index 0000000..5238d58 --- /dev/null +++ b/src/bin/cargo/commands/fix.rs @@ -0,0 +1,92 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("fix") + .about("Automatically fix lint warnings reported by rustc") + .arg_quiet() + .arg_package_spec( + "Package(s) to fix", + "Fix all packages in the workspace", + "Exclude packages from the fixes", + ) + .arg_jobs() + .arg_targets_all( + "Fix only this package's library", + "Fix only the specified binary", + "Fix all binaries", + "Fix only the specified example", + "Fix all examples", + "Fix only the specified test target", + "Fix all tests", + "Fix only the specified bench target", + "Fix all benches", + "Fix all targets (default)", + ) + .arg_release("Fix artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Fix for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_message_format() + .arg(flag( + "broken-code", + "Fix code even if it already has compiler errors", + )) + .arg(flag("edition", "Fix in preparation for the next edition")) + .arg(flag( + "edition-idioms", + "Fix warnings to migrate to the idioms of an edition", + )) + .arg(flag( + "allow-no-vcs", + "Fix code even if a VCS was not detected", + )) + .arg(flag( + "allow-dirty", + "Fix code even if the working directory is dirty", + )) + .arg(flag( + "allow-staged", + "Fix code even if the working directory has staged changes", + )) + .arg_ignore_rust_version() + .arg_timings() + .after_help("Run `cargo help fix` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + // This is a legacy behavior that causes `cargo fix` to pass `--test`. + let test = matches!( + args.get_one::<String>("profile").map(String::as_str), + Some("test") + ); + let mode = CompileMode::Check { test }; + + // Unlike other commands default `cargo fix` to all targets to fix as much + // code as we can. + let mut opts = + args.compile_options(config, mode, Some(&ws), ProfileChecking::LegacyTestOnly)?; + + if !opts.filter.is_specific() { + // cargo fix with no target selection implies `--all-targets`. + opts.filter = ops::CompileFilter::new_all_targets(); + } + + ops::fix( + &ws, + &mut ops::FixOptions { + edition: args.flag("edition"), + idioms: args.flag("edition-idioms"), + compile_opts: opts, + allow_dirty: args.flag("allow-dirty"), + allow_no_vcs: args.flag("allow-no-vcs"), + allow_staged: args.flag("allow-staged"), + broken_code: args.flag("broken-code"), + }, + )?; + Ok(()) +} diff --git a/src/bin/cargo/commands/generate_lockfile.rs b/src/bin/cargo/commands/generate_lockfile.rs new file mode 100644 index 0000000..7d06aad --- /dev/null +++ b/src/bin/cargo/commands/generate_lockfile.rs @@ -0,0 +1,17 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("generate-lockfile") + .about("Generate the lockfile for a package") + .arg_quiet() + .arg_manifest_path() + .after_help("Run `cargo help generate-lockfile` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + ops::generate_lockfile(&ws)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/git_checkout.rs b/src/bin/cargo/commands/git_checkout.rs new file mode 100644 index 0000000..90be9bc --- /dev/null +++ b/src/bin/cargo/commands/git_checkout.rs @@ -0,0 +1,14 @@ +use crate::command_prelude::*; + +const REMOVED: &str = "The `git-checkout` command has been removed."; + +pub fn cli() -> Command { + subcommand("git-checkout") + .about("This command has been removed") + .hide(true) + .override_help(REMOVED) +} + +pub fn exec(_config: &mut Config, _args: &ArgMatches) -> CliResult { + Err(anyhow::format_err!(REMOVED).into()) +} diff --git a/src/bin/cargo/commands/help.rs b/src/bin/cargo/commands/help.rs new file mode 100644 index 0000000..2839b93 --- /dev/null +++ b/src/bin/cargo/commands/help.rs @@ -0,0 +1,147 @@ +use crate::aliased_command; +use crate::command_prelude::*; +use cargo::util::errors::CargoResult; +use cargo::{drop_println, Config}; +use cargo_util::paths::resolve_executable; +use flate2::read::GzDecoder; +use std::ffi::OsStr; +use std::ffi::OsString; +use std::io::Read; +use std::io::Write; +use std::path::Path; + +const COMPRESSED_MAN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/man.tgz")); + +pub fn cli() -> Command { + subcommand("help") + .about("Displays help for a cargo subcommand") + .arg(Arg::new("COMMAND").action(ArgAction::Set)) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let subcommand = args.get_one::<String>("COMMAND"); + if let Some(subcommand) = subcommand { + if !try_help(config, subcommand)? { + match check_builtin(&subcommand) { + Some(s) => { + crate::execute_internal_subcommand( + config, + &[OsStr::new(s), OsStr::new("--help")], + )?; + } + None => { + crate::execute_external_subcommand( + config, + subcommand, + &[OsStr::new(subcommand), OsStr::new("--help")], + )?; + } + } + } + } else { + let mut cmd = crate::cli::cli(); + let _ = cmd.print_help(); + } + Ok(()) +} + +fn try_help(config: &Config, subcommand: &str) -> CargoResult<bool> { + let subcommand = match check_alias(config, subcommand) { + // If this alias is more than a simple subcommand pass-through, show the alias. + Some(argv) if argv.len() > 1 => { + let alias = argv.join(" "); + drop_println!(config, "`{}` is aliased to `{}`", subcommand, alias); + return Ok(true); + } + // Otherwise, resolve the alias into its subcommand. + Some(argv) => { + // An alias with an empty argv can be created via `"empty-alias" = ""`. + let first = argv.get(0).map(String::as_str).unwrap_or(subcommand); + first.to_string() + } + None => subcommand.to_string(), + }; + + let subcommand = match check_builtin(&subcommand) { + Some(s) => s, + None => return Ok(false), + }; + + if resolve_executable(Path::new("man")).is_ok() { + let man = match extract_man(subcommand, "1") { + Some(man) => man, + None => return Ok(false), + }; + write_and_spawn(subcommand, &man, "man")?; + } else { + let txt = match extract_man(subcommand, "txt") { + Some(txt) => txt, + None => return Ok(false), + }; + if resolve_executable(Path::new("less")).is_ok() { + write_and_spawn(subcommand, &txt, "less")?; + } else if resolve_executable(Path::new("more")).is_ok() { + write_and_spawn(subcommand, &txt, "more")?; + } else { + drop(std::io::stdout().write_all(&txt)); + } + } + Ok(true) +} + +/// Checks if the given subcommand is an alias. +/// +/// Returns None if it is not an alias. +fn check_alias(config: &Config, subcommand: &str) -> Option<Vec<String>> { + aliased_command(config, subcommand).ok().flatten() +} + +/// Checks if the given subcommand is a built-in command (not via an alias). +/// +/// Returns None if it is not a built-in command. +fn check_builtin(subcommand: &str) -> Option<&str> { + super::builtin_exec(subcommand).map(|_| subcommand) +} + +/// Extracts the given man page from the compressed archive. +/// +/// Returns None if the command wasn't found. +fn extract_man(subcommand: &str, extension: &str) -> Option<Vec<u8>> { + let extract_name = OsString::from(format!("cargo-{}.{}", subcommand, extension)); + let gz = GzDecoder::new(COMPRESSED_MAN); + let mut ar = tar::Archive::new(gz); + // Unwraps should be safe here, since this is a static archive generated + // by our build script. It should never be an invalid format! + for entry in ar.entries().unwrap() { + let mut entry = entry.unwrap(); + let path = entry.path().unwrap(); + if path.file_name().unwrap() != extract_name { + continue; + } + let mut result = Vec::new(); + entry.read_to_end(&mut result).unwrap(); + return Some(result); + } + None +} + +/// Write the contents of a man page to disk and spawn the given command to +/// display it. +fn write_and_spawn(name: &str, contents: &[u8], command: &str) -> CargoResult<()> { + let prefix = format!("cargo-{}.", name); + let mut tmp = tempfile::Builder::new().prefix(&prefix).tempfile()?; + let f = tmp.as_file_mut(); + f.write_all(contents)?; + f.flush()?; + let path = tmp.path(); + // Use a path relative to the temp directory so that it can work on + // cygwin/msys systems which don't handle windows-style paths. + let mut relative_name = std::ffi::OsString::from("./"); + relative_name.push(path.file_name().unwrap()); + let mut cmd = std::process::Command::new(command) + .arg(relative_name) + .current_dir(path.parent().unwrap()) + .spawn()?; + drop(cmd.wait()); + Ok(()) +} diff --git a/src/bin/cargo/commands/init.rs b/src/bin/cargo/commands/init.rs new file mode 100644 index 0000000..b280d4f --- /dev/null +++ b/src/bin/cargo/commands/init.rs @@ -0,0 +1,22 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("init") + .about("Create a new cargo package in an existing directory") + .arg_quiet() + .arg(Arg::new("path").action(ArgAction::Set).default_value(".")) + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .arg_new_opts() + .after_help("Run `cargo help init` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let opts = args.new_options(config)?; + let project_kind = ops::init(&opts, config)?; + config + .shell() + .status("Created", format!("{} package", project_kind))?; + Ok(()) +} diff --git a/src/bin/cargo/commands/install.rs b/src/bin/cargo/commands/install.rs new file mode 100644 index 0000000..790bfd2 --- /dev/null +++ b/src/bin/cargo/commands/install.rs @@ -0,0 +1,197 @@ +use crate::command_prelude::*; + +use cargo::core::{GitReference, SourceId, Workspace}; +use cargo::ops; +use cargo::util::IntoUrl; + +use cargo_util::paths; + +pub fn cli() -> Command { + subcommand("install") + .about("Install a Rust binary. Default location is $HOME/.cargo/bin") + .arg_quiet() + .arg( + Arg::new("crate") + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .num_args(0..), + ) + .arg( + opt("version", "Specify a version to install") + .alias("vers") + .value_name("VERSION") + .requires("crate"), + ) + .arg( + opt("git", "Git URL to install the specified crate from") + .value_name("URL") + .conflicts_with_all(&["path", "index", "registry"]), + ) + .arg( + opt("branch", "Branch to use when installing from git") + .value_name("BRANCH") + .requires("git"), + ) + .arg( + opt("tag", "Tag to use when installing from git") + .value_name("TAG") + .requires("git"), + ) + .arg( + opt("rev", "Specific commit to use when installing from git") + .value_name("SHA") + .requires("git"), + ) + .arg( + opt("path", "Filesystem path to local crate to install") + .value_name("PATH") + .conflicts_with_all(&["git", "index", "registry"]), + ) + .arg(flag( + "list", + "list all installed packages and their versions", + )) + .arg_jobs() + .arg(flag("force", "Force overwriting existing crates or binaries").short('f')) + .arg(flag("no-track", "Do not save tracking information")) + .arg_features() + .arg_profile("Install artifacts with the specified profile") + .arg(flag( + "debug", + "Build in debug mode (with the 'dev' profile) instead of release mode", + )) + .arg_targets_bins_examples( + "Install only the specified binary", + "Install all binaries", + "Install only the specified example", + "Install all examples", + ) + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg(opt("root", "Directory to install packages into").value_name("DIR")) + .arg( + opt("index", "Registry index to install from") + .value_name("INDEX") + .requires("crate") + .conflicts_with_all(&["git", "path", "registry"]), + ) + .arg( + opt("registry", "Registry to use") + .value_name("REGISTRY") + .requires("crate") + .conflicts_with_all(&["git", "path", "index"]), + ) + .arg_message_format() + .arg_timings() + .after_help("Run `cargo help install` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let path = args.value_of_path("path", config); + if let Some(path) = &path { + config.reload_rooted_at(path)?; + } else { + // TODO: Consider calling set_search_stop_path(home). + config.reload_rooted_at(config.home().clone().into_path_unlocked())?; + } + + // In general, we try to avoid normalizing paths in Cargo, + // but in these particular cases we need it to fix rust-lang/cargo#10283. + // (Handle `SourceId::for_path` and `Workspace::new`, + // but not `Config::reload_rooted_at` which is always cwd) + let path = path.map(|p| paths::normalize_path(&p)); + + let version = args.get_one::<String>("version").map(String::as_str); + let krates = args + .get_many::<String>("crate") + .unwrap_or_default() + .map(|k| resolve_crate(k, version)) + .collect::<crate::CargoResult<Vec<_>>>()?; + + let mut from_cwd = false; + + let source = if let Some(url) = args.get_one::<String>("git") { + let url = url.into_url()?; + let gitref = if let Some(branch) = args.get_one::<String>("branch") { + GitReference::Branch(branch.clone()) + } else if let Some(tag) = args.get_one::<String>("tag") { + GitReference::Tag(tag.clone()) + } else if let Some(rev) = args.get_one::<String>("rev") { + GitReference::Rev(rev.clone()) + } else { + GitReference::DefaultBranch + }; + SourceId::for_git(&url, gitref)? + } else if let Some(path) = &path { + SourceId::for_path(path)? + } else if krates.is_empty() { + from_cwd = true; + SourceId::for_path(config.cwd())? + } else if let Some(index) = args.get_one::<String>("index") { + SourceId::for_registry(&index.into_url()?)? + } else if let Some(registry) = args.registry(config)? { + SourceId::alt_registry(config, ®istry)? + } else { + SourceId::crates_io(config)? + }; + + let root = args.get_one::<String>("root").map(String::as_str); + + // We only provide workspace information for local crate installation from + // one of the following sources: + // - From current working directory (only work for edition 2015). + // - From a specific local file path (from `--path` arg). + // + // This workspace information is for emitting helpful messages from + // `ArgMatchesExt::compile_options` and won't affect the actual compilation. + let workspace = if from_cwd { + args.workspace(config).ok() + } else if let Some(path) = &path { + Workspace::new(&path.join("Cargo.toml"), config).ok() + } else { + None + }; + + let mut compile_opts = args.compile_options( + config, + CompileMode::Build, + workspace.as_ref(), + ProfileChecking::Custom, + )?; + + compile_opts.build_config.requested_profile = + args.get_profile_name(config, "release", ProfileChecking::Custom)?; + + if args.flag("list") { + ops::install_list(root, config)?; + } else { + ops::install( + config, + root, + krates, + source, + from_cwd, + &compile_opts, + args.flag("force"), + args.flag("no-track"), + )?; + } + Ok(()) +} + +fn resolve_crate<'k>( + mut krate: &'k str, + mut version: Option<&'k str>, +) -> crate::CargoResult<(&'k str, Option<&'k str>)> { + if let Some((k, v)) = krate.split_once('@') { + if version.is_some() { + anyhow::bail!("cannot specify both `@{v}` and `--version`"); + } + if k.is_empty() { + // by convention, arguments starting with `@` are response files + anyhow::bail!("missing crate name for `@{v}`"); + } + krate = k; + version = Some(v); + } + Ok((krate, version)) +} diff --git a/src/bin/cargo/commands/locate_project.rs b/src/bin/cargo/commands/locate_project.rs new file mode 100644 index 0000000..26c35cd --- /dev/null +++ b/src/bin/cargo/commands/locate_project.rs @@ -0,0 +1,93 @@ +use crate::command_prelude::*; +use anyhow::bail; +use cargo::{drop_println, CargoResult}; +use serde::Serialize; + +pub fn cli() -> Command { + subcommand("locate-project") + .about("Print a JSON representation of a Cargo.toml file's location") + .arg_quiet() + .arg_manifest_path() + .arg( + opt( + "message-format", + "Output representation [possible values: json, plain]", + ) + .value_name("FMT"), + ) + .arg(flag("workspace", "Locate Cargo.toml of the workspace root")) + .after_help("Run `cargo help locate-project` for more detailed information.\n") +} + +#[derive(Serialize)] +pub struct ProjectLocation<'a> { + root: &'a str, +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let root_manifest; + let workspace; + let root = match WhatToFind::parse(args) { + WhatToFind::CurrentManifest => { + root_manifest = args.root_manifest(config)?; + &root_manifest + } + WhatToFind::Workspace => { + workspace = args.workspace(config)?; + workspace.root_manifest() + } + }; + + let root = root + .to_str() + .ok_or_else(|| { + anyhow::format_err!( + "your package path contains characters \ + not representable in Unicode" + ) + }) + .map_err(|e| CliError::new(e, 1))?; + + let location = ProjectLocation { root }; + + match MessageFormat::parse(args)? { + MessageFormat::Json => config.shell().print_json(&location)?, + MessageFormat::Plain => drop_println!(config, "{}", location.root), + } + + Ok(()) +} + +enum WhatToFind { + CurrentManifest, + Workspace, +} + +impl WhatToFind { + fn parse(args: &ArgMatches) -> Self { + if args.flag("workspace") { + WhatToFind::Workspace + } else { + WhatToFind::CurrentManifest + } + } +} + +enum MessageFormat { + Json, + Plain, +} + +impl MessageFormat { + fn parse(args: &ArgMatches) -> CargoResult<Self> { + let fmt = match args.get_one::<String>("message-format") { + Some(fmt) => fmt, + None => return Ok(MessageFormat::Json), + }; + match fmt.to_ascii_lowercase().as_str() { + "json" => Ok(MessageFormat::Json), + "plain" => Ok(MessageFormat::Plain), + s => bail!("invalid message format specifier: `{}`", s), + } + } +} diff --git a/src/bin/cargo/commands/login.rs b/src/bin/cargo/commands/login.rs new file mode 100644 index 0000000..dac0457 --- /dev/null +++ b/src/bin/cargo/commands/login.rs @@ -0,0 +1,46 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("login") + .about( + "Save an api token from the registry locally. \ + If token is not specified, it will be read from stdin.", + ) + .arg_quiet() + .arg(Arg::new("token").action(ArgAction::Set)) + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .arg( + flag( + "generate-keypair", + "Generate a public/secret keypair (unstable)", + ) + .conflicts_with("token"), + ) + .arg( + flag("secret-key", "Prompt for secret key (unstable)") + .conflicts_with_all(&["generate-keypair", "token"]), + ) + .arg( + opt( + "key-subject", + "Set the key subject for this registry (unstable)", + ) + .value_name("SUBJECT") + .conflicts_with("token"), + ) + .after_help("Run `cargo help login` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + ops::registry_login( + config, + args.get_one::<String>("token").map(|s| s.as_str().into()), + args.get_one("registry").map(String::as_str), + args.flag("generate-keypair"), + args.flag("secret-key"), + args.get_one("key-subject").map(String::as_str), + )?; + Ok(()) +} diff --git a/src/bin/cargo/commands/logout.rs b/src/bin/cargo/commands/logout.rs new file mode 100644 index 0000000..bc16ee5 --- /dev/null +++ b/src/bin/cargo/commands/logout.rs @@ -0,0 +1,23 @@ +use crate::command_prelude::*; +use cargo::ops; + +pub fn cli() -> Command { + subcommand("logout") + .about("Remove an API token from the registry locally") + .arg_quiet() + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .after_help("Run `cargo help logout` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + if !config.cli_unstable().credential_process { + config + .cli_unstable() + .fail_if_stable_command(config, "logout", 8933)?; + } + ops::registry_logout( + config, + args.get_one::<String>("registry").map(String::as_str), + )?; + Ok(()) +} diff --git a/src/bin/cargo/commands/metadata.rs b/src/bin/cargo/commands/metadata.rs new file mode 100644 index 0000000..fdf5965 --- /dev/null +++ b/src/bin/cargo/commands/metadata.rs @@ -0,0 +1,56 @@ +use crate::command_prelude::*; +use cargo::ops::{self, OutputMetadataOptions}; + +pub fn cli() -> Command { + subcommand("metadata") + .about( + "Output the resolved dependencies of a package, \ + the concrete used versions including overrides, \ + in machine-readable format", + ) + .arg_quiet() + .arg_features() + .arg(multi_opt( + "filter-platform", + "TRIPLE", + "Only include resolve dependencies matching the given target-triple", + )) + .arg(flag( + "no-deps", + "Output information only about the workspace members \ + and don't fetch dependencies", + )) + .arg_manifest_path() + .arg( + opt("format-version", "Format version") + .value_name("VERSION") + .value_parser(["1"]), + ) + .after_help("Run `cargo help metadata` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + + let version = match args.get_one::<String>("format-version") { + None => { + config.shell().warn( + "please specify `--format-version` flag explicitly \ + to avoid compatibility problems", + )?; + 1 + } + Some(version) => version.parse().unwrap(), + }; + + let options = OutputMetadataOptions { + cli_features: args.cli_features()?, + no_deps: args.flag("no-deps"), + filter_platforms: args._values_of("filter-platform"), + version, + }; + + let result = ops::output_metadata(&ws, &options)?; + config.shell().print_json(&result)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/mod.rs b/src/bin/cargo/commands/mod.rs new file mode 100644 index 0000000..da31092 --- /dev/null +++ b/src/bin/cargo/commands/mod.rs @@ -0,0 +1,128 @@ +use crate::command_prelude::*; + +pub fn builtin() -> Vec<Command> { + vec![ + add::cli(), + bench::cli(), + build::cli(), + check::cli(), + clean::cli(), + config::cli(), + doc::cli(), + fetch::cli(), + fix::cli(), + generate_lockfile::cli(), + git_checkout::cli(), + help::cli(), + init::cli(), + install::cli(), + locate_project::cli(), + login::cli(), + logout::cli(), + metadata::cli(), + new::cli(), + owner::cli(), + package::cli(), + pkgid::cli(), + publish::cli(), + read_manifest::cli(), + remove::cli(), + report::cli(), + run::cli(), + rustc::cli(), + rustdoc::cli(), + search::cli(), + test::cli(), + tree::cli(), + uninstall::cli(), + update::cli(), + vendor::cli(), + verify_project::cli(), + version::cli(), + yank::cli(), + ] +} + +pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> CliResult> { + let f = match cmd { + "add" => add::exec, + "bench" => bench::exec, + "build" => build::exec, + "check" => check::exec, + "clean" => clean::exec, + "config" => config::exec, + "doc" => doc::exec, + "fetch" => fetch::exec, + "fix" => fix::exec, + "generate-lockfile" => generate_lockfile::exec, + "git-checkout" => git_checkout::exec, + "help" => help::exec, + "init" => init::exec, + "install" => install::exec, + "locate-project" => locate_project::exec, + "login" => login::exec, + "logout" => logout::exec, + "metadata" => metadata::exec, + "new" => new::exec, + "owner" => owner::exec, + "package" => package::exec, + "pkgid" => pkgid::exec, + "publish" => publish::exec, + "read-manifest" => read_manifest::exec, + "remove" => remove::exec, + "report" => report::exec, + "run" => run::exec, + "rustc" => rustc::exec, + "rustdoc" => rustdoc::exec, + "search" => search::exec, + "test" => test::exec, + "tree" => tree::exec, + "uninstall" => uninstall::exec, + "update" => update::exec, + "vendor" => vendor::exec, + "verify-project" => verify_project::exec, + "version" => version::exec, + "yank" => yank::exec, + _ => return None, + }; + Some(f) +} + +pub mod add; +pub mod bench; +pub mod build; +pub mod check; +pub mod clean; +pub mod config; +pub mod doc; +pub mod fetch; +pub mod fix; +pub mod generate_lockfile; +pub mod git_checkout; +pub mod help; +pub mod init; +pub mod install; +pub mod locate_project; +pub mod login; +pub mod logout; +pub mod metadata; +pub mod new; +pub mod owner; +pub mod package; +pub mod pkgid; +pub mod publish; +pub mod read_manifest; +pub mod remove; +pub mod report; +pub mod run; +pub mod rustc; +pub mod rustdoc; +pub mod search; +pub mod test; +pub mod tree; +pub mod uninstall; +pub mod update; +pub mod vendor; +pub mod verify_project; +pub mod version; +pub mod yank; diff --git a/src/bin/cargo/commands/new.rs b/src/bin/cargo/commands/new.rs new file mode 100644 index 0000000..18cf93d --- /dev/null +++ b/src/bin/cargo/commands/new.rs @@ -0,0 +1,30 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("new") + .about("Create a new cargo package at <path>") + .arg_quiet() + .arg(Arg::new("path").action(ArgAction::Set).required(true)) + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .arg_new_opts() + .after_help("Run `cargo help new` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let opts = args.new_options(config)?; + + ops::new(&opts, config)?; + let path = args.get_one::<String>("path").unwrap(); + let package_name = if let Some(name) = args.get_one::<String>("name") { + name + } else { + path + }; + config.shell().status( + "Created", + format!("{} `{}` package", opts.kind, package_name), + )?; + Ok(()) +} diff --git a/src/bin/cargo/commands/owner.rs b/src/bin/cargo/commands/owner.rs new file mode 100644 index 0000000..493072b --- /dev/null +++ b/src/bin/cargo/commands/owner.rs @@ -0,0 +1,51 @@ +use crate::command_prelude::*; + +use cargo::ops::{self, OwnersOptions}; +use cargo::util::auth::Secret; + +pub fn cli() -> Command { + subcommand("owner") + .about("Manage the owners of a crate on the registry") + .arg_quiet() + .arg(Arg::new("crate").action(ArgAction::Set)) + .arg( + multi_opt( + "add", + "LOGIN", + "Name of a user or team to invite as an owner", + ) + .short('a'), + ) + .arg( + multi_opt( + "remove", + "LOGIN", + "Name of a user or team to remove as an owner", + ) + .short('r'), + ) + .arg(flag("list", "List owners of a crate").short('l')) + .arg(opt("index", "Registry index to modify owners for").value_name("INDEX")) + .arg(opt("token", "API token to use when authenticating").value_name("TOKEN")) + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .after_help("Run `cargo help owner` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let registry = args.registry(config)?; + let opts = OwnersOptions { + krate: args.get_one::<String>("crate").cloned(), + token: args.get_one::<String>("token").cloned().map(Secret::from), + index: args.get_one::<String>("index").cloned(), + to_add: args + .get_many::<String>("add") + .map(|xs| xs.cloned().collect()), + to_remove: args + .get_many::<String>("remove") + .map(|xs| xs.cloned().collect()), + list: args.flag("list"), + registry, + }; + ops::modify_owners(config, &opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/package.rs b/src/bin/cargo/commands/package.rs new file mode 100644 index 0000000..ac6b1fe --- /dev/null +++ b/src/bin/cargo/commands/package.rs @@ -0,0 +1,62 @@ +use crate::command_prelude::*; + +use cargo::ops::{self, PackageOpts}; + +pub fn cli() -> Command { + subcommand("package") + .about("Assemble the local package into a distributable tarball") + .arg_quiet() + .arg( + flag( + "list", + "Print files included in a package without making one", + ) + .short('l'), + ) + .arg(flag( + "no-verify", + "Don't verify the contents by building them", + )) + .arg(flag( + "no-metadata", + "Ignore warnings about a lack of human-usable metadata", + )) + .arg(flag( + "allow-dirty", + "Allow dirty working directories to be packaged", + )) + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_features() + .arg_package_spec_no_all( + "Package(s) to assemble", + "Assemble all packages in the workspace", + "Don't assemble specified packages", + ) + .arg_manifest_path() + .arg_jobs() + .after_help("Run `cargo help package` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let specs = args.packages_from_flags()?; + + ops::package( + &ws, + &PackageOpts { + config, + verify: !args.flag("no-verify"), + list: args.flag("list"), + check_metadata: !args.flag("no-metadata"), + allow_dirty: args.flag("allow-dirty"), + to_package: specs, + targets: args.targets(), + jobs: args.jobs()?, + keep_going: args.keep_going(), + cli_features: args.cli_features()?, + }, + )?; + + Ok(()) +} diff --git a/src/bin/cargo/commands/pkgid.rs b/src/bin/cargo/commands/pkgid.rs new file mode 100644 index 0000000..664db75 --- /dev/null +++ b/src/bin/cargo/commands/pkgid.rs @@ -0,0 +1,28 @@ +use crate::command_prelude::*; + +use cargo::ops; +use cargo::util::print_available_packages; + +pub fn cli() -> Command { + subcommand("pkgid") + .about("Print a fully qualified package specification") + .arg_quiet() + .arg(Arg::new("spec").action(ArgAction::Set)) + .arg_package("Argument to get the package ID specifier for") + .arg_manifest_path() + .after_help("Run `cargo help pkgid` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + if args.is_present_with_zero_values("package") { + print_available_packages(&ws)? + } + let spec = args + .get_one::<String>("spec") + .or_else(|| args.get_one::<String>("package")) + .map(String::as_str); + let spec = ops::pkgid(&ws, spec)?; + cargo::drop_println!(config, "{}", spec); + Ok(()) +} diff --git a/src/bin/cargo/commands/publish.rs b/src/bin/cargo/commands/publish.rs new file mode 100644 index 0000000..c831d39 --- /dev/null +++ b/src/bin/cargo/commands/publish.rs @@ -0,0 +1,55 @@ +use crate::command_prelude::*; + +use cargo::ops::{self, PublishOpts}; + +pub fn cli() -> Command { + subcommand("publish") + .about("Upload a package to the registry") + .arg_quiet() + .arg_index() + .arg(opt("token", "Token to use when uploading").value_name("TOKEN")) + .arg(flag( + "no-verify", + "Don't verify the contents by building them", + )) + .arg(flag( + "allow-dirty", + "Allow dirty working directories to be packaged", + )) + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_package("Package to publish") + .arg_manifest_path() + .arg_features() + .arg_jobs() + .arg_dry_run("Perform all checks without uploading") + .arg(opt("registry", "Registry to publish to").value_name("REGISTRY")) + .after_help("Run `cargo help publish` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let registry = args.registry(config)?; + let ws = args.workspace(config)?; + let index = args.index()?; + + ops::publish( + &ws, + &PublishOpts { + config, + token: args + .get_one::<String>("token") + .map(|s| s.to_string().into()), + index, + verify: !args.flag("no-verify"), + allow_dirty: args.flag("allow-dirty"), + to_publish: args.packages_from_flags()?, + targets: args.targets(), + jobs: args.jobs()?, + keep_going: args.keep_going(), + dry_run: args.dry_run(), + registry, + cli_features: args.cli_features()?, + }, + )?; + Ok(()) +} diff --git a/src/bin/cargo/commands/read_manifest.rs b/src/bin/cargo/commands/read_manifest.rs new file mode 100644 index 0000000..a1f42bf --- /dev/null +++ b/src/bin/cargo/commands/read_manifest.rs @@ -0,0 +1,20 @@ +use crate::command_prelude::*; + +pub fn cli() -> Command { + subcommand("read-manifest") + .about( + "\ +Print a JSON representation of a Cargo.toml manifest. + +Deprecated, use `cargo metadata --no-deps` instead.\ +", + ) + .arg_quiet() + .arg_manifest_path() +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + config.shell().print_json(&ws.current()?.serialized())?; + Ok(()) +} diff --git a/src/bin/cargo/commands/remove.rs b/src/bin/cargo/commands/remove.rs new file mode 100644 index 0000000..50bc8b7 --- /dev/null +++ b/src/bin/cargo/commands/remove.rs @@ -0,0 +1,344 @@ +use cargo::core::dependency::DepKind; +use cargo::core::PackageIdSpec; +use cargo::core::Workspace; +use cargo::ops::cargo_remove::remove; +use cargo::ops::cargo_remove::RemoveOptions; +use cargo::ops::resolve_ws; +use cargo::util::command_prelude::*; +use cargo::util::print_available_packages; +use cargo::util::toml_mut::dependency::Dependency; +use cargo::util::toml_mut::dependency::MaybeWorkspace; +use cargo::util::toml_mut::dependency::Source; +use cargo::util::toml_mut::manifest::DepTable; +use cargo::util::toml_mut::manifest::LocalManifest; +use cargo::CargoResult; + +pub fn cli() -> clap::Command { + clap::Command::new("remove") + // Subcommand aliases are handled in `aliased_command()`. + // .alias("rm") + .about("Remove dependencies from a Cargo.toml manifest file") + .args([clap::Arg::new("dependencies") + .action(clap::ArgAction::Append) + .required(true) + .num_args(1..) + .value_name("DEP_ID") + .help("Dependencies to be removed")]) + .arg_package("Package to remove from") + .arg_manifest_path() + .arg_quiet() + .arg_dry_run("Don't actually write the manifest") + .next_help_heading("Section") + .args([ + clap::Arg::new("dev") + .long("dev") + .conflicts_with("build") + .action(clap::ArgAction::SetTrue) + .group("section") + .help("Remove as development dependency"), + clap::Arg::new("build") + .long("build") + .conflicts_with("dev") + .action(clap::ArgAction::SetTrue) + .group("section") + .help("Remove as build dependency"), + clap::Arg::new("target") + .long("target") + .num_args(1) + .value_name("TARGET") + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .help("Remove as dependency from the given target platform"), + ]) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let dry_run = args.dry_run(); + + let workspace = args.workspace(config)?; + + if args.is_present_with_zero_values("package") { + print_available_packages(&workspace)?; + } + + let packages = args.packages_from_flags()?; + let packages = packages.get_packages(&workspace)?; + let spec = match packages.len() { + 0 => { + return Err(CliError::new( + anyhow::format_err!( + "no packages selected to modify. Please specify one with `-p <PKGID>`" + ), + 101, + )); + } + 1 => packages[0], + _ => { + let names = packages.iter().map(|p| p.name()).collect::<Vec<_>>(); + return Err(CliError::new( + anyhow::format_err!( + "`cargo remove` could not determine which package to modify. \ + Use the `--package` option to specify a package. \n\ + available packages: {}", + names.join(", ") + ), + 101, + )); + } + }; + + let dependencies = args + .get_many::<String>("dependencies") + .expect("required(true)") + .cloned() + .collect::<Vec<_>>(); + + let section = parse_section(args); + + let options = RemoveOptions { + config, + spec, + dependencies, + section, + dry_run, + }; + remove(&options)?; + + if !dry_run { + // Clean up the workspace + gc_workspace(&workspace)?; + + // Reload the workspace since we've changed dependencies + let ws = args.workspace(config)?; + resolve_ws(&ws)?; + } + + Ok(()) +} + +fn parse_section(args: &ArgMatches) -> DepTable { + let dev = args.flag("dev"); + let build = args.flag("build"); + + let kind = if dev { + DepKind::Development + } else if build { + DepKind::Build + } else { + DepKind::Normal + }; + + let mut table = DepTable::new().set_kind(kind); + + if let Some(target) = args.get_one::<String>("target") { + assert!(!target.is_empty(), "Target specification may not be empty"); + table = table.set_target(target); + } + + table +} + +/// Clean up the workspace.dependencies, profile, patch, and replace sections of the root manifest +/// by removing dependencies which no longer have a reference to them. +fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> { + let mut manifest: toml_edit::Document = + cargo_util::paths::read(workspace.root_manifest())?.parse()?; + let mut is_modified = true; + + let members = workspace + .members() + .map(|p| LocalManifest::try_new(p.manifest_path())) + .collect::<CargoResult<Vec<_>>>()?; + + let mut dependencies = members + .iter() + .flat_map(|manifest| { + manifest.get_sections().into_iter().flat_map(|(_, table)| { + table + .as_table_like() + .unwrap() + .iter() + .map(|(key, item)| Dependency::from_toml(&manifest.path, key, item)) + .collect::<Vec<_>>() + }) + }) + .collect::<CargoResult<Vec<_>>>()?; + + // Clean up the workspace.dependencies section and replace instances of + // workspace dependencies with their definitions + if let Some(toml_edit::Item::Table(deps_table)) = manifest + .get_mut("workspace") + .and_then(|t| t.get_mut("dependencies")) + { + deps_table.set_implicit(true); + for (key, item) in deps_table.iter_mut() { + let ws_dep = Dependency::from_toml(&workspace.root(), key.get(), item)?; + + // search for uses of this workspace dependency + let mut is_used = false; + for dep in dependencies.iter_mut().filter(|d| { + d.toml_key() == key.get() && matches!(d.source(), Some(Source::Workspace(_))) + }) { + // HACK: Replace workspace references in `dependencies` to simplify later GC steps: + // 1. Avoid having to look it up again to determine the dependency source / spec + // 2. The entry might get deleted, preventing us from looking it up again + // + // This does lose extra information, like features enabled, but that shouldn't be a + // problem for GC + *dep = ws_dep.clone(); + + is_used = true; + } + + if !is_used { + *item = toml_edit::Item::None; + is_modified = true; + } + } + } + + // Clean up the profile section + // + // Example tables: + // - profile.dev.package.foo + // - profile.release.package."*" + // - profile.release.package."foo:2.1.0" + if let Some(toml_edit::Item::Table(profile_section_table)) = manifest.get_mut("profile") { + profile_section_table.set_implicit(true); + + for (_, item) in profile_section_table.iter_mut() { + if let toml_edit::Item::Table(profile_table) = item { + profile_table.set_implicit(true); + + if let Some(toml_edit::Item::Table(package_table)) = + profile_table.get_mut("package") + { + package_table.set_implicit(true); + + for (key, item) in package_table.iter_mut() { + if !spec_has_match( + &PackageIdSpec::parse(key.get())?, + &dependencies, + workspace.config(), + )? { + *item = toml_edit::Item::None; + is_modified = true; + } + } + } + } + } + } + + // Clean up the patch section + if let Some(toml_edit::Item::Table(patch_section_table)) = manifest.get_mut("patch") { + patch_section_table.set_implicit(true); + + // The key in each of the subtables is a source (either a registry or a URL) + for (source, item) in patch_section_table.iter_mut() { + if let toml_edit::Item::Table(patch_table) = item { + patch_table.set_implicit(true); + + for (key, item) in patch_table.iter_mut() { + let package_name = + Dependency::from_toml(&workspace.root_manifest(), key.get(), item)?.name; + if !source_has_match( + &package_name, + source.get(), + &dependencies, + workspace.config(), + )? { + *item = toml_edit::Item::None; + } + } + } + } + } + + // Clean up the replace section + if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") { + table.set_implicit(true); + + for (key, item) in table.iter_mut() { + if !spec_has_match( + &PackageIdSpec::parse(key.get())?, + &dependencies, + workspace.config(), + )? { + *item = toml_edit::Item::None; + is_modified = true; + } + } + } + + if is_modified { + cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?; + } + + Ok(()) +} + +/// Check whether or not a package ID spec matches any non-workspace dependencies. +fn spec_has_match( + spec: &PackageIdSpec, + dependencies: &[Dependency], + config: &Config, +) -> CargoResult<bool> { + for dep in dependencies { + if spec.name().as_str() != &dep.name { + continue; + } + + let version_matches = match (spec.version(), dep.version()) { + (Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(v), + (Some(_), None) => false, + (None, None | Some(_)) => true, + }; + if !version_matches { + continue; + } + + match dep.source_id(config)? { + MaybeWorkspace::Other(source_id) => { + if spec.url().map(|u| u == source_id.url()).unwrap_or(true) { + return Ok(true); + } + } + MaybeWorkspace::Workspace(_) => {} + } + } + + Ok(false) +} + +/// Check whether or not a source (URL or registry name) matches any non-workspace dependencies. +fn source_has_match( + name: &str, + source: &str, + dependencies: &[Dependency], + config: &Config, +) -> CargoResult<bool> { + for dep in dependencies { + if &dep.name != name { + continue; + } + + match dep.source_id(config)? { + MaybeWorkspace::Other(source_id) => { + if source_id.is_registry() { + if source_id.display_registry_name() == source + || source_id.url().as_str() == source + { + return Ok(true); + } + } else if source_id.is_git() { + if source_id.url().as_str() == source { + return Ok(true); + } + } + } + MaybeWorkspace::Workspace(_) => {} + } + } + + Ok(false) +} diff --git a/src/bin/cargo/commands/report.rs b/src/bin/cargo/commands/report.rs new file mode 100644 index 0000000..275a8f7 --- /dev/null +++ b/src/bin/cargo/commands/report.rs @@ -0,0 +1,49 @@ +use crate::command_prelude::*; +use cargo::core::compiler::future_incompat::{OnDiskReports, REPORT_PREAMBLE}; +use cargo::drop_println; + +pub fn cli() -> Command { + subcommand("report") + .about("Generate and display various kinds of reports") + .after_help("Run `cargo help report` for more detailed information.\n") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + subcommand("future-incompatibilities") + .alias("future-incompat") + .about("Reports any crates which will eventually stop compiling") + .arg( + opt( + "id", + "identifier of the report generated by a Cargo command invocation", + ) + .value_name("id"), + ) + .arg_package("Package to display a report for"), + ) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + match args.subcommand() { + Some(("future-incompatibilities", args)) => report_future_incompatibilities(config, args), + Some((cmd, _)) => { + unreachable!("unexpected command {}", cmd) + } + None => { + unreachable!("unexpected command") + } + } +} + +fn report_future_incompatibilities(config: &Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let reports = OnDiskReports::load(&ws)?; + let id = args + .value_of_u32("id")? + .unwrap_or_else(|| reports.last_id()); + let krate = args.get_one::<String>("package").map(String::as_str); + let report = reports.get_report(id, config, krate)?; + drop_println!(config, "{}", REPORT_PREAMBLE); + drop(config.shell().print_ansi_stdout(report.as_bytes())); + Ok(()) +} diff --git a/src/bin/cargo/commands/run.rs b/src/bin/cargo/commands/run.rs new file mode 100644 index 0000000..cde754c --- /dev/null +++ b/src/bin/cargo/commands/run.rs @@ -0,0 +1,103 @@ +use crate::command_prelude::*; +use crate::util::restricted_names::is_glob_pattern; +use cargo::core::Verbosity; +use cargo::ops::{self, CompileFilter, Packages}; +use cargo_util::ProcessError; + +pub fn cli() -> Command { + subcommand("run") + // subcommand aliases are handled in aliased_command() + // .alias("r") + .about("Run a binary or example of the local package") + .arg_quiet() + .arg( + Arg::new("args") + .help("Arguments for the binary or example to run") + .value_parser(value_parser!(std::ffi::OsString)) + .num_args(0..) + .trailing_var_arg(true), + ) + .arg_targets_bin_example( + "Name of the bin target to run", + "Name of the example target to run", + ) + .arg_package("Package with the target to run") + .arg_jobs() + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_message_format() + .arg_unit_graph() + .arg_ignore_rust_version() + .arg_timings() + .after_help("Run `cargo help run` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + + let mut compile_opts = args.compile_options( + config, + CompileMode::Build, + Some(&ws), + ProfileChecking::Custom, + )?; + + // Disallow `spec` to be an glob pattern + if let Packages::Packages(opt_in) = &compile_opts.spec { + if let Some(pattern) = opt_in.iter().find(|s| is_glob_pattern(s)) { + return Err(anyhow::anyhow!( + "`cargo run` does not support glob pattern `{}` on package selection", + pattern, + ) + .into()); + } + } + + if !args.contains_id("example") && !args.contains_id("bin") { + let default_runs: Vec<_> = compile_opts + .spec + .get_packages(&ws)? + .iter() + .filter_map(|pkg| pkg.manifest().default_run()) + .collect(); + if let [bin] = &default_runs[..] { + compile_opts.filter = CompileFilter::single_bin(bin.to_string()); + } else { + // ops::run will take care of errors if len pkgs != 1. + compile_opts.filter = CompileFilter::Default { + // Force this to false because the code in ops::run is not + // able to pre-check features before compilation starts to + // enforce that only 1 binary is built. + required_features_filterable: false, + }; + } + }; + + ops::run(&ws, &compile_opts, &values_os(args, "args")).map_err(|err| { + let proc_err = match err.downcast_ref::<ProcessError>() { + Some(e) => e, + None => return CliError::new(err, 101), + }; + + // If we never actually spawned the process then that sounds pretty + // bad and we always want to forward that up. + let exit_code = match proc_err.code { + Some(exit) => exit, + None => return CliError::new(err, 101), + }; + + // If `-q` was passed then we suppress extra error information about + // a failed process, we assume the process itself printed out enough + // information about why it failed so we don't do so as well + let is_quiet = config.shell().verbosity() == Verbosity::Quiet; + if is_quiet { + CliError::code(exit_code) + } else { + CliError::new(err, exit_code) + } + }) +} diff --git a/src/bin/cargo/commands/rustc.rs b/src/bin/cargo/commands/rustc.rs new file mode 100644 index 0000000..de73eb8 --- /dev/null +++ b/src/bin/cargo/commands/rustc.rs @@ -0,0 +1,100 @@ +use crate::command_prelude::*; +use cargo::ops; +use cargo::util::interning::InternedString; + +const PRINT_ARG_NAME: &str = "print"; +const CRATE_TYPE_ARG_NAME: &str = "crate-type"; + +pub fn cli() -> Command { + subcommand("rustc") + .about("Compile a package, and pass extra options to the compiler") + .arg_quiet() + .arg( + Arg::new("args") + .num_args(0..) + .help("Extra rustc flags") + .trailing_var_arg(true), + ) + .arg_package("Package to build") + .arg_jobs() + .arg_targets_all( + "Build only this package's library", + "Build only the specified binary", + "Build all binaries", + "Build only the specified example", + "Build all examples", + "Build only the specified test target", + "Build all tests", + "Build only the specified bench target", + "Build all benches", + "Build all targets", + ) + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Target triple which compiles will be for") + .arg( + opt( + PRINT_ARG_NAME, + "Output compiler information without compiling", + ) + .value_name("INFO"), + ) + .arg(multi_opt( + CRATE_TYPE_ARG_NAME, + "CRATE-TYPE", + "Comma separated list of types of crates for the compiler to emit", + )) + .arg_target_dir() + .arg_manifest_path() + .arg_message_format() + .arg_unit_graph() + .arg_ignore_rust_version() + .arg_future_incompat_report() + .arg_timings() + .after_help("Run `cargo help rustc` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + // This is a legacy behavior that changes the behavior based on the profile. + // If we want to support this more formally, I think adding a --mode flag + // would be warranted. + let mode = match args.get_one::<String>("profile").map(String::as_str) { + Some("test") => CompileMode::Test, + Some("bench") => CompileMode::Bench, + Some("check") => CompileMode::Check { test: false }, + _ => CompileMode::Build, + }; + let mut compile_opts = args.compile_options_for_single_package( + config, + mode, + Some(&ws), + ProfileChecking::LegacyRustc, + )?; + if compile_opts.build_config.requested_profile == "check" { + compile_opts.build_config.requested_profile = InternedString::new("dev"); + } + let target_args = values(args, "args"); + compile_opts.target_rustc_args = if target_args.is_empty() { + None + } else { + Some(target_args) + }; + if let Some(opt_value) = args.get_one::<String>(PRINT_ARG_NAME) { + config + .cli_unstable() + .fail_if_stable_opt(PRINT_ARG_NAME, 9357)?; + ops::print(&ws, &compile_opts, opt_value)?; + return Ok(()); + } + let crate_types = values(args, CRATE_TYPE_ARG_NAME); + compile_opts.target_rustc_crate_types = if crate_types.is_empty() { + None + } else { + Some(crate_types) + }; + ops::compile(&ws, &compile_opts)?; + + Ok(()) +} diff --git a/src/bin/cargo/commands/rustdoc.rs b/src/bin/cargo/commands/rustdoc.rs new file mode 100644 index 0000000..e87f435 --- /dev/null +++ b/src/bin/cargo/commands/rustdoc.rs @@ -0,0 +1,66 @@ +use cargo::ops::{self, DocOptions}; + +use crate::command_prelude::*; + +pub fn cli() -> Command { + subcommand("rustdoc") + .about("Build a package's documentation, using specified custom flags.") + .arg_quiet() + .arg( + Arg::new("args") + .help("Extra rustdoc flags") + .num_args(0..) + .trailing_var_arg(true), + ) + .arg(flag( + "open", + "Opens the docs in a browser after the operation", + )) + .arg_package("Package to document") + .arg_jobs() + .arg_targets_all( + "Build only this package's library", + "Build only the specified binary", + "Build all binaries", + "Build only the specified example", + "Build all examples", + "Build only the specified test target", + "Build all tests", + "Build only the specified bench target", + "Build all benches", + "Build all targets", + ) + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_message_format() + .arg_unit_graph() + .arg_ignore_rust_version() + .arg_timings() + .after_help("Run `cargo help rustdoc` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + let mut compile_opts = args.compile_options_for_single_package( + config, + CompileMode::Doc { deps: false }, + Some(&ws), + ProfileChecking::Custom, + )?; + let target_args = values(args, "args"); + compile_opts.target_rustdoc_args = if target_args.is_empty() { + None + } else { + Some(target_args) + }; + let doc_opts = DocOptions { + open_result: args.flag("open"), + compile_opts, + }; + ops::doc(&ws, &doc_opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/search.rs b/src/bin/cargo/commands/search.rs new file mode 100644 index 0000000..c55d932 --- /dev/null +++ b/src/bin/cargo/commands/search.rs @@ -0,0 +1,37 @@ +use crate::command_prelude::*; + +use std::cmp::min; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("search") + .about("Search packages in crates.io") + .arg_quiet() + .arg(Arg::new("query").num_args(0..)) + .arg_index() + .arg( + opt( + "limit", + "Limit the number of results (default: 10, max: 100)", + ) + .value_name("LIMIT"), + ) + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .after_help("Run `cargo help search` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let registry = args.registry(config)?; + let index = args.index()?; + let limit = args.value_of_u32("limit")?; + let limit = min(100, limit.unwrap_or(10)); + let query: Vec<&str> = args + .get_many::<String>("query") + .unwrap_or_default() + .map(String::as_str) + .collect(); + let query: String = query.join("+"); + ops::search(&query, config, index, limit, registry)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/test.rs b/src/bin/cargo/commands/test.rs new file mode 100644 index 0000000..607655a --- /dev/null +++ b/src/bin/cargo/commands/test.rs @@ -0,0 +1,113 @@ +use crate::command_prelude::*; +use cargo::ops; + +pub fn cli() -> Command { + subcommand("test") + // Subcommand aliases are handled in `aliased_command()`. + // .alias("t") + .about("Execute all unit and integration tests and build examples of a local package") + .arg( + Arg::new("TESTNAME") + .action(ArgAction::Set) + .help("If specified, only run tests containing this string in their names"), + ) + .arg( + Arg::new("args") + .help("Arguments for the test binary") + .num_args(0..) + .last(true), + ) + .arg( + flag( + "quiet", + "Display one character per test instead of one line", + ) + .short('q'), + ) + .arg_targets_all( + "Test only this package's library unit tests", + "Test only the specified binary", + "Test all binaries", + "Test only the specified example", + "Test all examples", + "Test only the specified test target", + "Test all tests", + "Test only the specified bench target", + "Test all benches", + "Test all targets", + ) + .arg(flag("doc", "Test only this library's documentation")) + .arg(flag("no-run", "Compile, but don't run tests")) + .arg(flag("no-fail-fast", "Run all tests regardless of failure")) + .arg_package_spec( + "Package to run tests for", + "Test all packages in the workspace", + "Exclude packages from the test", + ) + .arg_jobs() + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg_manifest_path() + .arg_ignore_rust_version() + .arg_message_format() + .arg_unit_graph() + .arg_future_incompat_report() + .arg_timings() + .after_help( + "Run `cargo help test` for more detailed information.\n\ + Run `cargo test -- --help` for test binary options.\n", + ) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + + let mut compile_opts = args.compile_options( + config, + CompileMode::Test, + Some(&ws), + ProfileChecking::Custom, + )?; + + compile_opts.build_config.requested_profile = + args.get_profile_name(config, "test", ProfileChecking::Custom)?; + + // `TESTNAME` is actually an argument of the test binary, but it's + // important, so we explicitly mention it and reconfigure. + let test_name = args.get_one::<String>("TESTNAME"); + let test_args = args.get_one::<String>("TESTNAME").into_iter(); + let test_args = test_args.chain(args.get_many::<String>("args").unwrap_or_default()); + let test_args = test_args.map(String::as_str).collect::<Vec<_>>(); + + let no_run = args.flag("no-run"); + let doc = args.flag("doc"); + if doc { + if compile_opts.filter.is_specific() { + return Err( + anyhow::format_err!("Can't mix --doc with other target selecting options").into(), + ); + } + if no_run { + return Err(anyhow::format_err!("Can't skip running doc tests with --no-run").into()); + } + compile_opts.build_config.mode = CompileMode::Doctest; + compile_opts.filter = ops::CompileFilter::lib_only(); + } else if test_name.is_some() && !compile_opts.filter.is_specific() { + // If arg `TESTNAME` is provided, assumed that the user knows what + // exactly they wants to test, so we use `all_test_targets` to + // avoid compiling unnecessary targets such as examples, which are + // included by the logic of default target filter. + compile_opts.filter = ops::CompileFilter::all_test_targets(); + } + + let ops = ops::TestOptions { + no_run, + no_fail_fast: args.flag("no-fail-fast"), + compile_opts, + }; + + ops::run_tests(&ws, &ops, &test_args) +} diff --git a/src/bin/cargo/commands/tree.rs b/src/bin/cargo/commands/tree.rs new file mode 100644 index 0000000..0a75178 --- /dev/null +++ b/src/bin/cargo/commands/tree.rs @@ -0,0 +1,305 @@ +use crate::cli; +use crate::command_prelude::*; +use anyhow::{bail, format_err}; +use cargo::core::dependency::DepKind; +use cargo::ops::tree::{self, EdgeKind}; +use cargo::ops::Packages; +use cargo::util::print_available_packages; +use cargo::util::CargoResult; +use std::collections::HashSet; +use std::str::FromStr; + +pub fn cli() -> Command { + subcommand("tree") + .about("Display a tree visualization of a dependency graph") + .arg_quiet() + .arg_manifest_path() + .arg_package_spec_no_all( + "Package to be used as the root of the tree", + "Display the tree for all packages in the workspace", + "Exclude specific workspace members", + ) + .arg( + flag("all", "Deprecated, use --no-dedupe instead") + .short('a') + .hide(true), + ) + .arg(flag("all-targets", "Deprecated, use --target=all instead").hide(true)) + .arg_features() + .arg_target_triple( + "Filter dependencies matching the given target-triple (default host platform). \ + Pass `all` to include all targets.", + ) + .arg(flag("no-dev-dependencies", "Deprecated, use -e=no-dev instead").hide(true)) + .arg( + multi_opt( + "edges", + "KINDS", + "The kinds of dependencies to display \ + (features, normal, build, dev, all, \ + no-normal, no-build, no-dev, no-proc-macro)", + ) + .short('e'), + ) + .arg( + optional_multi_opt( + "invert", + "SPEC", + "Invert the tree direction and focus on the given package", + ) + .short('i'), + ) + .arg(multi_opt( + "prune", + "SPEC", + "Prune the given package from the display of the dependency tree", + )) + .arg(opt("depth", "Maximum display depth of the dependency tree").value_name("DEPTH")) + .arg(flag("no-indent", "Deprecated, use --prefix=none instead").hide(true)) + .arg(flag("prefix-depth", "Deprecated, use --prefix=depth instead").hide(true)) + .arg( + opt( + "prefix", + "Change the prefix (indentation) of how each entry is displayed", + ) + .value_name("PREFIX") + .value_parser(["depth", "indent", "none"]) + .default_value("indent"), + ) + .arg(flag( + "no-dedupe", + "Do not de-duplicate (repeats all shared dependencies)", + )) + .arg( + flag( + "duplicates", + "Show only dependencies which come in multiple versions (implies -i)", + ) + .short('d') + .alias("duplicate"), + ) + .arg( + opt("charset", "Character set to use in output: utf8, ascii") + .value_name("CHARSET") + .value_parser(["utf8", "ascii"]) + .default_value("utf8"), + ) + .arg( + opt("format", "Format string used for printing dependencies") + .value_name("FORMAT") + .short('f') + .default_value("{p}"), + ) + .arg( + // Backwards compatibility with old cargo-tree. + flag("version", "Print version info and exit") + .short('V') + .hide(true), + ) + .after_help("Run `cargo help tree` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + if args.flag("version") { + let verbose = args.verbose() > 0; + let version = cli::get_version_string(verbose); + cargo::drop_print!(config, "{}", version); + return Ok(()); + } + let prefix = if args.flag("no-indent") { + config + .shell() + .warn("the --no-indent flag has been changed to --prefix=none")?; + "none" + } else if args.flag("prefix-depth") { + config + .shell() + .warn("the --prefix-depth flag has been changed to --prefix=depth")?; + "depth" + } else { + args.get_one::<String>("prefix").unwrap().as_str() + }; + let prefix = tree::Prefix::from_str(prefix).map_err(|e| anyhow::anyhow!("{}", e))?; + + let no_dedupe = args.flag("no-dedupe") || args.flag("all"); + if args.flag("all") { + config.shell().warn( + "The `cargo tree` --all flag has been changed to --no-dedupe, \ + and may be removed in a future version.\n\ + If you are looking to display all workspace members, use the --workspace flag.", + )?; + } + + let targets = if args.flag("all-targets") { + config + .shell() + .warn("the --all-targets flag has been changed to --target=all")?; + vec!["all".to_string()] + } else { + args._values_of("target") + }; + let target = tree::Target::from_cli(targets); + + let (edge_kinds, no_proc_macro) = parse_edge_kinds(config, args)?; + let graph_features = edge_kinds.contains(&EdgeKind::Feature); + + let pkgs_to_prune = args._values_of("prune"); + + let packages = args.packages_from_flags()?; + let mut invert = args + .get_many::<String>("invert") + .map_or_else(|| Vec::new(), |is| is.map(|s| s.to_string()).collect()); + if args.is_present_with_zero_values("invert") { + match &packages { + Packages::Packages(ps) => { + // Backwards compatibility with old syntax of `cargo tree -i -p foo`. + invert.extend(ps.clone()); + } + _ => { + return Err(format_err!( + "The `-i` flag requires a package name.\n\ +\n\ +The `-i` flag is used to inspect the reverse dependencies of a specific\n\ +package. It will invert the tree and display the packages that depend on the\n\ +given package.\n\ +\n\ +Note that in a workspace, by default it will only display the package's\n\ +reverse dependencies inside the tree of the workspace member in the current\n\ +directory. The --workspace flag can be used to extend it so that it will show\n\ +the package's reverse dependencies across the entire workspace. The -p flag\n\ +can be used to display the package's reverse dependencies only with the\n\ +subtree of the package given to -p.\n\ +" + ) + .into()); + } + } + } + + let ws = args.workspace(config)?; + + if args.is_present_with_zero_values("package") { + print_available_packages(&ws)?; + } + + let charset = tree::Charset::from_str(args.get_one::<String>("charset").unwrap()) + .map_err(|e| anyhow::anyhow!("{}", e))?; + let opts = tree::TreeOptions { + cli_features: args.cli_features()?, + packages, + target, + edge_kinds, + invert, + pkgs_to_prune, + prefix, + no_dedupe, + duplicates: args.flag("duplicates"), + charset, + format: args.get_one::<String>("format").cloned().unwrap(), + graph_features, + max_display_depth: args.value_of_u32("depth")?.unwrap_or(u32::MAX), + no_proc_macro, + }; + + if opts.graph_features && opts.duplicates { + return Err(format_err!("the `-e features` flag does not support `--duplicates`").into()); + } + + tree::build_and_print(&ws, &opts)?; + Ok(()) +} + +/// Parses `--edges` option. +/// +/// Returns a tuple of `EdgeKind` map and `no_proc_marco` flag. +fn parse_edge_kinds(config: &Config, args: &ArgMatches) -> CargoResult<(HashSet<EdgeKind>, bool)> { + let (kinds, no_proc_macro) = { + let mut no_proc_macro = false; + let mut kinds = args.get_many::<String>("edges").map_or_else( + || Vec::new(), + |es| { + es.flat_map(|e| e.split(',')) + .filter(|e| { + no_proc_macro = *e == "no-proc-macro"; + !no_proc_macro + }) + .collect() + }, + ); + + if args.flag("no-dev-dependencies") { + config + .shell() + .warn("the --no-dev-dependencies flag has changed to -e=no-dev")?; + kinds.push("no-dev"); + } + + if kinds.is_empty() { + kinds.extend(&["normal", "build", "dev"]); + } + + (kinds, no_proc_macro) + }; + + let mut result = HashSet::new(); + let insert_defaults = |result: &mut HashSet<EdgeKind>| { + result.insert(EdgeKind::Dep(DepKind::Normal)); + result.insert(EdgeKind::Dep(DepKind::Build)); + result.insert(EdgeKind::Dep(DepKind::Development)); + }; + let unknown = |k| { + bail!( + "unknown edge kind `{}`, valid values are \ + \"normal\", \"build\", \"dev\", \ + \"no-normal\", \"no-build\", \"no-dev\", \"no-proc-macro\", \ + \"features\", or \"all\"", + k + ) + }; + if kinds.iter().any(|k| k.starts_with("no-")) { + insert_defaults(&mut result); + for kind in &kinds { + match *kind { + "no-normal" => result.remove(&EdgeKind::Dep(DepKind::Normal)), + "no-build" => result.remove(&EdgeKind::Dep(DepKind::Build)), + "no-dev" => result.remove(&EdgeKind::Dep(DepKind::Development)), + "features" => result.insert(EdgeKind::Feature), + "normal" | "build" | "dev" | "all" => { + bail!( + "`{}` dependency kind cannot be mixed with \ + \"no-normal\", \"no-build\", or \"no-dev\" \ + dependency kinds", + kind + ) + } + k => return unknown(k), + }; + } + return Ok((result, no_proc_macro)); + } + for kind in &kinds { + match *kind { + "all" => { + insert_defaults(&mut result); + result.insert(EdgeKind::Feature); + } + "features" => { + result.insert(EdgeKind::Feature); + } + "normal" => { + result.insert(EdgeKind::Dep(DepKind::Normal)); + } + "build" => { + result.insert(EdgeKind::Dep(DepKind::Build)); + } + "dev" => { + result.insert(EdgeKind::Dep(DepKind::Development)); + } + k => return unknown(k), + } + } + if kinds.len() == 1 && kinds[0] == "features" { + insert_defaults(&mut result); + } + Ok((result, no_proc_macro)) +} diff --git a/src/bin/cargo/commands/uninstall.rs b/src/bin/cargo/commands/uninstall.rs new file mode 100644 index 0000000..46654b6 --- /dev/null +++ b/src/bin/cargo/commands/uninstall.rs @@ -0,0 +1,34 @@ +use crate::command_prelude::*; + +use cargo::ops; + +pub fn cli() -> Command { + subcommand("uninstall") + .about("Remove a Rust binary") + .arg_quiet() + .arg(Arg::new("spec").num_args(0..)) + .arg_package_spec_simple("Package to uninstall") + .arg(multi_opt("bin", "NAME", "Only uninstall the binary NAME")) + .arg(opt("root", "Directory to uninstall packages from").value_name("DIR")) + .after_help("Run `cargo help uninstall` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let root = args.get_one::<String>("root").map(String::as_str); + + if args.is_present_with_zero_values("package") { + return Err(anyhow::anyhow!( + "\"--package <SPEC>\" requires a SPEC format value.\n\ + Run `cargo help pkgid` for more information about SPEC format." + ) + .into()); + } + + let specs = args + .get_many::<String>("spec") + .unwrap_or_else(|| args.get_many::<String>("package").unwrap_or_default()) + .map(String::as_str) + .collect(); + ops::uninstall(root, specs, &values(args, "bin"), config)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/update.rs b/src/bin/cargo/commands/update.rs new file mode 100644 index 0000000..da33e8d --- /dev/null +++ b/src/bin/cargo/commands/update.rs @@ -0,0 +1,46 @@ +use crate::command_prelude::*; + +use cargo::ops::{self, UpdateOptions}; +use cargo::util::print_available_packages; + +pub fn cli() -> Command { + subcommand("update") + .about("Update dependencies as recorded in the local lock file") + .arg_quiet() + .arg(flag("workspace", "Only update the workspace packages").short('w')) + .arg_package_spec_simple("Package to update") + .arg(flag( + "aggressive", + "Force updating all dependencies of SPEC as well when used with -p", + )) + .arg_dry_run("Don't actually write the lockfile") + .arg( + opt( + "precise", + "Update a single dependency to exactly PRECISE when used with -p", + ) + .value_name("PRECISE") + .requires("package"), + ) + .arg_manifest_path() + .after_help("Run `cargo help update` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let ws = args.workspace(config)?; + + if args.is_present_with_zero_values("package") { + print_available_packages(&ws)?; + } + + let update_opts = UpdateOptions { + aggressive: args.flag("aggressive"), + precise: args.get_one::<String>("precise").map(String::as_str), + to_update: values(args, "package"), + dry_run: args.dry_run(), + workspace: args.flag("workspace"), + config, + }; + ops::update_lockfile(&ws, &update_opts)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/vendor.rs b/src/bin/cargo/commands/vendor.rs new file mode 100644 index 0000000..1fd79ec --- /dev/null +++ b/src/bin/cargo/commands/vendor.rs @@ -0,0 +1,100 @@ +use crate::command_prelude::*; +use cargo::ops; +use std::path::PathBuf; + +pub fn cli() -> Command { + subcommand("vendor") + .about("Vendor all dependencies for a project locally") + .arg_quiet() + .arg_manifest_path() + .arg( + Arg::new("path") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .help("Where to vendor crates (`vendor` by default)"), + ) + .arg(flag( + "no-delete", + "Don't delete older crates in the vendor directory", + )) + .arg( + Arg::new("tomls") + .short('s') + .long("sync") + .help("Additional `Cargo.toml` to sync and vendor") + .value_name("TOML") + .value_parser(clap::value_parser!(PathBuf)) + .action(clap::ArgAction::Append), + ) + .arg(flag( + "respect-source-config", + "Respect `[source]` config in `.cargo/config`", + )) + .arg(flag( + "versioned-dirs", + "Always include version in subdir name", + )) + .arg(flag("no-merge-sources", "Not supported").hide(true)) + .arg(flag("relative-path", "Not supported").hide(true)) + .arg(flag("only-git-deps", "Not supported").hide(true)) + .arg(flag("disallow-duplicates", "Not supported").hide(true)) + .after_help("Run `cargo help vendor` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + // We're doing the vendoring operation ourselves, so we don't actually want + // to respect any of the `source` configuration in Cargo itself. That's + // intended for other consumers of Cargo, but we want to go straight to the + // source, e.g. crates.io, to fetch crates. + if !args.flag("respect-source-config") { + config.values_mut()?.remove("source"); + } + + // When we moved `cargo vendor` into Cargo itself we didn't stabilize a few + // flags, so try to provide a helpful error message in that case to ensure + // that users currently using the flag aren't tripped up. + let crates_io_cargo_vendor_flag = if args.flag("no-merge-sources") { + Some("--no-merge-sources") + } else if args.flag("relative-path") { + Some("--relative-path") + } else if args.flag("only-git-deps") { + Some("--only-git-deps") + } else if args.flag("disallow-duplicates") { + Some("--disallow-duplicates") + } else { + None + }; + if let Some(flag) = crates_io_cargo_vendor_flag { + return Err(anyhow::format_err!( + "\ +the crates.io `cargo vendor` command has now been merged into Cargo itself +and does not support the flag `{}` currently; to continue using the flag you +can execute `cargo-vendor vendor ...`, and if you would like to see this flag +supported in Cargo itself please feel free to file an issue at +https://github.com/rust-lang/cargo/issues/new +", + flag + ) + .into()); + } + + let ws = args.workspace(config)?; + let path = args + .get_one::<PathBuf>("path") + .cloned() + .unwrap_or_else(|| PathBuf::from("vendor")); + ops::vendor( + &ws, + &ops::VendorOptions { + no_delete: args.flag("no-delete"), + destination: &path, + versioned_dirs: args.flag("versioned-dirs"), + extra: args + .get_many::<PathBuf>("tomls") + .unwrap_or_default() + .cloned() + .collect(), + }, + )?; + Ok(()) +} diff --git a/src/bin/cargo/commands/verify_project.rs b/src/bin/cargo/commands/verify_project.rs new file mode 100644 index 0000000..4d54926 --- /dev/null +++ b/src/bin/cargo/commands/verify_project.rs @@ -0,0 +1,26 @@ +use crate::command_prelude::*; + +use std::collections::HashMap; +use std::process; + +pub fn cli() -> Command { + subcommand("verify-project") + .about("Check correctness of crate manifest") + .arg_quiet() + .arg_manifest_path() + .after_help("Run `cargo help verify-project` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + if let Err(e) = args.workspace(config) { + config + .shell() + .print_json(&HashMap::from([("invalid", e.to_string())]))?; + process::exit(1) + } + + config + .shell() + .print_json(&HashMap::from([("success", "true")]))?; + Ok(()) +} diff --git a/src/bin/cargo/commands/version.rs b/src/bin/cargo/commands/version.rs new file mode 100644 index 0000000..ac1681f --- /dev/null +++ b/src/bin/cargo/commands/version.rs @@ -0,0 +1,16 @@ +use crate::cli; +use crate::command_prelude::*; + +pub fn cli() -> Command { + subcommand("version") + .about("Show version information") + .arg_quiet() + .after_help("Run `cargo help version` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let verbose = args.verbose() > 0; + let version = cli::get_version_string(verbose); + cargo::drop_print!(config, "{}", version); + Ok(()) +} diff --git a/src/bin/cargo/commands/yank.rs b/src/bin/cargo/commands/yank.rs new file mode 100644 index 0000000..3dee522 --- /dev/null +++ b/src/bin/cargo/commands/yank.rs @@ -0,0 +1,65 @@ +use crate::command_prelude::*; + +use cargo::ops; +use cargo::util::auth::Secret; + +pub fn cli() -> Command { + subcommand("yank") + .about("Remove a pushed crate from the index") + .arg_quiet() + .arg(Arg::new("crate").action(ArgAction::Set)) + .arg( + opt("version", "The version to yank or un-yank") + .alias("vers") + .value_name("VERSION"), + ) + .arg(flag( + "undo", + "Undo a yank, putting a version back into the index", + )) + .arg(opt("index", "Registry index to yank from").value_name("INDEX")) + .arg(opt("token", "API token to use when authenticating").value_name("TOKEN")) + .arg(opt("registry", "Registry to use").value_name("REGISTRY")) + .after_help("Run `cargo help yank` for more detailed information.\n") +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let registry = args.registry(config)?; + + let (krate, version) = resolve_crate( + args.get_one::<String>("crate").map(String::as_str), + args.get_one::<String>("version").map(String::as_str), + )?; + if version.is_none() { + return Err(anyhow::format_err!("`--version` is required").into()); + } + + ops::yank( + config, + krate.map(|s| s.to_string()), + version.map(|s| s.to_string()), + args.get_one::<String>("token").cloned().map(Secret::from), + args.get_one::<String>("index").cloned(), + args.flag("undo"), + registry, + )?; + Ok(()) +} + +fn resolve_crate<'k>( + mut krate: Option<&'k str>, + mut version: Option<&'k str>, +) -> crate::CargoResult<(Option<&'k str>, Option<&'k str>)> { + if let Some((k, v)) = krate.and_then(|k| k.split_once('@')) { + if version.is_some() { + anyhow::bail!("cannot specify both `@{v}` and `--version`"); + } + if k.is_empty() { + // by convention, arguments starting with `@` are response files + anyhow::bail!("missing crate name for `@{v}`"); + } + krate = Some(k); + version = Some(v); + } + Ok((krate, version)) +} diff --git a/src/bin/cargo/main.rs b/src/bin/cargo/main.rs new file mode 100644 index 0000000..28cd18d --- /dev/null +++ b/src/bin/cargo/main.rs @@ -0,0 +1,322 @@ +#![warn(rust_2018_idioms)] // while we're getting used to 2018 +#![allow(clippy::all)] + +use cargo::util::toml::StringOrVec; +use cargo::util::CliError; +use cargo::util::{self, closest_msg, command_prelude, CargoResult, CliResult, Config}; +use cargo_util::{ProcessBuilder, ProcessError}; +use std::collections::BTreeMap; +use std::env; +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; + +mod cli; +mod commands; + +use crate::command_prelude::*; + +fn main() { + #[cfg(feature = "pretty-env-logger")] + pretty_env_logger::init_custom_env("CARGO_LOG"); + #[cfg(not(feature = "pretty-env-logger"))] + env_logger::init_from_env("CARGO_LOG"); + + let mut config = cli::LazyConfig::new(); + + let result = if let Some(lock_addr) = cargo::ops::fix_get_proxy_lock_addr() { + cargo::ops::fix_exec_rustc(config.get(), &lock_addr).map_err(|e| CliError::from(e)) + } else { + let _token = cargo::util::job::setup(); + cli::main(&mut config) + }; + + match result { + Err(e) => cargo::exit_with_error(e, &mut config.get_mut().shell()), + Ok(()) => {} + } +} + +/// Table for defining the aliases which come builtin in `Cargo`. +/// The contents are structured as: `(alias, aliased_command, description)`. +const BUILTIN_ALIASES: [(&str, &str, &str); 6] = [ + ("b", "build", "alias: build"), + ("c", "check", "alias: check"), + ("d", "doc", "alias: doc"), + ("r", "run", "alias: run"), + ("t", "test", "alias: test"), + ("rm", "remove", "alias: remove"), +]; + +/// Function which contains the list of all of the builtin aliases and it's +/// corresponding execs represented as &str. +fn builtin_aliases_execs(cmd: &str) -> Option<&(&str, &str, &str)> { + BUILTIN_ALIASES.iter().find(|alias| alias.0 == cmd) +} + +/// Resolve the aliased command from the [`Config`] with a given command string. +/// +/// The search fallback chain is: +/// +/// 1. Get the aliased command as a string. +/// 2. If an `Err` occurs (missing key, type mismatch, or any possible error), +/// try to get it as an array again. +/// 3. If still cannot find any, finds one insides [`BUILTIN_ALIASES`]. +fn aliased_command(config: &Config, command: &str) -> CargoResult<Option<Vec<String>>> { + let alias_name = format!("alias.{}", command); + let user_alias = match config.get_string(&alias_name) { + Ok(Some(record)) => Some( + record + .val + .split_whitespace() + .map(|s| s.to_string()) + .collect(), + ), + Ok(None) => None, + Err(_) => config.get::<Option<Vec<String>>>(&alias_name)?, + }; + + let result = user_alias.or_else(|| { + builtin_aliases_execs(command).map(|command_str| vec![command_str.1.to_string()]) + }); + Ok(result) +} + +/// List all runnable commands +fn list_commands(config: &Config) -> BTreeMap<String, CommandInfo> { + let prefix = "cargo-"; + let suffix = env::consts::EXE_SUFFIX; + let mut commands = BTreeMap::new(); + for dir in search_directories(config) { + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + _ => continue, + }; + for entry in entries.filter_map(|e| e.ok()) { + let path = entry.path(); + let filename = match path.file_name().and_then(|s| s.to_str()) { + Some(filename) => filename, + _ => continue, + }; + if !filename.starts_with(prefix) || !filename.ends_with(suffix) { + continue; + } + if is_executable(entry.path()) { + let end = filename.len() - suffix.len(); + commands.insert( + filename[prefix.len()..end].to_string(), + CommandInfo::External { path: path.clone() }, + ); + } + } + } + + for cmd in commands::builtin() { + commands.insert( + cmd.get_name().to_string(), + CommandInfo::BuiltIn { + about: cmd.get_about().map(|s| s.to_string()), + }, + ); + } + + // Add the builtin_aliases and them descriptions to the + // `commands` `BTreeMap`. + for command in &BUILTIN_ALIASES { + commands.insert( + command.0.to_string(), + CommandInfo::BuiltIn { + about: Some(command.2.to_string()), + }, + ); + } + + // Add the user-defined aliases + if let Ok(aliases) = config.get::<BTreeMap<String, StringOrVec>>("alias") { + for (name, target) in aliases.iter() { + commands.insert( + name.to_string(), + CommandInfo::Alias { + target: target.clone(), + }, + ); + } + } + + // `help` is special, so it needs to be inserted separately. + commands.insert( + "help".to_string(), + CommandInfo::BuiltIn { + about: Some("Displays help for a cargo subcommand".to_string()), + }, + ); + + commands +} + +fn find_external_subcommand(config: &Config, cmd: &str) -> Option<PathBuf> { + let command_exe = format!("cargo-{}{}", cmd, env::consts::EXE_SUFFIX); + search_directories(config) + .iter() + .map(|dir| dir.join(&command_exe)) + .find(|file| is_executable(file)) +} + +fn execute_external_subcommand(config: &Config, cmd: &str, args: &[&OsStr]) -> CliResult { + let path = find_external_subcommand(config, cmd); + let command = match path { + Some(command) => command, + None => { + let err = if cmd.starts_with('+') { + anyhow::format_err!( + "no such command: `{}`\n\n\t\ + Cargo does not handle `+toolchain` directives.\n\t\ + Did you mean to invoke `cargo` through `rustup` instead?", + cmd + ) + } else { + let suggestions = list_commands(config); + let did_you_mean = closest_msg(cmd, suggestions.keys(), |c| c); + + anyhow::format_err!( + "no such command: `{}`{}\n\n\t\ + View all installed commands with `cargo --list`", + cmd, + did_you_mean + ) + }; + + return Err(CliError::new(err, 101)); + } + }; + execute_subcommand(config, Some(&command), args) +} + +fn execute_internal_subcommand(config: &Config, args: &[&OsStr]) -> CliResult { + execute_subcommand(config, None, args) +} + +// This function is used to execute a subcommand. It is used to execute both +// internal and external subcommands. +// If `cmd_path` is `None`, then the subcommand is an internal subcommand. +fn execute_subcommand(config: &Config, cmd_path: Option<&PathBuf>, args: &[&OsStr]) -> CliResult { + let cargo_exe = config.cargo_exe()?; + let mut cmd = match cmd_path { + Some(cmd_path) => ProcessBuilder::new(cmd_path), + None => ProcessBuilder::new(&cargo_exe), + }; + cmd.env(cargo::CARGO_ENV, cargo_exe).args(args); + if let Some(client) = config.jobserver_from_env() { + cmd.inherit_jobserver(client); + } + let err = match cmd.exec_replace() { + Ok(()) => return Ok(()), + Err(e) => e, + }; + + if let Some(perr) = err.downcast_ref::<ProcessError>() { + if let Some(code) = perr.code { + return Err(CliError::code(code)); + } + } + Err(CliError::new(err, 101)) +} + +#[cfg(unix)] +fn is_executable<P: AsRef<Path>>(path: P) -> bool { + use std::os::unix::prelude::*; + fs::metadata(path) + .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) + .unwrap_or(false) +} +#[cfg(windows)] +fn is_executable<P: AsRef<Path>>(path: P) -> bool { + path.as_ref().is_file() +} + +fn search_directories(config: &Config) -> Vec<PathBuf> { + let mut path_dirs = if let Some(val) = config.get_env_os("PATH") { + env::split_paths(&val).collect() + } else { + vec![] + }; + + let home_bin = config.home().clone().into_path_unlocked().join("bin"); + + // If any of that PATH elements contains `home_bin`, do not + // add it again. This is so that the users can control priority + // of it using PATH, while preserving the historical + // behavior of preferring it over system global directories even + // when not in PATH at all. + // See https://github.com/rust-lang/cargo/issues/11020 for details. + // + // Note: `p == home_bin` will ignore trailing slash, but we don't + // `canonicalize` the paths. + if !path_dirs.iter().any(|p| p == &home_bin) { + path_dirs.insert(0, home_bin); + }; + + path_dirs +} + +/// Initialize libgit2. +fn init_git(config: &Config) { + // Disabling the owner validation in git can, in theory, lead to code execution + // vulnerabilities. However, libgit2 does not launch executables, which is the foundation of + // the original security issue. Meanwhile, issues with refusing to load git repos in + // `CARGO_HOME` for example will likely be very frustrating for users. So, we disable the + // validation. + // + // For further discussion of Cargo's current interactions with git, see + // + // https://github.com/rust-lang/rfcs/pull/3279 + // + // and in particular the subsection on "Git support". + // + // Note that we only disable this when Cargo is run as a binary. If Cargo is used as a library, + // this code won't be invoked. Instead, developers will need to explicitly disable the + // validation in their code. This is inconvenient, but won't accidentally open consuming + // applications up to security issues if they use git2 to open repositories elsewhere in their + // code. + unsafe { + git2::opts::set_verify_owner_validation(false) + .expect("set_verify_owner_validation should never fail"); + } + + init_git_transports(config); +} + +/// Configure libgit2 to use libcurl if necessary. +/// +/// If the user has a non-default network configuration, then libgit2 will be +/// configured to use libcurl instead of the built-in networking support so +/// that those configuration settings can be used. +fn init_git_transports(config: &Config) { + // Only use a custom transport if any HTTP options are specified, + // such as proxies or custom certificate authorities. The custom + // transport, however, is not as well battle-tested. + + match cargo::ops::needs_custom_http_transport(config) { + Ok(true) => {} + _ => return, + } + + let handle = match cargo::ops::http_handle(config) { + Ok(handle) => handle, + Err(..) => return, + }; + + // The unsafety of the registration function derives from two aspects: + // + // 1. This call must be synchronized with all other registration calls as + // well as construction of new transports. + // 2. The argument is leaked. + // + // We're clear on point (1) because this is only called at the start of this + // binary (we know what the state of the world looks like) and we're mostly + // clear on point (2) because we'd only free it after everything is done + // anyway + unsafe { + git2_curl::register(handle); + } +} |