mirror of
https://github.com/rust-lang/cargo
synced 2024-06-30 23:14:41 +00:00
Compare commits
5 Commits
6596cbf833
...
4f6f3542db
Author | SHA1 | Date | |
---|---|---|---|
|
4f6f3542db | ||
|
9441b91186 | ||
|
32cdb261ef | ||
|
ed027736e7 | ||
|
476969f3cf |
|
@ -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(
|
||||
|
|
39
src/bin/cargo/commands/info.rs
Normal file
39
src/bin/cargo/commands/info.rs
Normal 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(())
|
||||
}
|
|
@ -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;
|
||||
|
|
398
src/cargo/ops/cargo_info/mod.rs
Normal file
398
src/cargo/ops/cargo_info/mod.rs
Normal 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 (®_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()
|
||||
}
|
489
src/cargo/ops/cargo_info/view.rs
Normal file
489
src/cargo/ops/cargo_info/view.rs
Normal 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(¤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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue
Block a user