Skip to content

Commit 6177c65

Browse files
committed
Implement future incompatibility report support
cc rust-lang/rust#71249 This implements the Cargo side of 'Cargo report future-incompat' Based on feedback from alexcrichton and est31, I'm implemented this a flag `--future-compat-report` on `cargo check/build/rustc`, rather than a separate `cargo describe-future-incompatibilities` command. This allows us to avoid writing additional information to disk (beyond the pre-existing recording of rustc command outputs). This PR contains: * Gating of all functionality behind `-Z report-future-incompat`. Without this flag, all user output is unchanged. * Passing `-Z emit-future-incompat-report` to rustc when `-Z report-future-incompat` is enabled * Parsing the rustc JSON future incompat report, and displaying it it a user-readable format. * Emitting a warning at the end of a build if any crates had future-incompat reports * A `--future-incompat-report` flag, which shows the full report for each affected crate. * Tests for all of the above. At the moment, we can use the `array_into_iter` to write a test. However, we might eventually get to a point where rustc is not currently emitting future-incompat reports for any lints. What would we want the cargo tests to do in this situation? This functionality didn't require any significant internal changes to Cargo, with one exception: we now process captured command output for all units, not just ones where we want to display warnings. This may result in a slightly longer time to run `cargo build/check/rustc` from a full cache. since we do slightly more work for each upstream dependency. Doing this seems unavoidable with the current architecture, since we need to process captured command outputs to detect any future-incompat-report messages that were emitted.
1 parent c694096 commit 6177c65

File tree

14 files changed

+430
-13
lines changed

14 files changed

+430
-13
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ im-rc = "15.0.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"
7677

7778
[target.'cfg(target_os = "macos")'.dependencies]
7879
core-foundation = { version = "0.9.0", features = ["mac_os_10_7_support"] }

src/bin/cargo/commands/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub fn cli() -> App {
4343
.arg_message_format()
4444
.arg_build_plan()
4545
.arg_unit_graph()
46+
.arg_future_incompat_report()
4647
.after_help("Run `cargo help build` for more detailed information.\n")
4748
}
4849

src/bin/cargo/commands/check.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub fn cli() -> App {
3535
.arg_ignore_rust_version()
3636
.arg_message_format()
3737
.arg_unit_graph()
38+
.arg_future_incompat_report()
3839
.after_help("Run `cargo help check` for more detailed information.\n")
3940
}
4041

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::command_prelude::*;
2+
use anyhow::anyhow;
3+
use cargo::core::compiler::future_incompat::{OnDiskReport, FUTURE_INCOMPAT_FILE};
4+
use cargo::core::nightly_features_allowed;
5+
use cargo::drop_eprint;
6+
use std::io::Read;
7+
8+
pub fn cli() -> App {
9+
subcommand("describe-future-incompatibilities")
10+
.arg(
11+
opt(
12+
"id",
13+
"identifier of the report [generated by a Cargo command invocation",
14+
)
15+
.value_name("id")
16+
.required(true),
17+
)
18+
.about("Reports any crates which will eventually stop compiling")
19+
}
20+
21+
pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
22+
if !nightly_features_allowed() {
23+
return Err(anyhow!(
24+
"`cargo describe-future-incompatibilities` can only be used on the nightly channel"
25+
)
26+
.into());
27+
}
28+
29+
let ws = args.workspace(config)?;
30+
let report_file = ws.target_dir().open_ro(
31+
FUTURE_INCOMPAT_FILE,
32+
ws.config(),
33+
"Future incompatible report",
34+
)?;
35+
36+
let mut file_contents = String::new();
37+
report_file
38+
.file()
39+
.read_to_string(&mut file_contents)
40+
.map_err(|e| anyhow!("Failed to read report: {:?}", e))?;
41+
let on_disk_report: OnDiskReport = serde_json::from_str(&file_contents).unwrap();
42+
43+
let id = args.value_of("id").unwrap();
44+
if id != on_disk_report.id {
45+
return Err(anyhow!("Expected an id of `{}`, but `{}` was provided on the command line. Your report may have been overwritten by a different one.", on_disk_report.id, id).into());
46+
}
47+
48+
drop_eprint!(config, "{}", on_disk_report.report);
49+
Ok(())
50+
}

