Skip to content

Commit 7441e8c

Browse files
committed
Auto merge of #8825 - Aaron1011:feature/report-future-incompat, r=ehuss
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.
2 parents c694096 + 139ed73 commit 7441e8c

File tree

15 files changed

+444
-13
lines changed

15 files changed

+444
-13
lines changed

Cargo.toml

+1
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

+1
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

+1
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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use crate::command_prelude::*;
2+
use anyhow::anyhow;
3+
use cargo::core::compiler::future_incompat::{OnDiskReport, FUTURE_INCOMPAT_FILE};
4+
use cargo::drop_eprint;
5+
use cargo::util::CargoResultExt;
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 !config.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+
.chain_err(|| "failed to read report")?;
41+
let on_disk_report: OnDiskReport =
42+
serde_json::from_str(&file_contents).chain_err(|| "failed to load report")?;
43+
44+
let id = args.value_of("id").unwrap();
45+
if id != on_disk_report.id {
46+
return Err(anyhow!(
47+
"Expected an id of `{}`, but `{}` was provided on the command line. \
48+
Your report may have been overwritten by a different one.",
49+
on_disk_report.id,
50+
id
51+
)
52+
.into());
53+
}
54+
55+
drop_eprint!(config, "{}", on_disk_report.report);
56+
Ok(())
57+
}

src/bin/cargo/commands/mod.rs

+3
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

+1
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

+3
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

+36
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 as 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

+109
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,7 @@ struct DrainState<'cfg> {
151157

152158
/// How many jobs we've finished
153159
finished: usize,
160+
per_crate_future_incompat_reports: Vec<FutureIncompatReportCrate>,
154161
}
155162

156163
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -162,6 +169,11 @@ impl std::fmt::Display for JobId {
162169
}
163170
}
164171

172+
struct FutureIncompatReportCrate {
173+
package_id: PackageId,
174+
report: Vec<FutureBreakageItem>,
175+
}
176+
165177
/// A `JobState` is constructed by `JobQueue::run` and passed to `Job::run`. It includes everything
166178
/// necessary to communicate between the main thread and the execution of the job.
167179
///
@@ -228,6 +240,7 @@ enum Message {
228240
FixDiagnostic(diagnostic_server::Message),
229241
Token(io::Result<Acquired>),
230242
Finish(JobId, Artifact, CargoResult<()>),
243+
FutureIncompatReport(JobId, Vec<FutureBreakageItem>),
231244

232245
// This client should get release_raw called on it with one of our tokens
233246
NeedsToken(JobId),
@@ -282,6 +295,11 @@ impl<'a> JobState<'a> {
282295
.push(Message::Finish(self.id, Artifact::Metadata, Ok(())));
283296
}
284297

298+
pub fn future_incompat_report(&self, report: Vec<FutureBreakageItem>) {
299+
self.messages
300+
.push(Message::FutureIncompatReport(self.id, report));
301+
}
302+
285303
/// The rustc underlying this Job is about to acquire a jobserver token (i.e., block)
286304
/// on the passed client.
287305
///
@@ -410,6 +428,7 @@ impl<'cfg> JobQueue<'cfg> {
410428
pending_queue: Vec::new(),
411429
print: DiagnosticPrinter::new(cx.bcx.config),
412430
finished: 0,
431+
per_crate_future_incompat_reports: Vec::new(),
413432
};
414433

415434
// Create a helper thread for acquiring jobserver tokens
@@ -591,6 +610,11 @@ impl<'cfg> DrainState<'cfg> {
591610
}
592611
}
593612
}
613+
Message::FutureIncompatReport(id, report) => {
614+
let package_id = self.active[&id].pkg.package_id();
615+
self.per_crate_future_incompat_reports
616+
.push(FutureIncompatReportCrate { package_id, report });
617+
}
594618
Message::Token(acquired_token) => {
595619
let token = acquired_token.chain_err(|| "failed to acquire jobserver token")?;
596620
self.tokens.push(token);
@@ -771,14 +795,99 @@ impl<'cfg> DrainState<'cfg> {
771795
if !cx.bcx.build_config.build_plan {
772796
// It doesn't really matter if this fails.
773797
drop(cx.bcx.config.shell().status("Finished", message));
798+
self.emit_future_incompat(cx);
774799
}
800+
775801
None
776802
} else {
777803
debug!("queue: {:#?}", self.queue);
778804
Some(internal("finished with jobs still left in the queue"))
779805
}
780806
}
781807

808+
fn emit_future_incompat(&mut self, cx: &mut Context<'_, '_>) {
809+
if cx.bcx.config.cli_unstable().enable_future_incompat_feature
810+
&& !self.per_crate_future_incompat_reports.is_empty()
811+
{
812+
self.per_crate_future_incompat_reports
813+
.sort_by_key(|r| r.package_id);
814+
815+
let crates_and_versions = self
816+
.per_crate_future_incompat_reports
817+
.iter()
818+
.map(|r| r.package_id.to_string())
819+
.collect::<Vec<_>>()
820+
.join(", ");
821+
822+
drop(cx.bcx.config.shell().warn(&format!(
823+
"the following crates contain code that will be rejected by a future version of Rust: {}",
824+
crates_and_versions
825+
)));
826+
827+
let mut full_report = String::new();
828+
let mut rng = thread_rng();
829+
830+
// Generate a short ID to allow detecting if a report gets overwritten
831+
let id: String = std::iter::repeat(())
832+
.map(|()| char::from(rng.sample(Alphanumeric)))
833+
.take(4)
834+
.collect();
835+
836+
for report in std::mem::take(&mut self.per_crate_future_incompat_reports) {
837+
full_report.push_str(&format!(
838+
"The crate `{}` currently triggers the following future incompatibility lints:\n",
839+
report.package_id
840+
));
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+
crate::display_warning_with_error(
872+
"failed to write on-disk future incompat report",
873+
&e,
874+
&mut cx.bcx.config.shell(),
875+
);
876+
}
877+
878+
if cx.bcx.build_config.future_incompat_report {
879+
drop_eprint!(cx.bcx.config, "{}", full_report);
880+
drop(cx.bcx.config.shell().note(
881+
&format!("this report can be shown with `cargo describe-future-incompatibilities -Z future-incompat-report --id {}`", id)
882+
));
883+
} else {
884+
drop(cx.bcx.config.shell().note(
885+
&format!("to see what the problems were, use the option `--future-incompat-report`, or run `cargo describe-future-incompatibilities --id {}`", id)
886+
));
887+
}
888+
}
889+
}
890+
782891
fn handle_error(
783892
&self,
784893
shell: &mut Shell,

0 commit comments

Comments
 (0)