Skip to content

Commit c76c8fd

Browse files
committed
feat: Add info cargo subcommand
1 parent 5ff9328 commit c76c8fd

File tree

6 files changed

+870
-1
lines changed

6 files changed

+870
-1
lines changed

src/bin/cargo/commands/info.rs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use anyhow::Context;
2+
use cargo::ops::info;
3+
use cargo::util::command_prelude::*;
4+
use cargo_util_schemas::core::PackageIdSpec;
5+
6+
pub fn cli() -> Command {
7+
Command::new("info")
8+
.about("Display information about a package in the registry")
9+
.arg(
10+
Arg::new("package")
11+
.required(true)
12+
.value_name("SPEC")
13+
.help_heading(heading::PACKAGE_SELECTION)
14+
.help("Package to inspect"),
15+
)
16+
.arg_index("Registry index URL to search packages in")
17+
.arg_registry("Registry to search packages in")
18+
.arg_silent_suggestion()
19+
.after_help(color_print::cstr!(
20+
"Run `<cyan,bold>cargo help info</>` for more detailed information.\n"
21+
))
22+
}
23+
24+
pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
25+
let package = args
26+
.get_one::<String>("package")
27+
.map(String::as_str)
28+
.unwrap();
29+
let spec = PackageIdSpec::parse(package)
30+
.with_context(|| format!("invalid package ID specification: `{package}`"))?;
31+
32+
let reg_or_index = args.registry_or_index(gctx)?;
33+
info(&spec, gctx, reg_or_index)?;
34+
Ok(())
35+
}

src/bin/cargo/commands/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub fn builtin() -> Vec<Command> {
1414
generate_lockfile::cli(),
1515
git_checkout::cli(),
1616
help::cli(),
17+
info::cli(),
1718
init::cli(),
1819
install::cli(),
1920
locate_project::cli(),
@@ -59,6 +60,7 @@ pub fn builtin_exec(cmd: &str) -> Option<Exec> {
5960
"generate-lockfile" => generate_lockfile::exec,
6061
"git-checkout" => git_checkout::exec,
6162
"help" => help::exec,
63+
"info" => info::exec,
6264
"init" => init::exec,
6365
"install" => install::exec,
6466
"locate-project" => locate_project::exec,
@@ -102,6 +104,7 @@ pub mod fix;
102104
pub mod generate_lockfile;
103105
pub mod git_checkout;
104106
pub mod help;
107+
pub mod info;
105108
pub mod init;
106109
pub mod install;
107110
pub mod locate_project;

src/cargo/ops/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub use self::cargo_update::write_manifest_upgrades;
2424
pub use self::cargo_update::UpdateOptions;
2525
pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions};
2626
pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile};
27+
pub use self::registry::info;
2728
pub use self::registry::modify_owners;
2829
pub use self::registry::publish;
2930
pub use self::registry::registry_login;

src/cargo/ops/registry/info/mod.rs

+287
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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

Comments
 (0)