Skip to content

Commit f3fee6d

Browse files
committed
Auto merge of #14141 - hi-rustin:rustin-patch-info, r=epage
feat: Add `info` cargo subcommand <!-- homu-ignore:start --> close #14081 close #948 fcp #14141 (comment) This PR added a new `info` cargo subcommand. # Background This adds a new subcommand to Cargo, `cargo info`. This subcommand would allow users to get information about a crate from the command line, without having to go to the web. The main motivation for this is to make it easier to get information about a crate from the command line. Currently, the way to get information about a crate is to go to the web and look it up on [crates.io] or find the crate's source code and look at the `Cargo.toml` file. This is not very convenient, especially not all information is displayed on the [crates.io] page. This command also has been requested by the community for a long time. You can find more discussion about this in [cargo#948]. Another motivation is to make the workflow of finding and evaluating crates more efficient. In the current workflow, users can search for crates using `cargo search`, but then they have to go to the web to get more information about the crate. This is not very efficient, especially if the user is just trying to get a quick overview of the crate. This would allow users to quickly get information about a crate without having to leave the terminal. [crates.io]: https://crates.io [cargo#948]: #948 Example usage: ```console ./target/debug/cargo info clap --verbose Credential cargo:token get crates-io clap #argument #cli #arg #parser #parse A simple to use, efficient, and full-featured Command Line Argument Parser version: 4.5.8 (latest 4.5.9) license: MIT OR Apache-2.0 rust-version: 1.74 documentation: https://docs.rs/clap/4.5.8 repository: https://github.com/clap-rs/clap crates.io: https://crates.io/crates/clap/4.5.8 features: +default = [std, color, help, usage, error-context, suggestions] color = [clap_builder/color] error-context = [clap_builder/error-context] help = [clap_builder/help] std = [clap_builder/std] suggestions = [clap_builder/suggestions] usage = [clap_builder/usage] cargo = [clap_builder/cargo] debug = [clap_builder/debug, clap_derive?/debug] deprecated = [clap_builder/deprecated, clap_derive?/deprecated] derive = [dep:clap_derive] env = [clap_builder/env] string = [clap_builder/string] unicode = [clap_builder/unicode] unstable-doc = [clap_builder/unstable-doc, derive] unstable-styles = [clap_builder/unstable-styles] unstable-v5 = [clap_builder/unstable-v5, clap_derive?/unstable-v5, deprecated] wrap_help = [clap_builder/wrap_help] dependencies: +clap_builder@=4.5.8 clap_derive@=4.5.8 owners: kbknapp (Kevin K.) github:rust-cli:maintainers (Maintainers) github:clap-rs:admins (Admins) note: to see how you depend on clap, run `cargo tree --invert --package [email protected]` ``` <img width="1425" alt="image" src="https://github.com/user-attachments/assets/e0813c45-624f-417c-a61d-eda03f9ab5ed"> *note:* this is showing the `--verbose` output to show every thing the user can possibly see. Normal operation does not include - dependencies ## Detailed design | Content | Explanation | Why | |----------------------------------------------------------------------------|-------------------------------------|-----------------------------------------------------------------------------------| | clap | Name | The basic information. | | #argument #cli #arg #parser #parse | Keywords (clickable) | It's more like a category, which you can use to search for relevant alternatives. | | A simple to use, efficient, and full-featured Command Line Argument Parser | Description | The basic information. | | version: 4.5.8 (latest 4.5.9) | Version | The basic information. | | license: MIT OR Apache-2.0 | License | When choosing a crate, it is crucial to consider the license. | | rust-version: 1.74 | MSRV | When choosing a crate, it is crucial to make sure it can work with your MSRV. | | documentation: <https://docs.rs/clap/4.5.8> | Documentation Link | Use these links can find more docs and information. | | repository: <https://github.com/clap-rs/clap> | Repo Link | Use these links can find more docs and information. | | crates.io: https://crates.io/crates/clap/4.5.8 | crates.io Link | Use these links can find more docs and information. | | features: | Default Features And Other Features | It helps for enabling features. | | dependencies: | All dependencies | It indicates what it depends on. | | owners: | Owners | It indicates who maintains the crate. | | note: to see how you depend on clap, run `cargo tree --invert --package [email protected]` | A note for cargo tree command | It will prompt the user that the package is depended on under the workspace, and the dependencies can be viewed using the cargo tree command. | ## Rendering features - For features enabled by users, a + prefix and colored output are now used for better visibility. - For features enabled automatically, colored output is used to distinguish them. - For disabled features, non-colored output is used to clearly indicate their status. ## Rendering deps Only show dependencies in verbose mode. - For dependencies required by the package, a + prefix and colored output are now used for better visibility. - For dependencies that are optional and activated, colored output is used to distinguish them. - For dependencies that are optional and not activated, non-colored output is used to clearly indicate their status. # Some important notes ## Downloading the crate from any Cargo compatible registry The `cargo info` command will download the crate from any Cargo compatible registry. It will then extract the information from the `Cargo.toml` file and display it in the terminal. If the crate is already in the local cache, it will not download the crate again. It will get the information from the local cache. ## Pick the correct version from the workspace When executed in a workspace directory, the cargo info command chooses the version that the workspace is currently using. If there's a lock file available, the version from this file will be used. In the absence of a lock file, the command attempts to select a version that is compatible with the Minimum Supported Rust Version (MSRV). And the lock file will be generated automatically. The following hierarchy is used to determine the MSRV: - First, the MSRV of the parent directory package is checked, if it exists. - If the parent directory package does not specify an MSRV, the minimal MSRV of the workspace is checked. - If neither the workspace nor the parent directory package specify an MSRV, the version of the current Rust compiler (rustc --version) is used. # Prior art ## NPM [npm] has a similar command called `npm info`. For example: ```console $ npm info lodash [email protected] | MIT | deps: none | versions: 114 Lodash modular utilities. https://lodash.com/ keywords: modules, stdlib, util dist .tarball: https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz .shasum: 679591c564c3bffaae8454cf0b3df370c3d6911c .integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== .unpackedSize: 1.4 MB maintainers: - mathias <[email protected]> - jdalton <[email protected]> - bnjmnt4n <[email protected]> dist-tags: latest: 4.17.21 published over a year ago by bnjmnt4n <[email protected]> ``` [npm]: https://www.npmjs.com/ ## Poetry [Poetry] has a similar command called `poetry show`. For example: ```console $ poetry show pendulum name : pendulum version : 1.4.2 description : Python datetimes made easy dependencies - python-dateutil >=2.6.1 - tzlocal >=1.4 - pytzdata >=2017.2.2 required by - calendar >=1.4.0 ``` [Poetry]: https://python-poetry.org/ # insta-stable As `@weihanglo` mentioned in #14141 (comment), commands that shadow third-party commands tend to be insta-stabilized to avoid an intermediate period where users can't access the third-party command (built-ins get priority) nor the built-in command (requires nightly) For the cargo-info command, there are two commands that this would shadow - [cargo-information](https://github.com/hi-rustin/cargo-information): hasn't been around too long and only has 4k downloads - [cargo-info](https://gitlab.com/imp/cargo-info) : been around longer and has 63k downloads We might be able to get away with having this unstable but starting from the assumption of insta-stabilization.
2 parents ec05ed9 + ba07215 commit f3fee6d

File tree

206 files changed

+5563
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

206 files changed

+5563
-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)