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
13pub(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 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 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 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 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 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
396fn 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(¤t) else {
440 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}