src/bin/cargo/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub fn builtin() -> Vec<App> {
66
build::cli(),
77
check::cli(),
88
clean::cli(),
9+
describe_future_incompatibilities::cli(),
910
doc::cli(),
1011
fetch::cli(),
1112
fix::cli(),
@@ -44,6 +45,7 @@ pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches<'_>) -> Cli
4445
"build" => build::exec,
4546
"check" => check::exec,
4647
"clean" => clean::exec,
48+
"describe-future-incompatibilities" => describe_future_incompatibilities::exec,
4749
"doc" => doc::exec,
4850
"fetch" => fetch::exec,
4951
"fix" => fix::exec,
@@ -82,6 +84,7 @@ pub mod bench;
8284
pub mod build;
8385
pub mod check;
8486
pub mod clean;
87+
pub mod describe_future_incompatibilities;
8588
pub mod doc;
8689
pub mod fetch;
8790
pub mod fix;

src/bin/cargo/commands/rustc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub fn cli() -> App {
4040
.arg_message_format()
4141
.arg_unit_graph()
4242
.arg_ignore_rust_version()
43+
.arg_future_incompat_report()
4344
.after_help("Run `cargo help rustc` for more detailed information.\n")
4445
}
4546

src/cargo/core/compiler/build_config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub struct BuildConfig {
3737
// Note that, although the cmd-line flag name is `out-dir`, in code we use
3838
// `export_dir`, to avoid confusion with out dir at `target/debug/deps`.
3939
pub export_dir: Option<PathBuf>,
40+
/// `true` to output a future incompatibility report at the end of the build
41+
pub future_incompat_report: bool,
4042
}
4143

4244
impl BuildConfig {
@@ -80,6 +82,7 @@ impl BuildConfig {
8082
primary_unit_rustc: None,
8183
rustfix_diagnostic_server: RefCell::new(None),
8284
export_dir: None,
85+
future_incompat_report: false,
8386
})
8487
}
8588

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
/// The future incompatibility report, emitted by the compiler as a JSON message.
4+
#[derive(serde::Deserialize)]
5+
pub struct FutureIncompatReport {
6+
pub future_incompat_report: Vec<FutureBreakageItem>,
7+
}
8+
9+
#[derive(Serialize, Deserialize)]
10+
pub struct FutureBreakageItem {
11+
/// The date at which this lint will become an error.
12+
/// Currently unused
13+
pub future_breakage_date: Option<String>,
14+
/// The original diagnostic emitted by the compiler
15+
pub diagnostic: Diagnostic,
16+
}
17+
18+
/// A diagnostic emitted by the compiler a a JSON message.
19+
/// We only care about the 'rendered' field
20+
#[derive(Serialize, Deserialize)]
21+
pub struct Diagnostic {
22+
pub rendered: String,
23+
}
24+
25+
/// The filename in the top-level `target` directory where we store
26+
/// the report
27+
pub const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
28+
29+
#[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,
36+
}

src/cargo/core/compiler/job_queue.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ use anyhow::format_err;
6060
use crossbeam_utils::thread::Scope;
6161
use jobserver::{Acquired, Client, HelperThread};
6262
use log::{debug, info, trace};
63+
use rand::distributions::Alphanumeric;
64+
use rand::{thread_rng, Rng};
6365

6466
use super::context::OutputFile;
6567
use super::job::{
@@ -68,7 +70,11 @@ use super::job::{
6870
};
6971
use super::timings::Timings;
7072
use super::{BuildContext, BuildPlan, CompileMode, Context, Unit};
73+
use crate::core::compiler::future_incompat::{
74+
FutureBreakageItem, OnDiskReport, FUTURE_INCOMPAT_FILE,
75+
};
7176
use crate::core::{PackageId, Shell, TargetKind};
77+
use crate::drop_eprint;
7278
use crate::util::diagnostic_server::{self, DiagnosticPrinter};
7379
use crate::util::machine_message::{self, Message as _};
7480
use crate::util::{self, internal, profile};
@@ -151,6 +157,8 @@ struct DrainState<'cfg> {
151157

152158
/// How many jobs we've finished
153159
finished: usize,
160+
show_future_incompat_report: bool,
161+
per_crate_future_incompat_reports: Vec<FutureIncompatReportCrate>,
154162
}
155163

156164
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -162,6 +170,11 @@ impl std::fmt::Display for JobId {
162170
}
163171
}
164172

