|
| 1 | +//! Implementation of `cargo info`. |
| 2 | +
|
| 3 | +use anyhow::bail; |
| 4 | +use cargo_credential::Operation; |
| 5 | +use cargo_util_schemas::core::{PackageIdSpec, PartialVersion}; |
| 6 | +use crates_io::User; |
| 7 | + |
| 8 | +use crate::core::registry::PackageRegistry; |
| 9 | +use crate::core::{Dependency, Package, PackageId, PackageIdSpecQuery, Registry, Workspace}; |
| 10 | +use crate::ops::registry::info::view::pretty_view; |
| 11 | +use crate::ops::registry::{get_source_id_with_package_id, RegistryOrIndex, RegistrySourceIds}; |
| 12 | +use crate::ops::resolve_ws; |
| 13 | +use crate::sources::source::QueryKind; |
| 14 | +use crate::sources::{IndexSummary, SourceConfigMap}; |
| 15 | +use crate::util::auth::AuthorizationErrorReason; |
| 16 | +use crate::util::cache_lock::CacheLockMode; |
| 17 | +use crate::util::command_prelude::root_manifest; |
| 18 | +use crate::{CargoResult, GlobalContext}; |
| 19 | + |
| 20 | +mod view; |
| 21 | + |
| 22 | +pub fn info( |
| 23 | + spec: &PackageIdSpec, |
| 24 | + gctx: &GlobalContext, |
| 25 | + reg_or_index: Option<RegistryOrIndex>, |
| 26 | +) -> CargoResult<()> { |
| 27 | + let source_config = SourceConfigMap::new(gctx)?; |
| 28 | + let mut registry = PackageRegistry::new_with_source_config(gctx, source_config)?; |
| 29 | + // Make sure we get the lock before we download anything. |
| 30 | + let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; |
| 31 | + registry.lock_patches(); |
| 32 | + |
| 33 | + // If we can find it in workspace, use it as a specific version. |
| 34 | + let nearest_manifest_path = root_manifest(None, gctx).ok(); |
| 35 | + let ws = nearest_manifest_path |
| 36 | + .as_ref() |
| 37 | + .and_then(|root| Workspace::new(root, gctx).ok()); |
| 38 | + validate_locked_and_frozen_options(ws.is_some(), gctx)?; |
| 39 | + let nearest_package = ws.as_ref().and_then(|ws| { |
| 40 | + nearest_manifest_path |
| 41 | + .as_ref() |
| 42 | + .and_then(|path| ws.members().find(|p| p.manifest_path() == path)) |
| 43 | + }); |
| 44 | + let (mut package_id, is_member) = find_pkgid_in_ws(nearest_package, ws.as_ref(), spec); |
| 45 | + let (use_package_source_id, source_ids) = |
| 46 | + get_source_id_with_package_id(gctx, package_id, reg_or_index.as_ref())?; |
| 47 | + // If we don't use the package's source, we need to query the package ID from the specified registry. |
| 48 | + if !use_package_source_id { |
| 49 | + package_id = None; |
| 50 | + } |
| 51 | + |
| 52 | + let msrv_from_nearest_manifest_path_or_ws = |
| 53 | + try_get_msrv_from_nearest_manifest_or_ws(nearest_package, ws.as_ref()); |
| 54 | + // If the workspace does not have a specific Rust version, |
| 55 | + // or if the command is not called within the workspace, then fallback to the global Rust version. |
| 56 | + let rustc_version = match msrv_from_nearest_manifest_path_or_ws { |
| 57 | + Some(msrv) => msrv, |
| 58 | + None => { |
| 59 | + let current_rustc = gctx.load_global_rustc(ws.as_ref())?.version; |
| 60 | + // Remove any pre-release identifiers for easier comparison. |
| 61 | + // Otherwise, the MSRV check will fail if the current Rust version is a nightly or beta version. |
| 62 | + semver::Version::new( |
| 63 | + current_rustc.major, |
| 64 | + current_rustc.minor, |
| 65 | + current_rustc.patch, |
| 66 | + ) |
| 67 | + .into() |
| 68 | + } |
| 69 | + }; |
| 70 | + // Only suggest cargo tree command when the package is not a workspace member. |
| 71 | + // For workspace members, `cargo tree --package <SPEC> --invert` is useless. It only prints itself. |
| 72 | + let suggest_cargo_tree_command = package_id.is_some() && !is_member; |
| 73 | + |
| 74 | + let summaries = query_summaries(spec, &mut registry, &source_ids)?; |
| 75 | + let package_id = match package_id { |
| 76 | + Some(id) => id, |
| 77 | + None => find_pkgid_in_summaries(&summaries, spec, &rustc_version, &source_ids)?, |
| 78 | + }; |
| 79 | + |
| 80 | + let package = registry.get(&[package_id])?; |
| 81 | + let package = package.get_one(package_id)?; |
| 82 | + let owners = try_list_owners( |
| 83 | + gctx, |
| 84 | + &source_ids, |
| 85 | + reg_or_index.as_ref(), |
| 86 | + package_id.name().as_str(), |
| 87 | + )?; |
| 88 | + pretty_view( |
| 89 | + package, |
| 90 | + &summaries, |
| 91 | + &owners, |
| 92 | + suggest_cargo_tree_command, |
| 93 | + gctx, |
| 94 | + )?; |
| 95 | + |
| 96 | + Ok(()) |
| 97 | +} |
| 98 | + |
| 99 | +fn find_pkgid_in_ws( |
| 100 | + nearest_package: Option<&Package>, |
| 101 | + ws: Option<&Workspace<'_>>, |
| 102 | + spec: &PackageIdSpec, |
| 103 | +) -> (Option<PackageId>, bool) { |
| 104 | + let Some(ws) = ws else { |
| 105 | + return (None, false); |
| 106 | + }; |
| 107 | + |
| 108 | + if let Some(member) = ws.members().find(|p| spec.matches(p.package_id())) { |
| 109 | + return (Some(member.package_id()), true); |
| 110 | + } |
| 111 | + |
| 112 | + let Ok((_, resolve)) = resolve_ws(ws, false) else { |
| 113 | + return (None, false); |
| 114 | + }; |
| 115 | + |
| 116 | + if let Some(package_id) = nearest_package |
| 117 | + .map(|p| p.package_id()) |
| 118 | + .into_iter() |
| 119 | + .flat_map(|p| resolve.deps(p)) |
| 120 | + .map(|(p, _)| p) |
| 121 | + .filter(|&p| spec.matches(p)) |
| 122 | + .max_by_key(|&p| p.version()) |
| 123 | + { |
| 124 | + return (Some(package_id), false); |
| 125 | + } |
| 126 | + |
| 127 | + if let Some(package_id) = ws |
| 128 | + .members() |
| 129 | + .map(|p| p.package_id()) |
| 130 | + .flat_map(|p| resolve.deps(p)) |
| 131 | + .map(|(p, _)| p) |
| 132 | + .filter(|&p| spec.matches(p)) |
| 133 | + .max_by_key(|&p| p.version()) |
| 134 | + { |
| 135 | + return (Some(package_id), false); |
| 136 | + } |
| 137 | + |
| 138 | + if let Some(package_id) = resolve |
| 139 | + .iter() |
| 140 | + .filter(|&p| spec.matches(p)) |
| 141 | + .max_by_key(|&p| p.version()) |
| 142 | + { |
| 143 | + return (Some(package_id), false); |
| 144 | + } |
| 145 | + |
| 146 | + (None, false) |
| 147 | +} |
| 148 | + |
| 149 | +fn find_pkgid_in_summaries( |
| 150 | + summaries: &[IndexSummary], |
| 151 | + spec: &PackageIdSpec, |
| 152 | + rustc_version: &PartialVersion, |
| 153 | + source_ids: &RegistrySourceIds, |
| 154 | +) -> CargoResult<PackageId> { |
| 155 | + let summary = summaries |
| 156 | + .iter() |
| 157 | + .filter(|s| spec.matches(s.package_id())) |
| 158 | + .max_by(|s1, s2| { |
| 159 | + // Check the MSRV compatibility. |
| 160 | + let s1_matches = s1 |
| 161 | + .as_summary() |
| 162 | + .rust_version() |
| 163 | + .map(|v| v.is_compatible_with(rustc_version)) |
| 164 | + .unwrap_or_else(|| false); |
| 165 | + let s2_matches = s2 |
| 166 | + .as_summary() |
| 167 | + .rust_version() |
| 168 | + .map(|v| v.is_compatible_with(rustc_version)) |
| 169 | + .unwrap_or_else(|| false); |
| 170 | + // MSRV compatible version is preferred. |
| 171 | + match (s1_matches, s2_matches) { |
| 172 | + (true, false) => std::cmp::Ordering::Greater, |
| 173 | + (false, true) => std::cmp::Ordering::Less, |
| 174 | + // If both summaries match the current Rust version or neither do, try to |
| 175 | + // pick the latest version. |
| 176 | + _ => s1.package_id().version().cmp(s2.package_id().version()), |
| 177 | + } |
| 178 | + }); |
| 179 | + |
| 180 | + match summary { |
| 181 | + Some(summary) => Ok(summary.package_id()), |
| 182 | + None => { |
| 183 | + anyhow::bail!( |
| 184 | + "could not find `{}` in registry `{}`", |
| 185 | + spec, |
| 186 | + source_ids.original.url() |
| 187 | + ) |
| 188 | + } |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +fn query_summaries( |
| 193 | + spec: &PackageIdSpec, |
| 194 | + registry: &mut PackageRegistry<'_>, |
| 195 | + source_ids: &RegistrySourceIds, |
| 196 | +) -> CargoResult<Vec<IndexSummary>> { |
| 197 | + // Query without version requirement to get all index summaries. |
| 198 | + let dep = Dependency::parse(spec.name(), None, source_ids.original)?; |
| 199 | + loop { |
| 200 | + // Exact to avoid returning all for path/git |
| 201 | + match registry.query_vec(&dep, QueryKind::Exact) { |
| 202 | + std::task::Poll::Ready(res) => { |
| 203 | + break res; |
| 204 | + } |
| 205 | + std::task::Poll::Pending => registry.block_until_ready()?, |
| 206 | + } |
| 207 | + } |
| 208 | +} |
| 209 | + |
| 210 | +// Try to list the login and name of all owners of a crate. |
| 211 | +fn try_list_owners( |
| 212 | + gctx: &GlobalContext, |
| 213 | + source_ids: &RegistrySourceIds, |
| 214 | + reg_or_index: Option<&RegistryOrIndex>, |
| 215 | + package_name: &str, |
| 216 | +) -> CargoResult<Option<Vec<String>>> { |
| 217 | + // Only remote registries support listing owners. |
| 218 | + if !source_ids.original.is_remote_registry() { |
| 219 | + return Ok(None); |
| 220 | + } |
| 221 | + match super::registry( |
| 222 | + gctx, |
| 223 | + source_ids, |
| 224 | + None, |
| 225 | + reg_or_index, |
| 226 | + false, |
| 227 | + Some(Operation::Read), |
| 228 | + ) { |
| 229 | + Ok(mut registry) => { |
| 230 | + let owners = registry.list_owners(package_name)?; |
| 231 | + let names = owners.iter().map(get_username).collect(); |
| 232 | + return Ok(Some(names)); |
| 233 | + } |
| 234 | + Err(err) => { |
| 235 | + // If the token is missing, it means the user is not logged in. |
| 236 | + // We don't want to show an error in this case. |
| 237 | + if err.to_string().contains( |
| 238 | + (AuthorizationErrorReason::TokenMissing) |
| 239 | + .to_string() |
| 240 | + .as_str(), |
| 241 | + ) { |
| 242 | + return Ok(None); |
| 243 | + } |
| 244 | + return Err(err); |
| 245 | + } |
| 246 | + } |
| 247 | +} |
| 248 | + |
| 249 | +fn get_username(u: &User) -> String { |
| 250 | + format!( |
| 251 | + "{}{}", |
| 252 | + u.login, |
| 253 | + u.name |
| 254 | + .as_ref() |
| 255 | + .map(|name| format!(" ({})", name)) |
| 256 | + .unwrap_or_default(), |
| 257 | + ) |
| 258 | +} |
| 259 | + |
| 260 | +fn validate_locked_and_frozen_options( |
| 261 | + in_workspace: bool, |
| 262 | + gctx: &GlobalContext, |
| 263 | +) -> Result<(), anyhow::Error> { |
| 264 | + // Only in workspace, we can use --frozen or --locked. |
| 265 | + if !in_workspace { |
| 266 | + if gctx.locked() { |
| 267 | + bail!("the option `--locked` can only be used within a workspace"); |
| 268 | + } |
| 269 | + |
| 270 | + if gctx.frozen() { |
| 271 | + bail!("the option `--frozen` can only be used within a workspace"); |
| 272 | + } |
| 273 | + } |
| 274 | + Ok(()) |
| 275 | +} |
| 276 | + |
| 277 | +fn try_get_msrv_from_nearest_manifest_or_ws( |
| 278 | + nearest_package: Option<&Package>, |
| 279 | + ws: Option<&Workspace<'_>>, |
| 280 | +) -> Option<PartialVersion> { |
| 281 | + // Try to get the MSRV from the nearest manifest. |
| 282 | + let rust_version = nearest_package.and_then(|p| p.rust_version().map(|v| v.as_partial())); |
| 283 | + // If the nearest manifest does not have a specific Rust version, try to get it from the workspace. |
| 284 | + rust_version |
| 285 | + .or_else(|| ws.and_then(|ws| ws.rust_version().map(|v| v.as_partial()))) |
| 286 | + .cloned() |
| 287 | +} |
0 commit comments