Compare commits

...

5 Commits

Author SHA1 Message Date
二手掉包工程师
4f6f3542db
Merge 476969f3cf into 9441b91186 2024-06-28 15:48:06 +02:00
bors
9441b91186 Auto merge of #14159 - dieterplex:migrate-git-snapbox, r=weihanglo
test: Migrate git to snapbox

Part of #14039.

There is a case need to modify regex for file size redaction.
2024-06-28 13:05:10 +00:00
d1t2
32cdb261ef
test: Migrate git to snapbox 2024-06-28 17:39:24 +08:00
d1t2
ed027736e7
test: Allow redact file size w/o fraction
`cargo clean` shows file size without fraction in summary when the size
is lower than 1024. And we need to avoid matching things like `%2B%23..`
found in other test cases, the trailing `\s` is added to regex.
2024-06-27 16:58:52 +08:00
hi-rustin
476969f3cf
feat: Add info cargo subcommand 2024-06-26 20:02:59 +08:00
7 changed files with 1523 additions and 537 deletions

View File

@ -171,7 +171,7 @@ fn add_common_redactions(subs: &mut snapbox::Redactions) {
.unwrap();
subs.insert(
"[FILE_SIZE]",
regex!(r"(?<redacted>[0-9]+(\.[0-9]+)([a-zA-Z]i)?)B"),
regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?([a-zA-Z]i)?)B\s"),
)
.unwrap();
subs.insert(

View File

@ -0,0 +1,39 @@
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")
.arg_silent_suggestion()
.after_help(color_print::cstr!(
"Run `<cyan,bold>cargo help info</>` for more detailed information.\n"
))
}
pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
let package = args
.get_one::<String>("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(())
}

View File

@ -14,6 +14,7 @@ pub fn builtin() -> Vec<Command> {
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<Exec> {
"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;

View File

@ -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<RegistryOrIndex>,
) -> 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 <SPEC> --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<PackageId>, 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<PackageId> {
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<Vec<IndexSummary>> {
// 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<Option<Vec<String>>> {
// 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<RegistryOrIndex>,
package_id: Option<PackageId>,
) -> CargoResult<(bool, RegistrySourceIds)> {
let (use_package_source_id, sid) = match (&reg_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<Option<CratesIoRegistry>> {
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<PackageId>,
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<PartialVersion> {
// 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()
}

View File

@ -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<Vec<String>>,
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::<Vec<_>>()
.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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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: <margin$}{style:#} = [{features}]",
features = current_activated
.iter()
.map(|s| format!("{style}{s}{style:#}"))
.collect::<Vec<String>>()
.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<String>, 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::<HashMap<_, _>>();
let mut activated_queue = explicit.to_vec();
while let Some(current) = activated_queue.pop() {
let Some(current_activated) = features.get(&current) 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
}

View File

@ -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;

File diff suppressed because it is too large Load Diff