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
15pub(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 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 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 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 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 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
398fn 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(¤t) else {
442 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}