Skip to main content

cargo/ops/registry/info/
view.rs

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