#
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, WARN};
    6use crate::{
    7    CargoResult, GlobalContext,
    8    core::{
    9        Dependency, FeatureMap, Package, PackageId, SourceId, dependency::DepKind, shell::Verbosity,
    10    },
    11    sources::IndexSummary,
    12    util::interning::InternedString,
    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 context = annotate_snippets::renderer::DEFAULT_CONTEXT_STYLE;
    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(), " {context}{message}{context:#}")?;
    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:#}{context}from {}{context:#}{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                " {context}(from {}){context:#}",
    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 = &["default".into()];
    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}