Skip to main content

cargo/ops/registry/info/
mod.rs

1//! Implementation of `cargo info`.
2
3use anyhow::bail;
4use cargo_util_schemas::core::{PackageIdSpec, PartialVersion};
5
6use crate::core::Summary;
7use crate::core::registry::PackageRegistry;
8use crate::core::{Dependency, Package, PackageId, PackageIdSpecQuery, Registry, Workspace};
9use crate::ops::registry::info::view::pretty_view;
10use crate::ops::registry::{RegistryOrIndex, RegistrySourceIds, get_source_id_with_package_id};
11use crate::ops::resolve_ws;
12use crate::sources::IndexSummary;
13use crate::sources::SourceConfigMap;
14use crate::sources::source::QueryKind;
15use crate::util::cache_lock::CacheLockMode;
16use crate::util::command_prelude::root_manifest;
17use crate::{CargoResult, GlobalContext};
18
19mod view;
20
21pub fn info(
22    spec: &PackageIdSpec,
23    gctx: &GlobalContext,
24    reg_or_index: Option<RegistryOrIndex>,
25    explicit_registry: bool,
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
46    // If a local package exists and no explicit registry/index was provided,
47    // prefer the local package over the default registry
48    let reg_or_index_to_use = if package_id.is_some() && !explicit_registry {
49        None
50    } else {
51        reg_or_index.as_ref()
52    };
53
54    let (use_package_source_id, source_ids) =
55        get_source_id_with_package_id(gctx, package_id, reg_or_index_to_use)?;
56    // If we don't use the package's source, we need to query the package ID from the specified registry.
57    if !use_package_source_id {
58        package_id = None;
59    }
60
61    let msrv_from_nearest_manifest_path_or_ws =
62        try_get_msrv_from_nearest_manifest_or_ws(nearest_package, ws.as_ref());
63    // If the workspace does not have a specific Rust version,
64    // or if the command is not called within the workspace, then fallback to the global Rust version.
65    let rustc_version = match msrv_from_nearest_manifest_path_or_ws {
66        Some(msrv) => msrv,
67        None => {
68            let current_rustc = gctx.load_global_rustc(ws.as_ref())?.version;
69            // Remove any pre-release identifiers for easier comparison.
70            // Otherwise, the MSRV check will fail if the current Rust version is a nightly or beta version.
71            semver::Version::new(
72                current_rustc.major,
73                current_rustc.minor,
74                current_rustc.patch,
75            )
76            .into()
77        }
78    };
79    // Only suggest cargo tree command when the package is not a workspace member.
80    // For workspace members, `cargo tree --package <SPEC> --invert` is useless. It only prints itself.
81    let suggest_cargo_tree_command = package_id.is_some() && !is_member;
82
83    let (summaries, normalized_name) = query_summaries(spec, &mut registry, &source_ids)?;
84    let normalized_spec = match normalized_name {
85        Some(name) if name != spec.name() => {
86            let mut normalized_spec = PackageIdSpec::new(name);
87
88            if let Some(version) = spec.partial_version().cloned() {
89                normalized_spec = normalized_spec.with_version(version);
90            }
91
92            if let Some(url) = spec.url().cloned() {
93                normalized_spec = normalized_spec.with_url(url);
94            }
95
96            if let Some(kind) = spec.kind().cloned() {
97                normalized_spec = normalized_spec.with_kind(kind);
98            }
99
100            normalized_spec
101        }
102        _ => spec.clone(),
103    };
104    let package_id = match package_id {
105        Some(id) => id,
106        None => find_pkgid_in_summaries(&summaries, &normalized_spec, &rustc_version, &source_ids)?,
107    };
108
109    if package_id.name() != spec.name() {
110        gctx.shell().warn(format!(
111            "translating `{}` to `{}`",
112            spec.name(),
113            package_id.name(),
114        ))?;
115    }
116
117    let package = registry.get(&[package_id])?;
118    let package = package.get_one(package_id)?;
119    pretty_view(package, &summaries, suggest_cargo_tree_command, gctx)?;
120
121    Ok(())
122}
123
124fn find_pkgid_in_ws(
125    nearest_package: Option<&Package>,
126    ws: Option<&Workspace<'_>>,
127    spec: &PackageIdSpec,
128) -> (Option<PackageId>, bool) {
129    let Some(ws) = ws else {
130        return (None, false);
131    };
132
133    if let Some(member) = ws.members().find(|p| spec.matches(p.package_id())) {
134        return (Some(member.package_id()), true);
135    }
136
137    let Ok((_, resolve)) = resolve_ws(ws, false) else {
138        return (None, false);
139    };
140
141    if let Some(package_id) = nearest_package
142        .map(|p| p.package_id())
143        .into_iter()
144        .flat_map(|p| resolve.deps(p))
145        .map(|(p, _)| p)
146        .filter(|&p| spec.matches(p))
147        .max_by_key(|&p| p.version())
148    {
149        return (Some(package_id), false);
150    }
151
152    if let Some(package_id) = ws
153        .members()
154        .map(|p| p.package_id())
155        .flat_map(|p| resolve.deps(p))
156        .map(|(p, _)| p)
157        .filter(|&p| spec.matches(p))
158        .max_by_key(|&p| p.version())
159    {
160        return (Some(package_id), false);
161    }
162
163    if let Some(package_id) = resolve
164        .iter()
165        .filter(|&p| spec.matches(p))
166        .max_by_key(|&p| p.version())
167    {
168        return (Some(package_id), false);
169    }
170
171    (None, false)
172}
173
174fn find_pkgid_in_summaries(
175    summaries: &[Summary],
176    normalized_spec: &PackageIdSpec,
177    rustc_version: &PartialVersion,
178    source_ids: &RegistrySourceIds,
179) -> CargoResult<PackageId> {
180    let summary = summaries
181        .iter()
182        .filter(|s| normalized_spec.matches(s.package_id()))
183        .max_by(|s1, s2| {
184            // Check the MSRV compatibility.
185            let s1_matches = s1
186                .rust_version()
187                .map(|v| v.is_compatible_with(rustc_version))
188                .unwrap_or_else(|| false);
189            let s2_matches = s2
190                .rust_version()
191                .map(|v| v.is_compatible_with(rustc_version))
192                .unwrap_or_else(|| false);
193            // MSRV compatible version is preferred.
194            match (s1_matches, s2_matches) {
195                (true, false) => std::cmp::Ordering::Greater,
196                (false, true) => std::cmp::Ordering::Less,
197                // If both summaries match the current Rust version or neither do, try to
198                // pick the latest version.
199                _ => s1.package_id().version().cmp(s2.package_id().version()),
200            }
201        });
202
203    match summary {
204        Some(summary) => Ok(summary.package_id()),
205        None => {
206            anyhow::bail!(
207                "could not find `{}` in registry `{}`",
208                normalized_spec,
209                source_ids.original.url()
210            )
211        }
212    }
213}
214
215fn query_summaries(
216    spec: &PackageIdSpec,
217    registry: &mut PackageRegistry<'_>,
218    source_ids: &RegistrySourceIds,
219) -> CargoResult<(Vec<Summary>, Option<String>)> {
220    // Query without version requirement to get all index summaries.
221    let dep = Dependency::parse(spec.name(), None, source_ids.original)?;
222    // Use normalized crate name lookup for user-provided package names.
223    let results: Vec<_> = crate::util::block_on(registry.query_vec(&dep, QueryKind::Normalized))?
224        .into_iter()
225        .filter_map(|s| match s {
226            IndexSummary::Candidate(s) => Some(s),
227            _ => None,
228        })
229        .collect();
230
231    let normalized_name = results.first().map(|s| s.package_id().name().to_string());
232
233    Ok((results, normalized_name))
234}
235
236fn validate_locked_and_frozen_options(
237    in_workspace: bool,
238    gctx: &GlobalContext,
239) -> Result<(), anyhow::Error> {
240    // Only in workspace, we can use --frozen or --locked.
241    if !in_workspace {
242        if let Some(locked_flag) = gctx.locked_flag() {
243            bail!("the option `{locked_flag}` can only be used within a workspace");
244        }
245    }
246    Ok(())
247}
248
249fn try_get_msrv_from_nearest_manifest_or_ws(
250    nearest_package: Option<&Package>,
251    ws: Option<&Workspace<'_>>,
252) -> Option<PartialVersion> {
253    // Try to get the MSRV from the nearest manifest.
254    let rust_version = nearest_package.and_then(|p| p.rust_version().map(|v| v.to_partial()));
255    // If the nearest manifest does not have a specific Rust version, try to get it from the workspace.
256    rust_version.or_else(|| ws.and_then(|ws| ws.lowest_rust_version().map(|v| v.to_partial())))
257}