From f582df01c462b8476032c5fbb173bfa90550b8d3 Mon Sep 17 00:00:00 2001 From: hi-rustin Date: Tue, 25 Jun 2024 20:45:33 +0800 Subject: [PATCH] feat: Add `info` cargo subcommand --- src/bin/cargo/commands/info.rs | 38 +++ src/bin/cargo/commands/mod.rs | 3 + src/cargo/ops/cargo_info/mod.rs | 398 +++++++++++++++++++++++++ src/cargo/ops/cargo_info/view.rs | 489 +++++++++++++++++++++++++++++++ src/cargo/ops/mod.rs | 1 + 5 files changed, 929 insertions(+) create mode 100644 src/bin/cargo/commands/info.rs create mode 100644 src/cargo/ops/cargo_info/mod.rs create mode 100644 src/cargo/ops/cargo_info/view.rs diff --git a/src/bin/cargo/commands/info.rs b/src/bin/cargo/commands/info.rs new file mode 100644 index 000000000000..9aeb700c90d6 --- /dev/null +++ b/src/bin/cargo/commands/info.rs @@ -0,0 +1,38 @@ +use cargo::ops::cargo_info::info; +use cargo::util::command_prelude::*; +use cargo_util_schemas::core::PackageIdSpec; + +pub fn cli() -> Command { + Command::new("info") + .about("Display information about a package in the registry") + .arg( + Arg::new("package") + .required(true) + .value_name("SPEC") + .help_heading(heading::PACKAGE_SELECTION) + .help("Package to inspect"), + ) + .arg_index("Registry index URL to search packages in") + .arg_registry("Registry to search packages in") + .after_help(color_print::cstr!( + "Run `cargo help info` for more detailed information.\n" + )) +} + +pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { + let package = args + .get_one::("package") + .map(String::as_str) + .unwrap(); + let spec = PackageIdSpec::parse(package).map_err(|e| { + anyhow::format_err!( + "invalid package id specification `{}`: {}", + package, + e.to_string() + ) + })?; + + let reg_or_index = args.registry_or_index(gctx)?; + info(&spec, gctx, reg_or_index)?; + Ok(()) +} diff --git a/src/bin/cargo/commands/mod.rs b/src/bin/cargo/commands/mod.rs index 02c3438dc470..b507226f3a97 100644 --- a/src/bin/cargo/commands/mod.rs +++ b/src/bin/cargo/commands/mod.rs @@ -14,6 +14,7 @@ pub fn builtin() -> Vec { generate_lockfile::cli(), git_checkout::cli(), help::cli(), + info::cli(), init::cli(), install::cli(), locate_project::cli(), @@ -59,6 +60,7 @@ pub fn builtin_exec(cmd: &str) -> Option { "generate-lockfile" => generate_lockfile::exec, "git-checkout" => git_checkout::exec, "help" => help::exec, + "info" => info::exec, "init" => init::exec, "install" => install::exec, "locate-project" => locate_project::exec, @@ -102,6 +104,7 @@ pub mod fix; pub mod generate_lockfile; pub mod git_checkout; pub mod help; +pub mod info; pub mod init; pub mod install; pub mod locate_project; diff --git a/src/cargo/ops/cargo_info/mod.rs b/src/cargo/ops/cargo_info/mod.rs new file mode 100644 index 000000000000..7f1074ad4487 --- /dev/null +++ b/src/cargo/ops/cargo_info/mod.rs @@ -0,0 +1,398 @@ +mod view; + +use std::collections::HashSet; +use std::task::Poll; + +use anyhow::{bail, Context}; +use cargo_credential::Operation; +use cargo_util_schemas::core::{PackageIdSpec, PartialVersion}; +use crates_io::Registry as CratesIoRegistry; +use crates_io::User; + +use crate::core::registry::PackageRegistry; +use crate::core::{ + Dependency, Package, PackageId, PackageIdSpecQuery, Registry, SourceId, Workspace, +}; +use crate::ops::cargo_info::view::pretty_view; +use crate::ops::registry::RegistryOrIndex; +use crate::ops::resolve_ws; +use crate::sources::source::{QueryKind, Source}; +use crate::sources::{IndexSummary, RegistrySource, SourceConfigMap}; +use crate::util::auth::{auth_token, AuthorizationErrorReason}; +use crate::util::cache_lock::CacheLockMode; +use crate::util::command_prelude::root_manifest; +use crate::util::network::http::http_handle; +use crate::{CargoResult, GlobalContext}; + +pub fn info( + spec: &PackageIdSpec, + gctx: &GlobalContext, + reg_or_index: Option, +) -> CargoResult<()> { + let source_config = SourceConfigMap::new(gctx)?; + let mut registry = PackageRegistry::new_with_source_config(gctx, source_config)?; + // Make sure we get the lock before we download anything. + let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; + registry.lock_patches(); + + // If we can find it in workspace, use it as a specific version. + let nearest_manifest_path = root_manifest(None, gctx).ok(); + let ws = nearest_manifest_path + .as_ref() + .and_then(|root| Workspace::new(root, gctx).ok()); + let nearest_package = ws.as_ref().and_then(|ws| { + nearest_manifest_path + .as_ref() + .and_then(|path| ws.members().find(|p| p.manifest_path() == path)) + }); + let (mut package_id, is_member) = find_pkgid_in_ws(nearest_package, ws.as_ref(), spec); + let (use_package_source_id, source_ids) = get_source_id(gctx, reg_or_index, package_id)?; + // If we don't use the package's source, we need to query the package ID from the specified registry. + if !use_package_source_id { + package_id = None; + } + + validate_locked_and_frozen_options(package_id, gctx)?; + + let msrv_from_nearest_manifest_path_or_ws = + try_get_msrv_from_nearest_manifest_or_ws(nearest_package, ws.as_ref()); + // If the workspace does not have a specific Rust version, + // or if the command is not called within the workspace, then fallback to the global Rust version. + let rustc_version = match msrv_from_nearest_manifest_path_or_ws { + Some(msrv) => msrv, + None => { + let current_rustc = gctx.load_global_rustc(ws.as_ref())?.version; + // Remove any pre-release identifiers for easier comparison. + // Otherwise, the MSRV check will fail if the current Rust version is a nightly or beta version. + semver::Version::new( + current_rustc.major, + current_rustc.minor, + current_rustc.patch, + ) + .into() + } + }; + // Only suggest cargo tree command when the package is not a workspace member. + // For workspace members, `cargo tree --package --invert` is useless. It only prints itself. + let suggest_cargo_tree_command = package_id.is_some() && !is_member; + + let summaries = query_summaries(spec, &mut registry, &source_ids)?; + let package_id = match package_id { + Some(id) => id, + None => find_pkgid_in_summaries(&summaries, spec, &rustc_version, &source_ids)?, + }; + + let package = registry.get(&[package_id])?; + let package = package.get_one(package_id)?; + let owners = try_list_owners(gctx, source_ids, package_id.name().as_str())?; + pretty_view( + package, + &summaries, + &owners, + suggest_cargo_tree_command, + gctx, + )?; + + Ok(()) +} + +fn find_pkgid_in_ws( + nearest_package: Option<&Package>, + ws: Option<&Workspace<'_>>, + spec: &PackageIdSpec, +) -> (Option, bool) { + let Some(ws) = ws else { + return (None, false); + }; + + if let Some(member) = ws.members().find(|p| spec.matches(p.package_id())) { + return (Some(member.package_id()), true); + } + + let Ok((_, resolve)) = resolve_ws(ws, false) else { + return (None, false); + }; + + if let Some(package_id) = nearest_package + .map(|p| p.package_id()) + .into_iter() + .flat_map(|p| resolve.deps(p)) + .map(|(p, _)| p) + .filter(|&p| spec.matches(p)) + .max_by_key(|&p| p.version()) + { + return (Some(package_id), false); + } + + if let Some(package_id) = ws + .members() + .map(|p| p.package_id()) + .flat_map(|p| resolve.deps(p)) + .map(|(p, _)| p) + .filter(|&p| spec.matches(p)) + .max_by_key(|&p| p.version()) + { + return (Some(package_id), false); + } + + if let Some(package_id) = resolve + .iter() + .filter(|&p| spec.matches(p)) + .max_by_key(|&p| p.version()) + { + return (Some(package_id), false); + } + + (None, false) +} + +fn find_pkgid_in_summaries( + summaries: &[IndexSummary], + spec: &PackageIdSpec, + rustc_version: &PartialVersion, + source_ids: &RegistrySourceIds, +) -> CargoResult { + let summary = summaries + .iter() + .filter(|s| spec.matches(s.package_id())) + .max_by(|s1, s2| { + // Check the MSRV compatibility. + let s1_matches = s1 + .as_summary() + .rust_version() + .map(|v| v.is_compatible_with(rustc_version)) + .unwrap_or_else(|| false); + let s2_matches = s2 + .as_summary() + .rust_version() + .map(|v| v.is_compatible_with(rustc_version)) + .unwrap_or_else(|| false); + // MSRV compatible version is preferred. + match (s1_matches, s2_matches) { + (true, false) => std::cmp::Ordering::Greater, + (false, true) => std::cmp::Ordering::Less, + // If both summaries match the current Rust version or neither do, try to + // pick the latest version. + _ => s1.package_id().version().cmp(s2.package_id().version()), + } + }); + + match summary { + Some(summary) => Ok(summary.package_id()), + None => { + anyhow::bail!( + "could not find `{}` in registry `{}`", + spec, + source_ids.original.url() + ) + } + } +} + +fn query_summaries( + spec: &PackageIdSpec, + registry: &mut PackageRegistry<'_>, + source_ids: &RegistrySourceIds, +) -> CargoResult> { + // Query without version requirement to get all index summaries. + let dep = Dependency::parse(spec.name(), None, source_ids.original)?; + loop { + // Exact to avoid returning all for path/git + match registry.query_vec(&dep, QueryKind::Exact) { + std::task::Poll::Ready(res) => { + break res; + } + std::task::Poll::Pending => registry.block_until_ready()?, + } + } +} + +// Try to list the login and name of all owners of a crate. +fn try_list_owners( + gctx: &GlobalContext, + source_ids: RegistrySourceIds, + package_name: &str, +) -> CargoResult>> { + // Only remote registries support listing owners. + if !source_ids.original.is_remote_registry() { + return Ok(None); + } + let registry = api_registry(gctx, source_ids)?; + match registry { + Some(mut registry) => { + let owners = registry.list_owners(package_name)?; + let names = owners.iter().map(get_username).collect(); + Ok(Some(names)) + } + None => Ok(None), + } +} + +fn get_username(u: &User) -> String { + format!( + "{}{}", + u.login, + u.name + .as_ref() + .map(|name| format!(" ({})", name)) + .unwrap_or_default(), + ) +} + +struct RegistrySourceIds { + /// Use when looking up the auth token, or writing out `Cargo.lock` + original: SourceId, + /// Use when interacting with the source (querying / publishing , etc) + /// + /// The source for crates.io may be replaced by a built-in source for accessing crates.io with + /// the sparse protocol, or a source for the testing framework (when the replace_crates_io + /// function is used) + /// + /// User-defined source replacement is not applied. + /// Note: This will be utilized when interfacing with the registry API. + replacement: SourceId, +} + +fn get_source_id( + gctx: &GlobalContext, + reg_or_index: Option, + package_id: Option, +) -> CargoResult<(bool, RegistrySourceIds)> { + let (use_package_source_id, sid) = match (®_or_index, package_id) { + (None, Some(package_id)) => (true, package_id.source_id()), + (None, None) => (false, SourceId::crates_io(gctx)?), + (Some(RegistryOrIndex::Index(url)), None) => (false, SourceId::for_registry(url)?), + (Some(RegistryOrIndex::Registry(r)), None) => (false, SourceId::alt_registry(gctx, r)?), + (Some(reg_or_index), Some(package_id)) => { + let sid = match reg_or_index { + RegistryOrIndex::Index(url) => SourceId::for_registry(url)?, + RegistryOrIndex::Registry(r) => SourceId::alt_registry(gctx, r)?, + }; + let package_source_id = package_id.source_id(); + // Same registry, use the package's source. + if sid == package_source_id { + (true, sid) + } else { + let pkg_source_replacement_sid = SourceConfigMap::new(gctx)? + .load(package_source_id, &HashSet::new())? + .replaced_source_id(); + // Use the package's source if the specified registry is a replacement for the package's source. + if pkg_source_replacement_sid == sid { + (true, package_source_id) + } else { + (false, sid) + } + } + } + }; + // Load source replacements that are built-in to Cargo. + let builtin_replacement_sid = SourceConfigMap::empty(gctx)? + .load(sid, &HashSet::new())? + .replaced_source_id(); + let replacement_sid = SourceConfigMap::new(gctx)? + .load(sid, &HashSet::new())? + .replaced_source_id(); + // Check if the user has configured source-replacement for the registry we are querying. + if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid { + // Neither --registry nor --index was passed and the user has configured source-replacement. + if let Some(replacement_name) = replacement_sid.alt_registry_key() { + bail!("crates-io is replaced with remote registry {replacement_name};\ninclude `--registry {replacement_name}` or `--registry crates-io`"); + } else { + bail!("crates-io is replaced with non-remote-registry source {replacement_sid};\ninclude `--registry crates-io` to use crates.io"); + } + } else { + Ok(( + use_package_source_id, + RegistrySourceIds { + original: sid, + replacement: builtin_replacement_sid, + }, + )) + } +} + +// Try to get the crates.io registry which is used to access the registry API. +// If the user is not logged in, the function will return None. +fn api_registry( + gctx: &GlobalContext, + source_ids: RegistrySourceIds, +) -> CargoResult> { + let cfg = { + let mut src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), gctx)?; + let cfg = loop { + match src.config()? { + Poll::Pending => src + .block_until_ready() + .with_context(|| format!("failed to update {}", source_ids.replacement))?, + Poll::Ready(cfg) => break cfg, + } + }; + cfg.expect("remote registries must have config") + }; + // This should only happen if the user has a custom registry configured. + // Some registries may not have API support. + let api_host = match cfg.api { + Some(api_host) => api_host, + None => return Ok(None), + }; + let token = match auth_token( + gctx, + &source_ids.original, + None, + Operation::Read, + vec![], + false, + ) { + Ok(token) => Some(token), + Err(err) => { + // If the token is missing, it means the user is not logged in. + // We don't want to show an error in this case. + if err.to_string().contains( + (AuthorizationErrorReason::TokenMissing) + .to_string() + .as_str(), + ) { + return Ok(None); + } + return Err(err); + } + }; + + let handle = http_handle(gctx)?; + Ok(Some(CratesIoRegistry::new_handle( + api_host, + token, + handle, + cfg.auth_required, + ))) +} + +fn validate_locked_and_frozen_options( + package_id: Option, + gctx: &GlobalContext, +) -> Result<(), anyhow::Error> { + let from_workspace = package_id.is_some(); + // Only in workspace, we can use --frozen or --locked. + if !from_workspace { + if gctx.locked() { + bail!("the option `--locked` can only be used within a workspace"); + } + + if gctx.frozen() { + bail!("the option `--frozen` can only be used within a workspace"); + } + } + Ok(()) +} + +fn try_get_msrv_from_nearest_manifest_or_ws( + nearest_package: Option<&Package>, + ws: Option<&Workspace<'_>>, +) -> Option { + // Try to get the MSRV from the nearest manifest. + let rust_version = nearest_package.and_then(|p| p.rust_version().map(|v| v.as_partial())); + // If the nearest manifest does not have a specific Rust version, try to get it from the workspace. + rust_version + .or_else(|| ws.and_then(|ws| ws.rust_version().map(|v| v.as_partial()))) + .cloned() +} diff --git a/src/cargo/ops/cargo_info/view.rs b/src/cargo/ops/cargo_info/view.rs new file mode 100644 index 000000000000..06ecd5913e2d --- /dev/null +++ b/src/cargo/ops/cargo_info/view.rs @@ -0,0 +1,489 @@ +use std::collections::HashMap; +use std::io::Write; + +use crate::util::style::{ERROR, HEADER, LITERAL, NOP, NOTE, WARN}; +use crate::{ + core::{ + dependency::DepKind, shell::Verbosity, Dependency, FeatureMap, Package, PackageId, SourceId, + }, + sources::IndexSummary, + util::interning::InternedString, + CargoResult, GlobalContext, +}; + +// Pretty print the package information. +pub(super) fn pretty_view( + package: &Package, + summaries: &[IndexSummary], + owners: &Option>, + suggest_cargo_tree_command: bool, + gctx: &GlobalContext, +) -> CargoResult<()> { + let summary = package.manifest().summary(); + let package_id = summary.package_id(); + let metadata = package.manifest().metadata(); + let is_package_from_crates_io = summary.source_id().is_crates_io(); + let header = HEADER; + let error = ERROR; + let warn = WARN; + let note = NOTE; + + let mut shell = gctx.shell(); + let verbosity = shell.verbosity(); + write!(shell.out(), "{header}{}{header:#}", package_id.name())?; + if !metadata.keywords.is_empty() { + let message = if is_package_from_crates_io { + metadata + .keywords + .iter() + .map(|keyword| { + let link = shell.out_hyperlink(format!("https://crates.io/keywords/{keyword}")); + format!("{link}#{keyword}{link:#}") + }) + .collect::>() + .join(" ") + } else { + format!("#{}", metadata.keywords.join(" #")) + }; + write!(shell.out(), " {note}{message}{note:#}")?; + } + + let stdout = shell.out(); + writeln!(stdout)?; + if let Some(ref description) = metadata.description { + writeln!(stdout, "{}", description.trim_end())?; + } + write!( + stdout, + "{header}version:{header:#} {}", + package_id.version() + )?; + // Add a warning message to stdout if the following conditions are met: + // 1. The package version is not the latest available version. + // 2. The package source is not crates.io. + match ( + summaries.iter().max_by_key(|s| s.as_summary().version()), + is_package_from_crates_io, + ) { + (Some(latest), false) if latest.as_summary().version() != package_id.version() => { + write!( + stdout, + " {warn}(latest {} {warn:#}{note}from {}{note:#}{warn}){warn:#}", + latest.as_summary().version(), + pretty_source(summary.source_id(), gctx) + )?; + } + (Some(latest), true) if latest.as_summary().version() != package_id.version() => { + write!( + stdout, + " {warn}(latest {}){warn:#}", + latest.as_summary().version(), + )?; + } + (_, false) => { + write!( + stdout, + " {note}(from {}){note:#}", + pretty_source(summary.source_id(), gctx) + )?; + } + (_, true) => {} + } + writeln!(stdout)?; + writeln!( + stdout, + "{header}license:{header:#} {}", + metadata + .license + .clone() + .unwrap_or_else(|| format!("{error}unknown{error:#}")) + )?; + // TODO: color MSRV as a warning if newer than either the "workspace" MSRV or `rustc --version` + writeln!( + stdout, + "{header}rust-version:{header:#} {}", + metadata + .rust_version + .as_ref() + .map(|v| v.to_string()) + .unwrap_or_else(|| format!("{warn}unknown{warn:#}")) + )?; + if let Some(ref link) = metadata.documentation.clone().or_else(|| { + is_package_from_crates_io.then(|| { + format!( + "https://docs.rs/{name}/{version}", + name = package_id.name(), + version = package_id.version() + ) + }) + }) { + writeln!(stdout, "{header}documentation:{header:#} {link}")?; + } + if let Some(ref link) = metadata.homepage { + writeln!(stdout, "{header}homepage:{header:#} {link}")?; + } + if let Some(ref link) = metadata.repository { + writeln!(stdout, "{header}repository:{header:#} {link}")?; + } + // Only print the crates.io link if the package is from crates.io. + if is_package_from_crates_io { + writeln!( + stdout, + "{header}crates.io:{header:#} https://crates.io/crates/{}/{}", + package_id.name(), + package_id.version() + )?; + } + + let activated = &[InternedString::new("default")]; + let resolved_features = resolve_features(activated, summary.features()); + pretty_features( + resolved_features.clone(), + summary.features(), + verbosity, + stdout, + )?; + + pretty_deps( + package, + &resolved_features, + summary.features(), + verbosity, + stdout, + gctx, + )?; + + if let Some(owners) = owners { + pretty_owners(owners, stdout)?; + } + + if suggest_cargo_tree_command { + suggest_cargo_tree(package_id, stdout)?; + } + + Ok(()) +} + +fn pretty_source(source: SourceId, ctx: &GlobalContext) -> String { + if let Some(relpath) = source + .local_path() + .and_then(|path| pathdiff::diff_paths(path, ctx.cwd())) + { + let path = std::path::Path::new(".").join(relpath); + path.display().to_string() + } else { + source.to_string() + } +} + +fn pretty_deps( + package: &Package, + resolved_features: &[(InternedString, FeatureStatus)], + features: &FeatureMap, + verbosity: Verbosity, + stdout: &mut dyn Write, + gctx: &GlobalContext, +) -> CargoResult<()> { + match verbosity { + Verbosity::Quiet | Verbosity::Normal => { + return Ok(()); + } + Verbosity::Verbose => {} + } + + let header = HEADER; + + let dependencies = package + .dependencies() + .iter() + .filter(|d| d.kind() == DepKind::Normal) + .collect::>(); + if !dependencies.is_empty() { + writeln!(stdout, "{header}dependencies:{header:#}")?; + print_deps(dependencies, resolved_features, features, stdout, gctx)?; + } + + let build_dependencies = package + .dependencies() + .iter() + .filter(|d| d.kind() == DepKind::Build) + .collect::>(); + if !build_dependencies.is_empty() { + writeln!(stdout, "{header}build-dependencies:{header:#}")?; + print_deps( + build_dependencies, + resolved_features, + features, + stdout, + gctx, + )?; + } + + Ok(()) +} + +fn print_deps( + dependencies: Vec<&Dependency>, + resolved_features: &[(InternedString, FeatureStatus)], + features: &FeatureMap, + stdout: &mut dyn Write, + gctx: &GlobalContext, +) -> Result<(), anyhow::Error> { + let enabled_by_user = HEADER; + let enabled = NOP; + let disabled = anstyle::Style::new() | anstyle::Effects::DIMMED; + + let mut dependencies = dependencies + .into_iter() + .map(|dependency| { + let status = if !dependency.is_optional() { + FeatureStatus::EnabledByUser + } else if resolved_features + .iter() + .filter(|(_, s)| !s.is_disabled()) + .filter_map(|(n, _)| features.get(n)) + .flatten() + .filter_map(|f| match f { + crate::core::FeatureValue::Feature(_) => None, + crate::core::FeatureValue::Dep { dep_name } => Some(dep_name), + crate::core::FeatureValue::DepFeature { dep_name, weak, .. } if *weak => { + Some(dep_name) + } + crate::core::FeatureValue::DepFeature { .. } => None, + }) + .any(|dep_name| *dep_name == dependency.name_in_toml()) + { + FeatureStatus::Enabled + } else { + FeatureStatus::Disabled + }; + (dependency, status) + }) + .collect::>(); + dependencies.sort_by_key(|(d, s)| (*s, d.package_name())); + for (dependency, status) in dependencies { + // 1. Only print the version requirement if it is a registry dependency. + // 2. Only print the source if it is not a registry dependency. + // For example: `bar (./crates/bar)` or `bar@=1.2.3`. + let (req, source) = if dependency.source_id().is_registry() { + ( + format!("@{}", pretty_req(dependency.version_req())), + String::new(), + ) + } else { + ( + String::new(), + format!(" ({})", pretty_source(dependency.source_id(), gctx)), + ) + }; + + if status == FeatureStatus::EnabledByUser { + write!(stdout, " {enabled_by_user}+{enabled_by_user:#}")?; + } else { + write!(stdout, " ")?; + } + let style = match status { + FeatureStatus::EnabledByUser | FeatureStatus::Enabled => enabled, + FeatureStatus::Disabled => disabled, + }; + writeln!( + stdout, + "{style}{}{}{}{style:#}", + dependency.package_name(), + req, + source + )?; + } + Ok(()) +} + +fn pretty_req(req: &crate::util::OptVersionReq) -> String { + let mut rendered = req.to_string(); + let strip_prefix = match req { + crate::util::OptVersionReq::Any => false, + crate::util::OptVersionReq::Req(req) + | crate::util::OptVersionReq::Locked(_, req) + | crate::util::OptVersionReq::Precise(_, req) => { + req.comparators.len() == 1 && rendered.starts_with('^') + } + }; + if strip_prefix { + rendered.remove(0); + rendered + } else { + rendered + } +} + +fn pretty_features( + resolved_features: Vec<(InternedString, FeatureStatus)>, + features: &FeatureMap, + verbosity: Verbosity, + stdout: &mut dyn Write, +) -> CargoResult<()> { + let header = HEADER; + let enabled_by_user = HEADER; + let enabled = NOP; + let disabled = anstyle::Style::new() | anstyle::Effects::DIMMED; + let summary = anstyle::Style::new() | anstyle::Effects::ITALIC; + + // If there are no features, return early. + let margin = features + .iter() + .map(|(name, _)| name.len()) + .max() + .unwrap_or_default(); + if margin == 0 { + return Ok(()); + } + + writeln!(stdout, "{header}features:{header:#}")?; + + const MAX_FEATURE_PRINTS: usize = 30; + let total_activated = resolved_features + .iter() + .filter(|(_, s)| !s.is_disabled()) + .count(); + let total_deactivated = resolved_features + .iter() + .filter(|(_, s)| s.is_disabled()) + .count(); + let show_all = match verbosity { + Verbosity::Quiet | Verbosity::Normal => false, + Verbosity::Verbose => true, + }; + let show_activated = total_activated <= MAX_FEATURE_PRINTS || show_all; + let show_deactivated = (total_activated + total_deactivated) <= MAX_FEATURE_PRINTS || show_all; + for (current, status, current_activated) in resolved_features + .iter() + .map(|(n, s)| (n, s, features.get(n).unwrap())) + { + if !status.is_disabled() && !show_activated { + continue; + } + if status.is_disabled() && !show_deactivated { + continue; + } + if *status == FeatureStatus::EnabledByUser { + write!(stdout, " {enabled_by_user}+{enabled_by_user:#}")?; + } else { + write!(stdout, " ")?; + } + let style = match status { + FeatureStatus::EnabledByUser | FeatureStatus::Enabled => enabled, + FeatureStatus::Disabled => disabled, + }; + writeln!( + stdout, + "{style}{current: >() + .join(", ") + )?; + } + if !show_activated { + writeln!( + stdout, + " {summary}{total_activated} activated features{summary:#}", + )?; + } + if !show_deactivated { + writeln!( + stdout, + " {summary}{total_deactivated} deactivated features{summary:#}", + )?; + } + + Ok(()) +} + +fn pretty_owners(owners: &Vec, stdout: &mut dyn Write) -> CargoResult<()> { + let header = HEADER; + + if !owners.is_empty() { + writeln!(stdout, "{header}owners:{header:#}",)?; + for owner in owners { + writeln!(stdout, " {}", owner)?; + } + } + + Ok(()) +} + +// Suggest the cargo tree command to view the dependency tree. +fn suggest_cargo_tree(package_id: PackageId, stdout: &mut dyn Write) -> CargoResult<()> { + let literal = LITERAL; + + note(format_args!( + "to see how you depend on {name}, run `{literal}cargo tree --invert --package {name}@{version}{literal:#}`", + name = package_id.name(), + version = package_id.version(), + ), stdout) +} + +pub(super) fn note(msg: impl std::fmt::Display, stdout: &mut dyn Write) -> CargoResult<()> { + let note = NOTE; + let bold = anstyle::Style::new() | anstyle::Effects::BOLD; + + writeln!(stdout, "{note}note{note:#}{bold}:{bold:#} {msg}",)?; + + Ok(()) +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum FeatureStatus { + EnabledByUser, + Enabled, + Disabled, +} + +impl FeatureStatus { + fn is_disabled(&self) -> bool { + *self == FeatureStatus::Disabled + } +} + +fn resolve_features( + explicit: &[InternedString], + features: &FeatureMap, +) -> Vec<(InternedString, FeatureStatus)> { + let mut resolved = features + .keys() + .cloned() + .map(|n| { + if explicit.contains(&n) { + (n, FeatureStatus::EnabledByUser) + } else { + (n, FeatureStatus::Disabled) + } + }) + .collect::>(); + + let mut activated_queue = explicit.to_vec(); + + while let Some(current) = activated_queue.pop() { + let Some(current_activated) = features.get(¤t) else { + // `default` isn't always present + continue; + }; + for activated in current_activated.iter().rev().filter_map(|f| match f { + crate::core::FeatureValue::Feature(name) => Some(name), + crate::core::FeatureValue::Dep { .. } + | crate::core::FeatureValue::DepFeature { .. } => None, + }) { + let Some(status) = resolved.get_mut(activated) else { + continue; + }; + if status.is_disabled() { + *status = FeatureStatus::Enabled; + activated_queue.push(*activated); + } + } + } + + let mut resolved: Vec<_> = resolved.into_iter().collect(); + resolved.sort_by_key(|(name, status)| (*status, *name)); + resolved +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 49dfaf53fc87..f3b303d8e208 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -46,6 +46,7 @@ pub(crate) mod cargo_compile; pub mod cargo_config; mod cargo_doc; mod cargo_fetch; +pub mod cargo_info; mod cargo_install; mod cargo_new; mod cargo_output_metadata;