Skip to content

Commit d5538c3

Browse files
committed
Make future-incompat-report output more user-friendly
When the user enables `--future-incompat-report`, we now display a high-level summary of the problem, as well as several suggestions for fixing the affected crates. The command `cargo report future-incompatibilities` now takes a `--crate` option, which can be used to display a report (including the actual lint messages) for a single crate. When this option is not used, we display the report for all crates.
1 parent ad85ec9 commit d5538c3

File tree

4 files changed

+232
-110
lines changed

4 files changed

+232
-110
lines changed

src/bin/cargo/commands/report.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ pub fn cli() -> App {
1818
"identifier of the report generated by a Cargo command invocation",
1919
)
2020
.value_name("id"),
21+
)
22+
.arg(
23+
opt("crate", "identifier of the crate to display a report for")
24+
.value_name("crate"),
2125
),
2226
)
2327
}
@@ -38,7 +42,8 @@ fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliR
3842
let id = args
3943
.value_of_u32("id")?
4044
.unwrap_or_else(|| reports.last_id());
41-
let report = reports.get_report(id, config)?;
45+
let krate = args.value_of("crate");
46+
let report = reports.get_report(id, config, krate)?;
4247
drop_println!(config, "{}", REPORT_PREAMBLE);
4348
drop(config.shell().print_ansi_stdout(report.as_bytes()));
4449
Ok(())

src/cargo/core/compiler/future_incompat.rs

Lines changed: 46 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
//! Support for future-incompatible warning reporting.
22
3-
use crate::core::{Dependency, PackageId, Workspace};
4-
use crate::sources::SourceConfigMap;
3+
use crate::core::{PackageId, Workspace};
54
use crate::util::{iter_join, CargoResult, Config};
65
use anyhow::{bail, format_err, Context};
76
use serde::{Deserialize, Serialize};
8-
use std::collections::{BTreeSet, HashMap, HashSet};
9-
use std::fmt::Write as _;
7+
use std::collections::BTreeMap;
108
use std::io::{Read, Write};
119

1210
pub const REPORT_PREAMBLE: &str = "\
@@ -78,7 +76,10 @@ struct OnDiskReport {
7876
/// Unique reference to the report for the `--id` CLI flag.
7977
id: u32,
8078
/// Report, suitable for printing to the console.
81-
report: String,
79+
/// Maps crate names to the corresponding report
80+
/// We use a `BTreeMap` so that the iteration order
81+
/// is stable across multiple runs of `cargo`
82+
per_crate: BTreeMap<String, String>,
8283
}
8384

