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
14pub(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 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 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 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 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 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
393fn 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(¤t) else {
437 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}