mirror of
https://github.com/rust-lang/cargo.git
synced 2025-10-01 11:30:39 +00:00
feat: Add info
cargo subcommand
This commit is contained in:
parent
5ff9328cb4
commit
c76c8fd9aa
35
src/bin/cargo/commands/info.rs
Normal file
35
src/bin/cargo/commands/info.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use cargo::ops::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)
|
||||||
|
.with_context(|| format!("invalid package ID specification: `{package}`"))?;
|
||||||
|
|
||||||
|
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(),
|
generate_lockfile::cli(),
|
||||||
git_checkout::cli(),
|
git_checkout::cli(),
|
||||||
help::cli(),
|
help::cli(),
|
||||||
|
info::cli(),
|
||||||
init::cli(),
|
init::cli(),
|
||||||
install::cli(),
|
install::cli(),
|
||||||
locate_project::cli(),
|
locate_project::cli(),
|
||||||
@ -59,6 +60,7 @@ pub fn builtin_exec(cmd: &str) -> Option<Exec> {
|
|||||||
"generate-lockfile" => generate_lockfile::exec,
|
"generate-lockfile" => generate_lockfile::exec,
|
||||||
"git-checkout" => git_checkout::exec,
|
"git-checkout" => git_checkout::exec,
|
||||||
"help" => help::exec,
|
"help" => help::exec,
|
||||||
|
"info" => info::exec,
|
||||||
"init" => init::exec,
|
"init" => init::exec,
|
||||||
"install" => install::exec,
|
"install" => install::exec,
|
||||||
"locate-project" => locate_project::exec,
|
"locate-project" => locate_project::exec,
|
||||||
@ -102,6 +104,7 @@ pub mod fix;
|
|||||||
pub mod generate_lockfile;
|
pub mod generate_lockfile;
|
||||||
pub mod git_checkout;
|
pub mod git_checkout;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
|
pub mod info;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod install;
|
pub mod install;
|
||||||
pub mod locate_project;
|
pub mod locate_project;
|
||||||
|
@ -24,6 +24,7 @@ pub use self::cargo_update::write_manifest_upgrades;
|
|||||||
pub use self::cargo_update::UpdateOptions;
|
pub use self::cargo_update::UpdateOptions;
|
||||||
pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions};
|
pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions};
|
||||||
pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile};
|
pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile};
|
||||||
|
pub use self::registry::info;
|
||||||
pub use self::registry::modify_owners;
|
pub use self::registry::modify_owners;
|
||||||
pub use self::registry::publish;
|
pub use self::registry::publish;
|
||||||
pub use self::registry::registry_login;
|
pub use self::registry::registry_login;
|
||||||
|
287
src/cargo/ops/registry/info/mod.rs
Normal file
287
src/cargo/ops/registry/info/mod.rs
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
//! Implementation of `cargo info`.
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use cargo_credential::Operation;
|
||||||
|
use cargo_util_schemas::core::{PackageIdSpec, PartialVersion};
|
||||||
|
use crates_io::User;
|
||||||
|
|
||||||
|
use crate::core::registry::PackageRegistry;
|
||||||
|
use crate::core::{Dependency, Package, PackageId, PackageIdSpecQuery, Registry, Workspace};
|
||||||
|
use crate::ops::registry::info::view::pretty_view;
|
||||||
|
use crate::ops::registry::{get_source_id_with_package_id, RegistryOrIndex, RegistrySourceIds};
|
||||||
|
use crate::ops::resolve_ws;
|
||||||
|
use crate::sources::source::QueryKind;
|
||||||
|
use crate::sources::{IndexSummary, SourceConfigMap};
|
||||||
|
use crate::util::auth::AuthorizationErrorReason;
|
||||||
|
use crate::util::cache_lock::CacheLockMode;
|
||||||
|
use crate::util::command_prelude::root_manifest;
|
||||||
|
use crate::{CargoResult, GlobalContext};
|
||||||
|
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
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());
|
||||||
|
validate_locked_and_frozen_options(ws.is_some(), gctx)?;
|
||||||
|
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_with_package_id(gctx, package_id, reg_or_index.as_ref())?;
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
reg_or_index.as_ref(),
|
||||||
|
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,
|
||||||
|
reg_or_index: Option<&RegistryOrIndex>,
|
||||||
|
package_name: &str,
|
||||||
|
) -> CargoResult<Option<Vec<String>>> {
|
||||||
|
// Only remote registries support listing owners.
|
||||||
|
if !source_ids.original.is_remote_registry() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
match super::registry(
|
||||||
|
gctx,
|
||||||
|
source_ids,
|
||||||
|
None,
|
||||||
|
reg_or_index,
|
||||||
|
false,
|
||||||
|
Some(Operation::Read),
|
||||||
|
) {
|
||||||
|
Ok(mut registry) => {
|
||||||
|
let owners = registry.list_owners(package_name)?;
|
||||||
|
let names = owners.iter().map(get_username).collect();
|
||||||
|
return Ok(Some(names));
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_username(u: &User) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
u.login,
|
||||||
|
u.name
|
||||||
|
.as_ref()
|
||||||
|
.map(|name| format!(" ({})", name))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_locked_and_frozen_options(
|
||||||
|
in_workspace: bool,
|
||||||
|
gctx: &GlobalContext,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
// Only in workspace, we can use --frozen or --locked.
|
||||||
|
if !in_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/registry/info/view.rs
Normal file
489
src/cargo/ops/registry/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
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html
|
//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html
|
||||||
|
|
||||||
|
mod info;
|
||||||
mod login;
|
mod login;
|
||||||
mod logout;
|
mod logout;
|
||||||
mod owner;
|
mod owner;
|
||||||
@ -18,7 +19,7 @@ use cargo_credential::{Operation, Secret};
|
|||||||
use crates_io::Registry;
|
use crates_io::Registry;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::core::SourceId;
|
use crate::core::{PackageId, SourceId};
|
||||||
use crate::sources::source::Source;
|
use crate::sources::source::Source;
|
||||||
use crate::sources::{RegistrySource, SourceConfigMap};
|
use crate::sources::{RegistrySource, SourceConfigMap};
|
||||||
use crate::util::auth;
|
use crate::util::auth;
|
||||||
@ -27,6 +28,7 @@ use crate::util::context::{GlobalContext, PathAndArgs};
|
|||||||
use crate::util::errors::CargoResult;
|
use crate::util::errors::CargoResult;
|
||||||
use crate::util::network::http::http_handle;
|
use crate::util::network::http::http_handle;
|
||||||
|
|
||||||
|
pub use self::info::info;
|
||||||
pub use self::login::registry_login;
|
pub use self::login::registry_login;
|
||||||
pub use self::logout::registry_logout;
|
pub use self::logout::registry_logout;
|
||||||
pub use self::owner::modify_owners;
|
pub use self::owner::modify_owners;
|
||||||
@ -206,6 +208,47 @@ fn get_source_id(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Very similar to [`get_source_id`], but is used when the `package_id` is known.
|
||||||
|
fn get_source_id_with_package_id(
|
||||||
|
gctx: &GlobalContext,
|
||||||
|
package_id: Option<PackageId>,
|
||||||
|
reg_or_index: Option<&RegistryOrIndex>,
|
||||||
|
) -> 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 = get_initial_source_id_from_registry_or_index(gctx, reg_or_index)?;
|
||||||
|
let package_source_id = package_id.source_id();
|
||||||
|
// 1. Same registry, use the package's source.
|
||||||
|
// 2. Use the package's source if the specified registry is a replacement for the package's source.
|
||||||
|
if sid == package_source_id
|
||||||
|
|| is_replacement_for_package_source(gctx, sid, package_source_id)?
|
||||||
|
{
|
||||||
|
(true, package_source_id)
|
||||||
|
} else {
|
||||||
|
(false, sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (builtin_replacement_sid, replacement_sid) = get_replacement_source_ids(gctx, sid)?;
|
||||||
|
|
||||||
|
if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid {
|
||||||
|
bail!(gen_replacement_error(replacement_sid));
|
||||||
|
} else {
|
||||||
|
Ok((
|
||||||
|
use_package_source_id,
|
||||||
|
RegistrySourceIds {
|
||||||
|
original: sid,
|
||||||
|
replacement: builtin_replacement_sid,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_initial_source_id(
|
fn get_initial_source_id(
|
||||||
gctx: &GlobalContext,
|
gctx: &GlobalContext,
|
||||||
reg_or_index: Option<&RegistryOrIndex>,
|
reg_or_index: Option<&RegistryOrIndex>,
|
||||||
@ -239,6 +282,17 @@ fn get_replacement_source_ids(
|
|||||||
Ok((builtin_replacement_sid, replacement_sid))
|
Ok((builtin_replacement_sid, replacement_sid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_replacement_for_package_source(
|
||||||
|
gctx: &GlobalContext,
|
||||||
|
sid: SourceId,
|
||||||
|
package_source_id: SourceId,
|
||||||
|
) -> CargoResult<bool> {
|
||||||
|
let pkg_source_replacement_sid = SourceConfigMap::new(gctx)?
|
||||||
|
.load(package_source_id, &HashSet::new())?
|
||||||
|
.replaced_source_id();
|
||||||
|
Ok(pkg_source_replacement_sid == sid)
|
||||||
|
}
|
||||||
|
|
||||||
fn gen_replacement_error(replacement_sid: SourceId) -> String {
|
fn gen_replacement_error(replacement_sid: SourceId) -> String {
|
||||||
// Neither --registry nor --index was passed and the user has configured source-replacement.
|
// Neither --registry nor --index was passed and the user has configured source-replacement.
|
||||||
let error_message = if let Some(replacement_name) = replacement_sid.alt_registry_key() {
|
let error_message = if let Some(replacement_name) = replacement_sid.alt_registry_key() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user