cargo/ops/registry/info/
view.rs

1use std::collections::HashMap;
2use std::io::Write;
3
4use crate::core::Shell;
5use crate::util::style::{ERROR, HEADER, LITERAL, NOP, NOTE, WARN};
6use crate::{
7    core::{
8        dependency::DepKind, shell::Verbosity, Dependency, FeatureMap, Package, PackageId, SourceId,
9    },
10    sources::IndexSummary,
11    util::interning::InternedString,
12    CargoResult, GlobalContext,
13};
14
15// Pretty print the package information.
16pub(super) fn pretty_view(
17    package: &Package,
18    summaries: &[IndexSummary],
19    suggest_cargo_tree_command: bool,
20    gctx: &GlobalContext,
21) -> CargoResult<()> {
22    let summary = package.manifest().summary();
23    let package_id = summary.package_id();
24    let metadata = package.manifest().metadata();
25    let is_package_from_crates_io = summary.source_id().is_crates_io();
26    let header = HEADER;
27    let error = ERROR;
28    let warn = WARN;
29    let note = NOTE;
30
31    let mut shell = gctx.shell();
32    let verbosity = shell.verbosity();
33    write!(shell.out(), "{header}{}{header:#}", package_id.name())?;
34    if !metadata.keywords.is_empty() {
35        let message = if is_package_from_crates_io {
36            metadata
37                .keywords
38                .iter()
39                .map(|keyword| {
40                    let link = shell.out_hyperlink(format!("https://crates.io/keywords/{keyword}"));
41                    format!("{link}#{keyword}{link:#}")
42                })
43                .collect::<Vec<_>>()
44                .join(" ")
45        } else {
46            format!("#{}", metadata.keywords.join(" #"))
47        };
48        write!(shell.out(), " {note}{message}{note:#}")?;
49    }
50
51    let stdout = shell.out();
52    writeln!(stdout)?;
53    if let Some(ref description) = metadata.description {
54        writeln!(stdout, "{}", description.trim_end())?;
55    }
56    write!(
57        stdout,
58        "{header}version:{header:#} {}",
59        package_id.version()
60    )?;
61    // Add a warning message to stdout if the following conditions are met:
62    // 1. The package version is not the latest available version.
63    // 2. The package source is not crates.io.
64    match (
65        summaries.iter().max_by_key(|s| s.as_summary().version()),
66        is_package_from_crates_io,
67    ) {
68        (Some(latest), false) if latest.as_summary().version() != package_id.version() => {
69            write!(
70                stdout,
71                " {warn}(latest {} {warn:#}{note}from {}{note:#}{warn}){warn:#}",
72                latest.as_summary().version(),
73                pretty_source(summary.source_id(), gctx)
74            )?;
75        }
76        (Some(latest), true) if latest.as_summary().version() != package_id.version() => {
77            write!(
78                stdout,
79                " {warn}(latest {}){warn:#}",
80                latest.as_summary().version(),
81            )?;
82        }
83        (_, false) => {
84            write!(
85                stdout,
86                " {note}(from {}){note:#}",
87                pretty_source(summary.source_id(), gctx)
88            )?;
89        }
90        (_, true) => {}
91    }
92    writeln!(stdout)?;
93    writeln!(
94        stdout,
95        "{header}license:{header:#} {}",
96        metadata
97            .license
98            .clone()
99            .unwrap_or_else(|| format!("{error}unknown{error:#}"))
100    )?;
101    // TODO: color MSRV as a warning if newer than either the "workspace" MSRV or `rustc --version`
102    writeln!(
103        stdout,
104        "{header}rust-version:{header:#} {}",
105        metadata
106            .rust_version
107            .as_ref()
108            .map(|v| v.to_string())
109            .unwrap_or_else(|| format!("{warn}unknown{warn:#}"))
110    )?;
111    if let Some(ref link) = metadata.documentation.clone().or_else(|| {
112        is_package_from_crates_io.then(|| {
113            format!(
114                "https://docs.rs/{name}/{version}",
115                name = package_id.name(),
116                version = package_id.version()
117            )
118        })
119    }) {
120        writeln!(stdout, "{header}documentation:{header:#} {link}")?;
121    }
122    if let Some(ref link) = metadata.homepage {
123        writeln!(stdout, "{header}homepage:{header:#} {link}")?;
124    }
125    if let Some(ref link) = metadata.repository {
126        writeln!(stdout, "{header}repository:{header:#} {link}")?;
127    }
128    // Only print the crates.io link if the package is from crates.io.
129    if is_package_from_crates_io {
130        writeln!(
131            stdout,
132            "{header}crates.io:{header:#} https://crates.io/crates/{}/{}",
133            package_id.name(),
134            package_id.version()
135        )?;
136    }
137
138    let activated = &[InternedString::new("default")];
139    let resolved_features = resolve_features(activated, summary.features());
140    pretty_features(
141        resolved_features.clone(),
142        summary.features(),
143        verbosity,
144        stdout,
145    )?;
146
147    pretty_deps(
148        package,
149        &resolved_features,
150        summary.features(),
151        verbosity,
152        stdout,
153        gctx,
154    )?;
155
156    if suggest_cargo_tree_command {
157        suggest_cargo_tree(package_id, &mut shell)?;
158    }
159
160    Ok(())
161}
162
163fn pretty_source(source: SourceId, ctx: &GlobalContext) -> String {
164    if let Some(relpath) = source
165        .local_path()
166        .and_then(|path| pathdiff::diff_paths(path, ctx.cwd()))
167    {
168        let path = std::path::Path::new(".").join(relpath);
169        path.display().to_string()
170    } else {
171        source.to_string()
172    }
173}
174
175fn pretty_deps(
176    package: &Package,
177    resolved_features: &[(InternedString, FeatureStatus)],
178    features: &FeatureMap,
179    verbosity: Verbosity,
180    stdout: &mut dyn Write,
181    gctx: &GlobalContext,
182) -> CargoResult<()> {
183    match verbosity {
184        Verbosity::Quiet | Verbosity::Normal => {
185            return Ok(());
186        }
187        Verbosity::Verbose => {}
188    }
189
190    let header = HEADER;
191
192    let dependencies = package
193        .dependencies()
194        .iter()
195        .filter(|d| d.kind() == DepKind::Normal)
196        .collect::<Vec<_>>();
197    if !dependencies.is_empty() {
198        writeln!(stdout, "{header}dependencies:{header:#}")?;
199        print_deps(dependencies, resolved_features, features, stdout, gctx)?;
200    }
201
202    let build_dependencies = package
203        .dependencies()
204        .iter()
205        .filter(|d| d.kind() == DepKind::Build)
206        .collect::<Vec<_>>();
207    if !build_dependencies.is_empty() {
208        writeln!(stdout, "{header}build-dependencies:{header:#}")?;
209        print_deps(
210            build_dependencies,
211            resolved_features,
212            features,
213            stdout,
214            gctx,
215        )?;
216    }
217
218    Ok(())
219}
220
221fn print_deps(
222    dependencies: Vec<&Dependency>,
223    resolved_features: &[(InternedString, FeatureStatus)],
224    features: &FeatureMap,
225    stdout: &mut dyn Write,
226    gctx: &GlobalContext,
227) -> Result<(), anyhow::Error> {
228    let enabled_by_user = HEADER;
229    let enabled = NOP;
230    let disabled = anstyle::Style::new() | anstyle::Effects::DIMMED;
231
232    let mut dependencies = dependencies
233        .into_iter()
234        .map(|dependency| {
235            let status = if !dependency.is_optional() {
236                FeatureStatus::EnabledByUser
237            } else if resolved_features
238                .iter()
239                .filter(|(_, s)| !s.is_disabled())
240                .filter_map(|(n, _)| features.get(n))
241                .flatten()
242                .filter_map(|f| match f {
243                    crate::core::FeatureValue::Feature(_) => None,
244                    crate::core::FeatureValue::Dep { dep_name } => Some(dep_name),
245                    crate::core::FeatureValue::DepFeature { dep_name, weak, .. } if *weak => {
246                        Some(dep_name)
247                    }
248                    crate::core::FeatureValue::DepFeature { .. } => None,
249                })
250                .any(|dep_name| *dep_name == dependency.name_in_toml())
251            {
252                FeatureStatus::Enabled
253            } else {
254                FeatureStatus::Disabled
255            };
256            (dependency, status)
257        })
258        .collect::<Vec<_>>();
259    dependencies.sort_by_key(|(d, s)| (*s, d.package_name()));
260    for (dependency, status) in dependencies {
261        // 1. Only print the version requirement if it is a registry dependency.
262        // 2. Only print the source if it is not a registry dependency.
263        // For example: `bar (./crates/bar)` or `bar@=1.2.3`.
264        let (req, source) = if dependency.source_id().is_registry() {
265            (
266                format!("@{}", pretty_req(dependency.version_req())),
267                String::new(),
268            )
269        } else {
270            (
271                String::new(),
272                format!(" ({})", pretty_source(dependency.source_id(), gctx)),
273            )
274        };
275
276        if status == FeatureStatus::EnabledByUser {
277            write!(stdout, " {enabled_by_user}+{enabled_by_user:#}")?;
278        } else {
279            write!(stdout, "  ")?;
280        }
281        let style = match status {
282            FeatureStatus::EnabledByUser | FeatureStatus::Enabled => enabled,
283            FeatureStatus::Disabled => disabled,
284        };
285        writeln!(
286            stdout,
287            "{style}{}{}{}{style:#}",
288            dependency.package_name(),
289            req,
290            source
291        )?;
292    }
293    Ok(())
294}
295
296fn pretty_req(req: &crate::util::OptVersionReq) -> String {
297    let mut rendered = req.to_string();
298    let strip_prefix = match req {
299        crate::util::OptVersionReq::Any => false,
300        crate::util::OptVersionReq::Req(req)
301        | crate::util::OptVersionReq::Locked(_, req)
302        | crate::util::OptVersionReq::Precise(_, req) => {
303            req.comparators.len() == 1 && rendered.starts_with('^')
304        }
305    };
306    if strip_prefix {
307        rendered.remove(0);
308        rendered
309    } else {
310        rendered
311    }
312}
313
314fn pretty_features(
315    resolved_features: Vec<(InternedString, FeatureStatus)>,
316    features: &FeatureMap,
317    verbosity: Verbosity,
318    stdout: &mut dyn Write,
319) -> CargoResult<()> {
320    let header = HEADER;
321    let enabled_by_user = HEADER;
322    let enabled = NOP;
323    let disabled = anstyle::Style::new() | anstyle::Effects::DIMMED;
324    let summary = anstyle::Style::new() | anstyle::Effects::ITALIC;
325
326    // If there are no features, return early.
327    let margin = features
328        .iter()
329        .map(|(name, _)| name.len())
330        .max()
331        .unwrap_or_default();
332    if margin == 0 {
333        return Ok(());
334    }
335
336    writeln!(stdout, "{header}features:{header:#}")?;
337
338    const MAX_FEATURE_PRINTS: usize = 30;
339    let total_activated = resolved_features
340        .iter()
341        .filter(|(_, s)| !s.is_disabled())
342        .count();
343    let total_deactivated = resolved_features
344        .iter()
345        .filter(|(_, s)| s.is_disabled())
346        .count();
347    let show_all = match verbosity {
348        Verbosity::Quiet | Verbosity::Normal => false,
349        Verbosity::Verbose => true,
350    };
351    let show_activated = total_activated <= MAX_FEATURE_PRINTS || show_all;
352    let show_deactivated = (total_activated + total_deactivated) <= MAX_FEATURE_PRINTS || show_all;
353    for (current, status, current_activated) in resolved_features
354        .iter()
355        .map(|(n, s)| (n, s, features.get(n).unwrap()))
356    {
357        if !status.is_disabled() && !show_activated {
358            continue;
359        }
360        if status.is_disabled() && !show_deactivated {
361            continue;
362        }
363        if *status == FeatureStatus::EnabledByUser {
364            write!(stdout, " {enabled_by_user}+{enabled_by_user:#}")?;
365        } else {
366            write!(stdout, "  ")?;
367        }
368        let style = match status {
369            FeatureStatus::EnabledByUser | FeatureStatus::Enabled => enabled,
370            FeatureStatus::Disabled => disabled,
371        };
372        writeln!(
373            stdout,
374            "{style}{current: <margin$}{style:#} = [{features}]",
375            features = current_activated
376                .iter()
377                .map(|s| format!("{style}{s}{style:#}"))
378                .collect::<Vec<String>>()
379                .join(", ")
380        )?;
381    }
382    if !show_activated {
383        writeln!(
384            stdout,
385            "  {summary}{total_activated} activated features{summary:#}",
386        )?;
387    }
388    if !show_deactivated {
389        writeln!(
390            stdout,
391            "  {summary}{total_deactivated} deactivated features{summary:#}",
392        )?;
393    }
394
395    Ok(())
396}
397
398// Suggest the cargo tree command to view the dependency tree.
399fn suggest_cargo_tree(package_id: PackageId, shell: &mut Shell) -> CargoResult<()> {
400    let literal = LITERAL;
401
402    shell.note(format_args!(
403        "to see how you depend on {name}, run `{literal}cargo tree --invert --package {name}@{version}{literal:#}`",
404        name = package_id.name(),
405        version = package_id.version(),
406    ))
407}
408
409#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
410enum FeatureStatus {
411    EnabledByUser,
412    Enabled,
413    Disabled,
414}
415
416impl FeatureStatus {
417    fn is_disabled(&self) -> bool {
418        *self == FeatureStatus::Disabled
419    }
420}
421
422fn resolve_features(
423    explicit: &[InternedString],
424    features: &FeatureMap,
425) -> Vec<(InternedString, FeatureStatus)> {
426    let mut resolved = features
427        .keys()
428        .cloned()
429        .map(|n| {
430            if explicit.contains(&n) {
431                (n, FeatureStatus::EnabledByUser)
432            } else {
433                (n, FeatureStatus::Disabled)
434            }
435        })
436        .collect::<HashMap<_, _>>();
437
438    let mut activated_queue = explicit.to_vec();
439
440    while let Some(current) = activated_queue.pop() {
441        let Some(current_activated) = features.get(&current) else {
442            // `default` isn't always present
443            continue;
444        };
445        for activated in current_activated.iter().rev().filter_map(|f| match f {
446            crate::core::FeatureValue::Feature(name) => Some(name),
447            crate::core::FeatureValue::Dep { .. }
448            | crate::core::FeatureValue::DepFeature { .. } => None,
449        }) {
450            let Some(status) = resolved.get_mut(activated) else {
451                continue;
452            };
453            if status.is_disabled() {
454                *status = FeatureStatus::Enabled;
455                activated_queue.push(*activated);
456            }
457        }
458    }
459
460    let mut resolved: Vec<_> = resolved.into_iter().collect();
461    resolved.sort_by_key(|(name, status)| (*status, *name));
462    resolved
463}