Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add info cargo subcommand #14141

Merged
merged 4 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/bin/cargo/commands/info.rs
Original file line number Diff line number Diff line change
@@ -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 {
Rustin170506 marked this conversation as resolved.
Show resolved Hide resolved
Command::new("info")
epage marked this conversation as resolved.
Show resolved Hide resolved
.about("Display information about a package in the registry")
.arg(
Arg::new("package")
Rustin170506 marked this conversation as resolved.
Show resolved Hide resolved
.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(())
}
3 changes: 3 additions & 0 deletions src/bin/cargo/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/cargo/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub use self::cargo_update::write_manifest_upgrades;
pub use self::cargo_update::UpdateOptions;
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::registry::info;
pub use self::registry::modify_owners;
pub use self::registry::publish;
pub use self::registry::registry_login;
Expand Down
287 changes: 287 additions & 0 deletions src/cargo/ops/registry/info/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
//! Implementation of `cargo info`.

use anyhow::bail;
Rustin170506 marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that we handle duplicate package by max_by regardless MSRV of globally maximum. Should we have something reminding user when a query is ambiguous?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should mention this detail here. From the start, I provided all the details about how we select a package, but Ed thought it was too detailed. He believes it might create more work if we want to change these implementation details.

.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()
}
Loading