Skip to content

Commit 8130be7

Browse files
committed
Auto merge of #9606 - ehuss:future-incompat-enhancements, r=alexcrichton
Updates to future-incompatible reporting. This includes several changes to future-incompatible reports: - Now able to retain multiple reports on disk (currently 5). - `cargo report future-incompatibilities` will now show the last report without `--id`. - Changed `cargo report future-incompatibilities` to display the report on stdout. Although these are "diagnostics", I see this more as a human-readable "report", which might benefit from being more easily piped (like to a pager). I also expect all other report subcommands to print to stdout. - Fixed a bug where saving the reports to disk was not truncating the file. If multiple reports were saved, and future ones were *shorter* than a previous one, the on-disk structure would be corrupted. - Reports are now always stored with ANSI escape codes, and the color filtering is done only when displayed. - Some slight adjustments to the formatting of the report. - When the wrong `--id` is passed, show available reports. - Give a slightly better error message when there are no reports. - Fixed bug where "0 dependencies has warnings" was unconditionally displayed (it should only be displayed if --future-incompat-report is used) - Added `future-incompat` as an alias for the `future-incompatibilities` subcommand which can be quite a verbose command. - When showing the note at the end of the build, make sure the list is unique (for example, if a single package had multiple targets that trigger a warning). - Added a note at the bottom of the report that indicates if newer versions of a package are available.
2 parents f2c22a3 + afe7314 commit 8130be7

File tree

6 files changed

+557
-179
lines changed

6 files changed

+557
-179
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ itertools = "0.10.0"
7373
# See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`
7474
# for more information.
7575
rustc-workspace-hack = "1.0.0"
76-
rand = "0.8.3"
7776

7877
[target.'cfg(windows)'.dependencies]
7978
fwdansi = "1.1.0"

src/bin/cargo/commands/report.rs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use crate::command_prelude::*;
2-
use anyhow::{anyhow, Context as _};
3-
use cargo::core::compiler::future_incompat::{OnDiskReport, FUTURE_INCOMPAT_FILE};
4-
use cargo::drop_eprint;
5-
use std::io::Read;
2+
use anyhow::anyhow;
3+
use cargo::core::compiler::future_incompat::{OnDiskReports, REPORT_PREAMBLE};
4+
use cargo::drop_println;
65

