Skip to main content

cargo/ops/registry/info/
view.rs

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