173+
struct FutureIncompatReportCrate {
174+
package_id: PackageId,
175+
report: Vec<FutureBreakageItem>,
176+
}
177+
165178
/// A `JobState` is constructed by `JobQueue::run` and passed to `Job::run`. It includes everything
166179
/// necessary to communicate between the main thread and the execution of the job.
167180
///
@@ -228,6 +241,7 @@ enum Message {
228241
FixDiagnostic(diagnostic_server::Message),
229242
Token(io::Result<Acquired>),
230243
Finish(JobId, Artifact, CargoResult<()>),
244+
FutureIncompatReport(JobId, Vec<FutureBreakageItem>),
231245

232246
// This client should get release_raw called on it with one of our tokens
233247
NeedsToken(JobId),
@@ -282,6 +296,11 @@ impl<'a> JobState<'a> {
282296
.push(Message::Finish(self.id, Artifact::Metadata, Ok(())));
283297
}
284298

299+
pub fn future_incompat_report(&self, report: Vec<FutureBreakageItem>) {
300+
self.messages
301+
.push(Message::FutureIncompatReport(self.id, report));
302+
}
303+
285304
/// The rustc underlying this Job is about to acquire a jobserver token (i.e., block)
286305
/// on the passed client.
287306
///
@@ -410,6 +429,8 @@ impl<'cfg> JobQueue<'cfg> {
410429
pending_queue: Vec::new(),
411430
print: DiagnosticPrinter::new(cx.bcx.config),
412431
finished: 0,
432+
show_future_incompat_report: cx.bcx.build_config.future_incompat_report,
433+
per_crate_future_incompat_reports: Vec::new(),
413434
};
414435

415436
// Create a helper thread for acquiring jobserver tokens
@@ -591,6 +612,14 @@ impl<'cfg> DrainState<'cfg> {
591612
}
592613
}
593614
}
615+
Message::FutureIncompatReport(id, report) => {
616+
let unit = self.active[&id].clone();
617+
self.per_crate_future_incompat_reports
618+
.push(FutureIncompatReportCrate {
619+
package_id: unit.pkg.package_id(),
620+
report,
621+
});
622+
}
594623
Message::Token(acquired_token) => {
595624
let token = acquired_token.chain_err(|| "failed to acquire jobserver token")?;
596625
self.tokens.push(token);
@@ -771,14 +800,93 @@ impl<'cfg> DrainState<'cfg> {
771800
if !cx.bcx.build_config.build_plan {
772801
// It doesn't really matter if this fails.
773802
drop(cx.bcx.config.shell().status("Finished", message));
803+
self.emit_future_incompat(cx);
774804
}
805+
775806
None
776807
} else {
777808
debug!("queue: {:#?}", self.queue);
778809
Some(internal("finished with jobs still left in the queue"))
779810
}
780811
}
781812

813+
fn emit_future_incompat(&mut self, cx: &mut Context<'_, '_>) {
814+
if cx.bcx.config.cli_unstable().enable_future_incompat_feature
815+
&& !self.per_crate_future_incompat_reports.is_empty()
816+
{
817+
self.per_crate_future_incompat_reports
818+
.sort_by_key(|r| r.package_id);
819+
820+
let crates_and_versions = self
821+
.per_crate_future_incompat_reports
822+
.iter()
823+
.map(|r| format!("{}", r.package_id))
824+
.collect::<Vec<_>>()
825+
.join(", ");
826+
827+
drop(cx.bcx.config.shell().warn(&format!("the following crates contain code that will be rejected by a future version of Rust: {}",
828+
crates_and_versions)));
829+
830+
let mut full_report = String::new();
831+
let mut rng = thread_rng();
832+
833+
// Generate a short ID to allow detecting if a report gets overwritten
834+
let id: String = std::iter::repeat(())
835+
.map(|()| char::from(rng.sample(Alphanumeric)))
836+
.take(4)
837+
.collect();
838+
839+
for report in std::mem::take(&mut self.per_crate_future_incompat_reports) {
840+
full_report.push_str(&format!("The crate `{}` currently triggers the following future incompatibility lints:\n", report.package_id));
841+
for item in report.report {
842+
let rendered = if cx.bcx.config.shell().err_supports_color() {
843+
item.diagnostic.rendered
844+
} else {
845+
strip_ansi_escapes::strip(&item.diagnostic.rendered)
846+
.map(|v| String::from_utf8(v).expect("utf8"))
847+
.expect("strip should never fail")
848+
};
849+
850+
for line in rendered.lines() {
851+
full_report.push_str(&format!("> {}\n", line));
852+
}
853+
}
854+
}
855+
856+
let report_file = cx.bcx.ws.target_dir().open_rw(
857+
FUTURE_INCOMPAT_FILE,
858+
cx.bcx.config,
859+
"Future incompatibility report",
860+
);
861+
let err = report_file
862+
.and_then(|report_file| {
863+
let on_disk_report = OnDiskReport {
864+
id: id.clone(),
865+
report: full_report.clone(),
866+
};
867+
serde_json::to_writer(report_file, &on_disk_report).map_err(|e| e.into())
868+
})
869+
.err();
870+
if let Some(e) = err {
871+
drop(cx.bcx.config.shell().warn(&format!(
872+
"Failed to open on-disk future incompat report: {:?}",
873+
e
874+
)));
875+
}
876+
877+
if self.show_future_incompat_report {
878+
drop_eprint!(cx.bcx.config, "{}", full_report);
879+
drop(cx.bcx.config.shell().note(
880+
&format!("this report can be shown with `cargo describe-future-incompatibilities -Z future-incompat-report --id {}`", id)
881+
));
882+
} else {
883+
drop(cx.bcx.config.shell().note(
884+
&format!("to see what the problems were, use the option `--future-incompat-report`, or run `cargo describe-future-incompatibilities --id {}`", id)
885+
));
886+
}
887+
}
888+
}
889+
782890
fn handle_error(
783891
&self,
784892
shell: &mut Shell,

0 commit comments

Comments
 (0)