76
pub fn cli() -> App {
87
subcommand("report")
@@ -11,14 +10,14 @@ pub fn cli() -> App {
1110
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
1211
.subcommand(
1312
subcommand("future-incompatibilities")
13+
.alias("future-incompat")
1414
.about("Reports any crates which will eventually stop compiling")
1515
.arg(
1616
opt(
1717
"id",
1818
"identifier of the report generated by a Cargo command invocation",
1919
)
20-
.value_name("id")
21-
.required(true),
20+
.value_name("id"),
2221
),
2322
)
2423
}
@@ -35,31 +34,12 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
3534

3635
fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliResult {
3736
let ws = args.workspace(config)?;
38-
let report_file = ws.target_dir().open_ro(
39-
FUTURE_INCOMPAT_FILE,
40-
ws.config(),
41-
"Future incompatible report",
42-
)?;
43-
44-
let mut file_contents = String::new();
45-
report_file
46-
.file()
47-
.read_to_string(&mut file_contents)
48-
.with_context(|| "failed to read report")?;
49-
let on_disk_report: OnDiskReport =
50-
serde_json::from_str(&file_contents).with_context(|| "failed to load report")?;
51-
52-
let id = args.value_of("id").unwrap();
53-
if id != on_disk_report.id {
54-
return Err(anyhow!(
55-
"Expected an id of `{}`, but `{}` was provided on the command line. \
56-
Your report may have been overwritten by a different one.",
57-
on_disk_report.id,
58-
id
59-
)
60-
.into());
61-
}
62-
63-
drop_eprint!(config, "{}", on_disk_report.report);
37+
let reports = OnDiskReports::load(&ws)?;
38+
let id = args
39+
.value_of_u32("id")?
40+
.unwrap_or_else(|| reports.last_id());
41+
let report = reports.get_report(id, config)?;
42+
drop_println!(config, "{}", REPORT_PREAMBLE);
43+
drop(config.shell().print_ansi_stdout(report.as_bytes()));
6444
Ok(())
6545
}

src/cargo/core/compiler/future_incompat.rs

Lines changed: 259 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
1+
//! Support for future-incompatible warning reporting.
2+
3+
use crate::core::{Dependency, PackageId, Workspace};
4+
use crate::sources::SourceConfigMap;
5+
use crate::util::{iter_join, CargoResult, Config};
6+
use anyhow::{bail, format_err, Context};
17
use serde::{Deserialize, Serialize};
8+
use std::collections::{BTreeSet, HashMap, HashSet};
9+
use std::fmt::Write as _;
10+
use std::io::{Read, Write};
11+
12+
pub const REPORT_PREAMBLE: &str = "\
13+
The following warnings were discovered during the build. These warnings are an
14+
indication that the packages contain code that will become an error in a
15+
future release of Rust. These warnings typically cover changes to close
16+
soundness problems, unintended or undocumented behavior, or critical problems
17+
that cannot be fixed in a backwards-compatible fashion, and are not expected
18+
to be in wide use.
19+
20+
Each warning should contain a link for more information on what the warning
21+
means and how to resolve it.
22+
";
23+
24+
/// Current version of the on-disk format.
25+
const ON_DISK_VERSION: u32 = 0;
226

327
/// The future incompatibility report, emitted by the compiler as a JSON message.
428
#[derive(serde::Deserialize)]
529
pub struct FutureIncompatReport {
630
pub future_incompat_report: Vec<FutureBreakageItem>,
731
}
832

33+
/// Structure used for collecting reports in-memory.
34+
pub struct FutureIncompatReportPackage {
35+
pub package_id: PackageId,
36+
pub items: Vec<FutureBreakageItem>,
37+
}
38+
39+
/// A single future-incompatible warning emitted by rustc.
940
#[derive(Serialize, Deserialize)]
1041
pub struct FutureBreakageItem {
1142
/// The date at which this lint will become an error.
@@ -24,13 +55,234 @@ pub struct Diagnostic {
2455

2556
/// The filename in the top-level `target` directory where we store
2657
/// the report
27-
pub const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
58+
const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
59+
/// Max number of reports to save on disk.
60+
const MAX_REPORTS: usize = 5;
2861

62+
/// The structure saved to disk containing the reports.
2963
#[derive(Serialize, Deserialize)]
30-
pub struct OnDiskReport {
31-
// A Cargo-generated id used to detect when a report has been overwritten
32-
pub id: String,
33-
// Cannot be a &str, since Serde needs
34-
// to be able to un-escape the JSON
35-
pub report: String,
64+
pub struct OnDiskReports {
65+
/// A schema version number, to handle older cargo's from trying to read
66+
/// something that they don't understand.
67+
version: u32,
68+
/// The report ID to use for the next report to save.
69+
next_id: u32,
70+
/// Available reports.
71+
reports: Vec<OnDiskReport>,
72+
}
73+
74+
/// A single report for a given compilation session.
75+
#[derive(Serialize, Deserialize)]
76+
struct OnDiskReport {
77+
/// Unique reference to the report for the `--id` CLI flag.
78+
id: u32,
79+
/// Report, suitable for printing to the console.
80+
report: String,
81+
}
82+
83+
impl Default for OnDiskReports {
84+
fn default() -> OnDiskReports {
85+
OnDiskReports {
86+
version: ON_DISK_VERSION,
87+
next_id: 1,
88+
reports: Vec::new(),
89+
}
90+
}
91+
}
92+
93+
impl OnDiskReports {
94+
/// Saves a new report.
95+
pub fn save_report(
96+
ws: &Workspace<'_>,
97+
per_package_reports: &[FutureIncompatReportPackage],
98+
) -> OnDiskReports {
99+
let mut current_reports = match Self::load(ws) {
100+
Ok(r) => r,
101+
Err(e) => {
102+
log::debug!(
103+
"saving future-incompatible reports failed to load current reports: {:?}",
104+
e
105+
);
106+
OnDiskReports::default()
107+
}
108+
};
109+
let report = OnDiskReport {
110+
id: current_reports.next_id,
111+
report: render_report(ws, per_package_reports),
112+
};
113+
current_reports.next_id += 1;
114+
current_reports.reports.push(report);
115+
if current_reports.reports.len() > MAX_REPORTS {
116+
current_reports.reports.remove(0);
117+
}
118+
let on_disk = serde_json::to_vec(&current_reports).unwrap();
119+
if let Err(e) = ws
120+
.target_dir()
121+
.open_rw(
122+
FUTURE_INCOMPAT_FILE,
123+
ws.config(),
124+
"Future incompatibility report",
125+
)
126+
.and_then(|file| {
127+
let mut file = file.file();
128+
file.set_len(0)?;
129+
file.write_all(&on_disk)?;
130+
Ok(())
131+
})
132+
{
133+
crate::display_warning_with_error(
134+
"failed to write on-disk future incompatible report",
135+
&e,
136+
&mut ws.config().shell(),
137+
);
138+
}
139+
current_reports
140+
}
141+
142+
/// Loads the on-disk reports.
143+
pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
144+
let report_file = match ws.target_dir().open_ro(
145+
FUTURE_INCOMPAT_FILE,
146+
ws.config(),
147+
"Future incompatible report",
148+
) {
149+
Ok(r) => r,
150+
Err(e) => {
151+
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
152+
if io_err.kind() == std::io::ErrorKind::NotFound {
153+
bail!("no reports are currently available");
154+
}
155+
}
156+
return Err(e);
157+
}
158+
};
159+
160+
let mut file_contents = String::new();
161+
report_file
162+
.file()
163+
.read_to_string(&mut file_contents)
164+
.with_context(|| "failed to read report")?;
165+
let on_disk_reports: OnDiskReports =
166+
serde_json::from_str(&file_contents).with_context(|| "failed to load report")?;
167+
if on_disk_reports.version != ON_DISK_VERSION {
168+
bail!("unable to read reports; reports were saved from a future version of Cargo");
169+
}
170+
Ok(on_disk_reports)
171+
}
172+
173+
/// Returns the most recent report ID.
174+
pub fn last_id(&self) -> u32 {
175+
self.reports.last().map(|r| r.id).unwrap()
176+
}
177+
178+
pub fn get_report(&self, id: u32, config: &Config) -> CargoResult<String> {
179+
let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
180+
let available = iter_join(self.reports.iter().map(|r| r.id.to_string()), ", ");
181+
format_err!(
182+
"could not find report with ID {}\n\
183+
Available IDs are: {}",
184+
id,
185+
available
186+
)
187+
})?;
188+
let report = if config.shell().err_supports_color() {
189+
report.report.clone()
190+
} else {
191+
strip_ansi_escapes::strip(&report.report)
192+
.map(|v| String::from_utf8(v).expect("utf8"))
193+
.expect("strip should never fail")
194+
};
195+
Ok(report)
196+
}
197+
}
198+
199+
fn render_report(
200+
ws: &Workspace<'_>,
201+
per_package_reports: &[FutureIncompatReportPackage],
202+
) -> String {
203+
let mut per_package_reports: Vec<_> = per_package_reports.iter().collect();
204+
per_package_reports.sort_by_key(|r| r.package_id);
205+
let mut rendered = String::new();
206+
for per_package in &per_package_reports {
207+
rendered.push_str(&format!(
208+
"The package `{}` currently triggers the following future \
209+
incompatibility lints:\n",
210+
per_package.package_id
211+
));
212+
for item in &per_package.items {
213+
rendered.extend(
214+
item.diagnostic
215+
.rendered
216+
.lines()
217+
.map(|l| format!("> {}\n", l)),
218+
);
219+
}
220+
rendered.push('\n');
221+
}
222+
if let Some(s) = render_suggestions(ws, &per_package_reports) {
223+
rendered.push_str(&s);
224+
}
225+
rendered
226+
}
227+
228+
fn render_suggestions(
229+
ws: &Workspace<'_>,
230+
per_package_reports: &[&FutureIncompatReportPackage],
231+
) -> Option<String> {
232+
// This in general ignores all errors since this is opportunistic.
233+
let _lock = ws.config().acquire_package_cache_lock().ok()?;
234+
// Create a set of updated registry sources.
235+
let map = SourceConfigMap::new(ws.config()).ok()?;
236+
let package_ids: BTreeSet<_> = per_package_reports
237+
.iter()
238+
.map(|r| r.package_id)
239+
.filter(|pkg_id| pkg_id.source_id().is_registry())
240+
.collect();
241+
let source_ids: HashSet<_> = package_ids
242+
.iter()
243+
.map(|pkg_id| pkg_id.source_id())
244+
.collect();
245+
let mut sources: HashMap<_, _> = source_ids
246+
.into_iter()
247+
.filter_map(|sid| {
248+
let source = map.load(sid, &HashSet::new()).ok()?;
249+
Some((sid, source))
250+
})
251+
.collect();
252+
// Query the sources for new versions.
253+
let mut suggestions = String::new();
254+
for pkg_id in package_ids {
255+
let source = match sources.get_mut(&pkg_id.source_id()) {
256+
Some(s) => s,
257+
None => continue,
258+
};
259+
let dep = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()).ok()?;
260+
let summaries = source.query_vec(&dep).ok()?;
261+
let versions = itertools::sorted(
262+
summaries
263+
.iter()
264+
.map(|summary| summary.version())
265+
.filter(|version| *version > pkg_id.version()),
266+
);
267+
let versions = versions.map(|version| version.to_string());
268+
let versions = iter_join(versions, ", ");
269+
if !versions.is_empty() {
270+
writeln!(
271+
suggestions,
272+
"{} has the following newer versions available: {}",
273+
pkg_id, versions
274+
)
275+
.unwrap();
276+
}
277+
}
278+
if suggestions.is_empty() {
279+
None
280+
} else {
281+
Some(format!(
282+
"The following packages appear to have newer versions available.\n\
283+
You may want to consider updating them to a newer version to see if the \
284+
issue has been fixed.\n\n{}",
285+
suggestions
286+
))
287+
}
36288
}

0 commit comments

Comments
 (0)