use std::collections::HashSet;
use std::task::Poll;
use anyhow::{bail, Context as _};
use cargo::core::registry::PackageRegistry;
use cargo::core::PackageIdSpecQuery;
use cargo::core::{Dependency, Package, PackageId, PackageIdSpec, Registry, SourceId, Workspace};
use cargo::ops::RegistryOrIndex;
use cargo::sources::source::{QueryKind, Source};
use cargo::sources::IndexSummary;
use cargo::sources::{RegistrySource, SourceConfigMap};
use cargo::util::auth::{auth_token, AuthorizationErrorReason};
use cargo::util::cache_lock::CacheLockMode;
use cargo::util::command_prelude::root_manifest;
use cargo::util::network::http::http_handle;
use cargo::{ops, CargoResult, GlobalContext};
use cargo_credential::Operation;
use cargo_util_schemas::core::PartialVersion;
use crates_io::Registry as CratesIoRegistry;
use crates_io::User;
use super::view::pretty_view;
pub fn info(
spec: &PackageIdSpec,
gctx: &GlobalContext,
reg_or_index: Option<RegistryOrIndex>,
) -> CargoResult<()> {
let mut registry = PackageRegistry::new(gctx)?;
let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
registry.lock_patches();
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 !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());
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;
semver::Version::new(
current_rustc.major,
current_rustc.minor,
current_rustc.patch,
)
.into()
}
};
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<&cargo::core::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)) = ops::resolve_ws(ws) 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| {
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);
match (s1_matches, s2_matches) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => 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>> {
let dep = Dependency::parse(spec.name(), None, source_ids.original)?;
loop {
match registry.query_vec(&dep, QueryKind::Exact) {
std::task::Poll::Ready(res) => {
break res;
}
std::task::Poll::Pending => registry.block_until_ready()?,
}
}
}
fn try_list_owners(
gctx: &GlobalContext,
source_ids: RegistrySourceIds,
package_name: &str,
) -> CargoResult<Option<Vec<String>>> {
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 {
original: SourceId,
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();
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();
if pkg_source_replacement_sid == sid {
(true, package_source_id)
} else {
(false, sid)
}
}
}
};
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();
if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid {
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,
},
))
}
}
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")
};
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 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();
if !from_workspace {
if gctx.locked() {
anyhow::bail!("the option `--locked` can only be used within a workspace");
}
if gctx.frozen() {
anyhow::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> {
let rust_version = nearest_package.and_then(|p| p.rust_version().map(|v| v.as_partial()));
rust_version
.or_else(|| ws.and_then(|ws| ws.rust_version().map(|v| v.as_partial())))
.cloned()
}