8485
impl Default for OnDiskReports {
@@ -109,7 +110,7 @@ impl OnDiskReports {
109110
};
110111
let report = OnDiskReport {
111112
id: current_reports.next_id,
112-
report: render_report(ws, per_package_reports),
113+
per_crate: render_report(per_package_reports),
113114
};
114115
current_reports.next_id += 1;
115116
current_reports.reports.push(report);
@@ -176,7 +177,12 @@ impl OnDiskReports {
176177
self.reports.last().map(|r| r.id).unwrap()
177178
}
178179

179-
pub fn get_report(&self, id: u32, config: &Config) -> CargoResult<String> {
180+
pub fn get_report(
181+
&self,
182+
id: u32,
183+
config: &Config,
184+
package: Option<&str>,
185+
) -> CargoResult<String> {
180186
let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
181187
let available = iter_join(self.reports.iter().map(|r| r.id.to_string()), ", ");
182188
format_err!(
@@ -186,25 +192,45 @@ impl OnDiskReports {
186192
available
187193
)
188194
})?;
189-
let report = if config.shell().err_supports_color() {
190-
report.report.clone()
195+
let to_display = if let Some(package) = package {
196+
report
197+
.per_crate
198+
.get(package)
199+
.ok_or_else(|| {
200+
format_err!(
201+
"could not find package with ID `{}`\n
202+
Available packages are: {}\n
203+
Omit the `--crate` flag to display a report for all crates",
204+
package,
205+
iter_join(report.per_crate.keys(), ", ")
206+
)
207+
})?
208+
.clone()
191209
} else {
192-
strip_ansi_escapes::strip(&report.report)
210+
report
211+
.per_crate
212+
.values()
213+
.cloned()
214+
.collect::<Vec<_>>()
215+
.join("\n")
216+
};
217+
let to_display = if config.shell().err_supports_color() {
218+
to_display
219+
} else {
220+
strip_ansi_escapes::strip(&to_display)
193221
.map(|v| String::from_utf8(v).expect("utf8"))
194222
.expect("strip should never fail")
195223
};
196-
Ok(report)
224+
Ok(to_display)
197225
}
198226
}
199227

200-
fn render_report(
201-
ws: &Workspace<'_>,
202-
per_package_reports: &[FutureIncompatReportPackage],
203-
) -> String {
204-
let mut per_package_reports: Vec<_> = per_package_reports.iter().collect();
205-
per_package_reports.sort_by_key(|r| r.package_id);
206-
let mut rendered = String::new();
207-
for per_package in &per_package_reports {
228+
fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> {
229+
let mut report: BTreeMap<String, String> = BTreeMap::new();
230+
for per_package in per_package_reports {
231+
let rendered = report
232+
.entry(per_package.package_id.to_string())
233+
.or_default();
208234
rendered.push_str(&format!(
209235
"The package `{}` currently triggers the following future \
210236
incompatibility lints:\n",
@@ -218,72 +244,6 @@ fn render_report(
218244
.map(|l| format!("> {}\n", l)),
219245
);
220246
}
221-
rendered.push('\n');
222-
}
223-
if let Some(s) = render_suggestions(ws, &per_package_reports) {
224-
rendered.push_str(&s);
225-
}
226-
rendered
227-
}
228-
229-
fn render_suggestions(
230-
ws: &Workspace<'_>,
231-
per_package_reports: &[&FutureIncompatReportPackage],
232-
) -> Option<String> {
233-
// This in general ignores all errors since this is opportunistic.
234-
let _lock = ws.config().acquire_package_cache_lock().ok()?;
235-
// Create a set of updated registry sources.
236-
let map = SourceConfigMap::new(ws.config()).ok()?;
237-
let package_ids: BTreeSet<_> = per_package_reports
238-
.iter()
239-
.map(|r| r.package_id)
240-
.filter(|pkg_id| pkg_id.source_id().is_registry())
241-
.collect();
242-
let source_ids: HashSet<_> = package_ids
243-
.iter()
244-
.map(|pkg_id| pkg_id.source_id())
245-
.collect();
246-
let mut sources: HashMap<_, _> = source_ids
247-
.into_iter()
248-
.filter_map(|sid| {
249-
let source = map.load(sid, &HashSet::new()).ok()?;
250-
Some((sid, source))
251-
})
252-
.collect();
253-
// Query the sources for new versions.
254-
let mut suggestions = String::new();
255-
for pkg_id in package_ids {
256-
let source = match sources.get_mut(&pkg_id.source_id()) {
257-
Some(s) => s,
258-
None => continue,
259-
};
260-
let dep = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()).ok()?;
261-
let summaries = source.query_vec(&dep).ok()?;
262-
let versions = itertools::sorted(
263-
summaries
264-
.iter()
265-
.map(|summary| summary.version())
266-
.filter(|version| *version > pkg_id.version()),
267-
);
268-
let versions = versions.map(|version| version.to_string());
269-
let versions = iter_join(versions, ", ");
270-
if !versions.is_empty() {
271-
writeln!(
272-
suggestions,
273-
"{} has the following newer versions available: {}",
274-
pkg_id, versions
275-
)
276-
.unwrap();
277-
}
278-
}
279-
if suggestions.is_empty() {
280-
None
281-
} else {
282-
Some(format!(
283-
"The following packages appear to have newer versions available.\n\
284-
You may want to consider updating them to a newer version to see if the \
285-
issue has been fixed.\n\n{}",
286-
suggestions
287-
))
288247
}
248+
report
289249
}

src/cargo/core/compiler/job_queue.rs

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,13 @@ use crate::core::compiler::future_incompat::{
7575
FutureBreakageItem, FutureIncompatReportPackage, OnDiskReports,
7676
};
7777
use crate::core::resolver::ResolveBehavior;
78+
use crate::core::{Dependency, Workspace};
7879
use crate::core::{PackageId, Shell, TargetKind};
80+
use crate::sources::SourceConfigMap;
7981
use crate::util::diagnostic_server::{self, DiagnosticPrinter};
8082
use crate::util::machine_message::{self, Message as _};
8183
use crate::util::CargoResult;
82-
use crate::util::{self, internal, profile};
84+
use crate::util::{self, internal, iter_join, profile};
8385
use crate::util::{Config, DependencyQueue, Progress, ProgressStyle, Queue};
8486

8587
/// This structure is backed by the `DependencyQueue` type and manages the
@@ -911,15 +913,12 @@ impl<'cfg> DrainState<'cfg> {
911913
}
912914

913915
// Get a list of unique and sorted package name/versions.
914-
let package_vers: BTreeSet<_> = self
916+
let package_ids: BTreeSet<_> = self
915917
.per_package_future_incompat_reports
916918
.iter()
917919
.map(|r| r.package_id)
918920
.collect();
919-
let package_vers: Vec<_> = package_vers
920-
.into_iter()
921-
.map(|pid| pid.to_string())
922-
.collect();
921+
let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect();
923922

924923
if should_display_message || bcx.build_config.future_incompat_report {
925924
drop(bcx.config.shell().warn(&format!(
@@ -934,8 +933,76 @@ impl<'cfg> DrainState<'cfg> {
934933
let report_id = on_disk_reports.last_id();
935934

936935
if bcx.build_config.future_incompat_report {
937-
let rendered = on_disk_reports.get_report(report_id, bcx.config).unwrap();
938-
drop(bcx.config.shell().print_ansi_stderr(rendered.as_bytes()));
936+
let upstream_info = package_ids
937+
.iter()
938+
.map(|package_id| {
939+
let manifest = bcx.packages.get_one(*package_id).unwrap().manifest();
940+
format!(
941+
"
942+
- {name}
943+
- Repository: {url}
944+
- Detailed warning command: `cargo report future-incompatibilities --id {id} --crate '{name}'",
945+
name = package_id,
946+
url = manifest
947+
.metadata()
948+
.repository
949+
.as_deref()
950+
.unwrap_or("<not found>"),
951+
id = report_id,
952+
)
953+
})
954+
.collect::<Vec<_>>()
955+
.join("\n");
956+
957+
let (compat, incompat) =
958+
get_updates(bcx.ws, &package_ids).unwrap_or((String::new(), String::new()));
959+
960+
let compat_message = if !compat.is_empty() {
961+
format!(
962+
"
963+
- Some affected dependencies have minor or patch version updates available:
964+
{compat}",
965+
compat = compat
966+
)
967+
} else {
968+
String::new()
969+
};
970+
971+
let incompat_message = if !incompat.is_empty() {
972+
format!(
973+
"
974+
- If a minor dependency update does not help, you can try updating to a new
975+
major version of those dependencies. You have to do this manually:
976+
{incompat}
977+
",
978+
incompat = incompat
979+
)
980+
} else {
981+
String::new()
982+
};
983+
984+
drop(bcx.config.shell().note(&format!(
985+
"
986+
To solve this problem, you can try the following approaches:
987+
988+
{compat_message}
989+
{incompat_message}
990+
- If the issue is not solved by updating the dependencies, a fix has to be
991+
implemented by those dependencies. You can help with that by notifying the
992+
maintainers of this problem (e.g. by creating a bug report) or by proposing a
993+
fix to the maintainers (e.g. by creating a pull request):
994+
{upstream_info}
995+
996+
- If waiting for an upstream fix is not an option, you can use the `[patch]`
997+
section in `Cargo.toml` to use your own version of the dependency. For more
998+
information, see:
999+
https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section
1000+
",
1001+
upstream_info = upstream_info,
1002+
compat_message = compat_message,
1003+
incompat_message = incompat_message
1004+
)));
1005+
9391006
drop(bcx.config.shell().note(&format!(
9401007
"this report can be shown with `cargo report \
9411008
future-incompatibilities -Z future-incompat-report --id {}`",
@@ -1283,3 +1350,75 @@ feature resolver. Try updating to diesel 1.4.8 to fix this error.
12831350
Ok(())
12841351
}
12851352
}
1353+
1354+
// Returns a pair (compatible_updates, incompatible_updates),
1355+
// of semver-compatible and semver-incompatible update versions,
1356+
// respectively.
1357+
fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<(String, String)> {
1358+
// This in general ignores all errors since this is opportunistic.
1359+
let _lock = ws.config().acquire_package_cache_lock().ok()?;
1360+
// Create a set of updated registry sources.
1361+
let map = SourceConfigMap::new(ws.config()).ok()?;
1362+
let package_ids: BTreeSet<_> = package_ids
1363+
.iter()
1364+
.filter(|pkg_id| pkg_id.source_id().is_registry())
1365+
.collect();
1366+
let source_ids: HashSet<_> = package_ids
1367+
.iter()
1368+
.map(|pkg_id| pkg_id.source_id())
1369+
.collect();
1370+
let mut sources: HashMap<_, _> = source_ids
1371+
.into_iter()
1372+
.filter_map(|sid| {
1373+
let source = map.load(sid, &HashSet::new()).ok()?;
1374+
Some((sid, source))
1375+
})
1376+
.collect();
1377+
// Query the sources for new versions.
1378+
let mut compatible = String::new();
1379+
let mut incompatible = String::new();
1380+
for pkg_id in package_ids {
1381+
let source = match sources.get_mut(&pkg_id.source_id()) {
1382+
Some(s) => s,
1383+
None => continue,
1384+
};
1385+
let dep = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()).ok()?;
1386+
let summaries = source.query_vec(&dep).ok()?;
1387+
let (mut compatible_versions, mut incompatible_versions): (Vec<_>, Vec<_>) = summaries
1388+
.iter()
1389+
.map(|summary| summary.version())
1390+
.filter(|version| *version > pkg_id.version())
1391+
.partition(|version| version.major == pkg_id.version().major);
1392+
compatible_versions.sort();
1393+
incompatible_versions.sort();
1394+
1395+
let compatible_versions = compatible_versions
1396+
.into_iter()
1397+
.map(|version| version.to_string());
1398+
let compatible_versions = iter_join(compatible_versions, ", ");
1399+
1400+
let incompatible_versions = incompatible_versions
1401+
.into_iter()
1402+
.map(|version| version.to_string());
1403+
let incompatible_versions = iter_join(incompatible_versions, ", ");
1404+
1405+
if !compatible_versions.is_empty() {
1406+
writeln!(
1407+
compatible,
1408+
"{} has the following newer versions available: {}",
1409+
pkg_id, compatible_versions
1410+
)
1411+
.unwrap();
1412+
}
1413+
1414+
if !incompatible_versions.is_empty() {
1415+
writeln!(
1416+
incompatible,
1417+
"{} has the following newer versions available: {}",
1418+
pkg_id, incompatible_versions
1419+
)
1420+
.unwrap();
1421+
}
1422+
}
1423+
Some((compatible, incompatible))
1424+
}

0 commit comments

Comments
 (0)