Skip to content

Commit 45699e9

Browse files
committed
Add support for customizing JSON diagnostics from Cargo
Cargo has of #7143 enabled pipelined compilation by default which affects how the compiler is invoked, especially with respect to JSON messages. This, in some testing, has proven to cause quite a few issues with rustbuild's current integration with Cargo. This commit is aimed at adding features to Cargo to solve this issue. This commit adds the ability to customize the stream of JSON messages coming from Cargo. The new feature for Cargo is that it now also mirrors rustc in how you can configure the JSON stream. Multiple `--message-format` arguments are now supported and the value specified is a comma-separated list of directives. In addition to the existing `human`, `short`, and `json` directives these new directives have been added: * `json-render-diagnostics` - instructs Cargo to render rustc diagnostics and only print out JSON messages for artifacts and Cargo things. * `json-diagnostic-short` - indicates that the `rendered` field of rustc diagnostics should use the "short" rendering. * `json-diagnostic-rendered-ansi` - indicates that the `rendered` field of rustc diagnostics should embed ansi color codes. The first option here, `json-render-diagnostics`, will be used by rustbuild unconditionally. Additionally `json-diagnostic-short` will be conditionally used based on the input to rustbuild itself. This should be enough for external tools to customize how Cargo is invoked and how all kinds of JSON diagnostics get printed, and it's thought that we can relatively easily tweak this as necessary to extend it and such.
1 parent fe7b427 commit 45699e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+973
-227
lines changed

CHANGELOG.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@
55

66
### Added
77

8+
- Cargo build pipelining has been enabled by default to leverage more idle CPU
9+
parallelism during builds.
10+
[#7143](https://github.com/rust-lang/cargo/pull/7143)
11+
- The `--message-format` option to Cargo can now be specified multiple times and
12+
accepts a comma-separated list of values. In addition to the previous values
13+
it also now accepts `json-diagnostic-short` and
14+
`json-diagnostic-rendered-ansi` which configures the output coming from rustc
15+
in `json` message mode.
16+
[#7214](https://github.com/rust-lang/cargo/pull/7214)
17+
818
### Changed
919

1020
### Fixed
11-
- (Nightly only): Fixed exponential blowup when using CARGO_BUILD_PIPELINING.
21+
- (Nightly only): Fixed exponential blowup when using `CARGO_BUILD_PIPELINING`.
1222
[#7062](https://github.com/rust-lang/cargo/pull/7062)
1323
- Fixed using the wrong directory when updating git repositories when using
1424
the `git-fetch-with-cli` config option, and the `GIT_DIR` environment

src/cargo/core/compiler/build_config.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ impl BuildConfig {
112112
/// Whether or not the *user* wants JSON output. Whether or not rustc
113113
/// actually uses JSON is decided in `add_error_format`.
114114
pub fn emit_json(&self) -> bool {
115-
self.message_format == MessageFormat::Json
115+
match self.message_format {
116+
MessageFormat::Json { .. } => true,
117+
_ => false,
118+
}
116119
}
117120

118121
pub fn test(&self) -> bool {
@@ -123,7 +126,17 @@ impl BuildConfig {
123126
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124127
pub enum MessageFormat {
125128
Human,
126-
Json,
129+
Json {
130+
/// Whether rustc diagnostics are rendered by cargo or included into the
131+
/// output stream.
132+
render_diagnostics: bool,
133+
/// Whether the `rendered` field of rustc diagnostics are using the
134+
/// "short" rendering.
135+
short: bool,
136+
/// Whether the `rendered` field of rustc diagnostics embed ansi color
137+
/// codes.
138+
ansi: bool,
139+
},
127140
Short,
128141
}
129142

src/cargo/core/compiler/mod.rs

+89-64
Original file line numberDiff line numberDiff line change
@@ -754,27 +754,47 @@ fn add_error_format_and_color(
754754
if pipelined {
755755
json.push_str(",artifacts");
756756
}
757-
if cx.bcx.build_config.message_format == MessageFormat::Short {
758-
json.push_str(",diagnostic-short");
757+
match cx.bcx.build_config.message_format {
758+
MessageFormat::Short | MessageFormat::Json { short: true, .. } => {
759+
json.push_str(",diagnostic-short");
760+
}
761+
_ => {}
759762
}
760763
cmd.arg(json);
761764
} else {
765+
let mut color = true;
762766
match cx.bcx.build_config.message_format {
763767
MessageFormat::Human => (),
764-
MessageFormat::Json => {
768+
MessageFormat::Json {
769+
ansi,
770+
short,
771+
render_diagnostics,
772+
} => {
765773
cmd.arg("--error-format").arg("json");
774+
// If ansi is explicitly requested, enable it. If we're
775+
// rendering diagnostics ourselves then also enable it because
776+
// we'll figure out what to do with the colors later.
777+
if ansi || render_diagnostics {
778+
cmd.arg("--json=diagnostic-rendered-ansi");
779+
}
780+
if short {
781+
cmd.arg("--json=diagnostic-short");
782+
}
783+
color = false;
766784
}
767785
MessageFormat::Short => {
768786
cmd.arg("--error-format").arg("short");
769787
}
770788
}
771789

772-
let color = if cx.bcx.config.shell().supports_color() {
773-
"always"
774-
} else {
775-
"never"
776-
};
777-
cmd.args(&["--color", color]);
790+
if color {
791+
let color = if cx.bcx.config.shell().supports_color() {
792+
"always"
793+
} else {
794+
"never"
795+
};
796+
cmd.args(&["--color", color]);
797+
}
778798
}
779799
Ok(())
780800
}
@@ -1094,9 +1114,8 @@ impl Kind {
10941114
}
10951115

10961116
struct OutputOptions {
1097-
/// Get the `"rendered"` field from the JSON output and display it on
1098-
/// stderr instead of the JSON message.
1099-
extract_rendered_messages: bool,
1117+
/// What format we're emitting from Cargo itself.
1118+
format: MessageFormat,
11001119
/// Look for JSON message that indicates .rmeta file is available for
11011120
/// pipelined compilation.
11021121
look_for_metadata_directive: bool,
@@ -1110,7 +1129,6 @@ struct OutputOptions {
11101129

11111130
impl OutputOptions {
11121131
fn new<'a>(cx: &Context<'a, '_>, unit: &Unit<'a>) -> OutputOptions {
1113-
let extract_rendered_messages = cx.bcx.build_config.message_format != MessageFormat::Json;
11141132
let look_for_metadata_directive = cx.rmeta_required(unit);
11151133
let color = cx.bcx.config.shell().supports_color();
11161134
let cache_cell = if cx.bcx.build_config.cache_messages() {
@@ -1122,7 +1140,7 @@ impl OutputOptions {
11221140
None
11231141
};
11241142
OutputOptions {
1125-
extract_rendered_messages,
1143+
format: cx.bcx.build_config.message_format,
11261144
look_for_metadata_directive,
11271145
color,
11281146
cache_cell,
@@ -1179,55 +1197,66 @@ fn on_stderr_line(
11791197
}
11801198
};
11811199

1182-
// In some modes of compilation Cargo switches the compiler to JSON mode
1183-
// but the user didn't request that so we still want to print pretty rustc
1184-
// colorized diagnostics. In those cases (`extract_rendered_messages`) we
1185-
// take a look at the JSON blob we go, see if it's a relevant diagnostics,
1186-
// and if so forward just that diagnostic for us to print.
1187-
if options.extract_rendered_messages {
1188-
#[derive(serde::Deserialize)]
1189-
struct CompilerMessage {
1190-
rendered: String,
1200+
// Depending on what we're emitting from Cargo itself, we figure out what to
1201+
// do with this JSON message.
1202+
match options.format {
1203+
// In the "human" output formats (human/short) or if diagnostic messages
1204+
// from rustc aren't being included in the output of Cargo's JSON
1205+
// messages then we extract the diagnostic (if present) here and handle
1206+
// it ourselves.
1207+
MessageFormat::Human
1208+
| MessageFormat::Short
1209+
| MessageFormat::Json {
1210+
render_diagnostics: true,
1211+
..
1212+
} => {
1213+
#[derive(serde::Deserialize)]
1214+
struct CompilerMessage {
1215+
rendered: String,
1216+
}
1217+
if let Ok(mut error) = serde_json::from_str::<CompilerMessage>(compiler_message.get()) {
1218+
// state.stderr will add a newline
1219+
if error.rendered.ends_with('\n') {
1220+
error.rendered.pop();
1221+
}
1222+
let rendered = if options.color {
1223+
error.rendered
1224+
} else {
1225+
// Strip only fails if the the Writer fails, which is Cursor
1226+
// on a Vec, which should never fail.
1227+
strip_ansi_escapes::strip(&error.rendered)
1228+
.map(|v| String::from_utf8(v).expect("utf8"))
1229+
.expect("strip should never fail")
1230+
};
1231+
state.stderr(rendered);
1232+
return Ok(());
1233+
}
11911234
}
1192-
if let Ok(mut error) = serde_json::from_str::<CompilerMessage>(compiler_message.get()) {
1193-
// state.stderr will add a newline
1194-
if error.rendered.ends_with('\n') {
1195-
error.rendered.pop();
1235+
1236+
// Remove color information from the rendered string. When pipelining is
1237+
// enabled and/or when cached messages are enabled we're always asking
1238+
// for ANSI colors from rustc, so unconditionally postprocess here and
1239+
// remove ansi color codes.
1240+
MessageFormat::Json { ansi: false, .. } => {
1241+
#[derive(serde::Deserialize, serde::Serialize)]
1242+
struct CompilerMessage {
1243+
rendered: String,
1244+
#[serde(flatten)]
1245+
other: std::collections::BTreeMap<String, serde_json::Value>,
11961246
}
1197-
let rendered = if options.color {
1198-
error.rendered
1199-
} else {
1200-
// Strip only fails if the the Writer fails, which is Cursor
1201-
// on a Vec, which should never fail.
1202-
strip_ansi_escapes::strip(&error.rendered)
1247+
if let Ok(mut error) = serde_json::from_str::<CompilerMessage>(compiler_message.get()) {
1248+
error.rendered = strip_ansi_escapes::strip(&error.rendered)
12031249
.map(|v| String::from_utf8(v).expect("utf8"))
1204-
.expect("strip should never fail")
1205-
};
1206-
state.stderr(rendered);
1207-
return Ok(());
1208-
}
1209-
} else {
1210-
// Remove color information from the rendered string. rustc has not
1211-
// included color in the past, so to avoid breaking anything, strip it
1212-
// out when --json=diagnostic-rendered-ansi is used. This runs
1213-
// unconditionally under the assumption that Cargo will eventually
1214-
// move to this as the default mode. Perhaps in the future, cargo
1215-
// could allow the user to enable/disable color (such as with a
1216-
// `--json` or `--color` or `--message-format` flag).
1217-
#[derive(serde::Deserialize, serde::Serialize)]
1218-
struct CompilerMessage {
1219-
rendered: String,
1220-
#[serde(flatten)]
1221-
other: std::collections::BTreeMap<String, serde_json::Value>,
1222-
}
1223-
if let Ok(mut error) = serde_json::from_str::<CompilerMessage>(compiler_message.get()) {
1224-
error.rendered = strip_ansi_escapes::strip(&error.rendered)
1225-
.map(|v| String::from_utf8(v).expect("utf8"))
1226-
.unwrap_or(error.rendered);
1227-
let new_line = serde_json::to_string(&error)?;
1228-
let new_msg: Box<serde_json::value::RawValue> = serde_json::from_str(&new_line)?;
1229-
compiler_message = new_msg;
1250+
.unwrap_or(error.rendered);
1251+
let new_line = serde_json::to_string(&error)?;
1252+
let new_msg: Box<serde_json::value::RawValue> = serde_json::from_str(&new_line)?;
1253+
compiler_message = new_msg;
1254+
}
12301255
}
1256+
1257+
// If ansi colors are desired then we should be good to go! We can just
1258+
// pass through this message as-is.
1259+
MessageFormat::Json { ansi: true, .. } => {}
12311260
}
12321261

12331262
// In some modes of execution we will execute rustc with `-Z
@@ -1278,12 +1307,8 @@ fn replay_output_cache(
12781307
color: bool,
12791308
) -> Work {
12801309
let target = target.clone();
1281-
let extract_rendered_messages = match format {
1282-
MessageFormat::Human | MessageFormat::Short => true,
1283-
MessageFormat::Json => false,
1284-
};
12851310
let mut options = OutputOptions {
1286-
extract_rendered_messages,
1311+
format,
12871312
look_for_metadata_directive: false,
12881313
color,
12891314
cache_cell: None,

src/cargo/util/command_prelude.rs

+65-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
use std::ffi::{OsStr, OsString};
2-
use std::fs;
3-
use std::path::PathBuf;
4-
51
use crate::core::compiler::{BuildConfig, MessageFormat};
62
use crate::core::Workspace;
73
use crate::ops::{CompileFilter, CompileOptions, NewOptions, Packages, VersionControl};
@@ -14,6 +10,10 @@ use crate::util::{
1410
};
1511
use crate::CargoResult;
1612
use clap::{self, SubCommand};
13+
use failure::bail;
14+
use std::ffi::{OsStr, OsString};
15+
use std::fs;
16+
use std::path::PathBuf;
1717

1818
pub use crate::core::compiler::CompileMode;
1919
pub use crate::{CliError, CliResult, Config};
@@ -134,13 +134,7 @@ pub trait AppExt: Sized {
134134
}
135135

136136
fn arg_message_format(self) -> Self {
137-
self._arg(
138-
opt("message-format", "Error format")
139-
.value_name("FMT")
140-
.case_insensitive(true)
141-
.possible_values(&["human", "json", "short"])
142-
.default_value("human"),
143-
)
137+
self._arg(multi_opt("message-format", "FMT", "Error format"))
144138
}
145139

146140
fn arg_build_plan(self) -> Self {
@@ -301,23 +295,70 @@ pub trait ArgMatchesExt {
301295
self._values_of("package"),
302296
)?;
303297

304-
let message_format = match self._value_of("message-format") {
305-
None => MessageFormat::Human,
306-
Some(f) => {
307-
if f.eq_ignore_ascii_case("json") {
308-
MessageFormat::Json
309-
} else if f.eq_ignore_ascii_case("human") {
310-
MessageFormat::Human
311-
} else if f.eq_ignore_ascii_case("short") {
312-
MessageFormat::Short
313-
} else {
314-
panic!("Impossible message format: {:?}", f)
298+
let mut message_format = None;
299+
let default_json = MessageFormat::Json {
300+
short: false,
301+
ansi: false,
302+
render_diagnostics: false,
303+
};
304+
for fmt in self._values_of("message-format") {
305+
for fmt in fmt.split(',') {
306+
let fmt = fmt.to_ascii_lowercase();
307+
match fmt.as_str() {
308+
"json" => {
309+
if message_format.is_some() {
310+
bail!("cannot specify two kinds of `message-format` arguments");
311+
}
312+
message_format = Some(default_json);
313+
}
314+
"human" => {
315+
if message_format.is_some() {
316+
bail!("cannot specify two kinds of `message-format` arguments");
317+
}
318+
message_format = Some(MessageFormat::Human);
319+
}
320+
"short" => {
321+
if message_format.is_some() {
322+
bail!("cannot specify two kinds of `message-format` arguments");
323+
}
324+
message_format = Some(MessageFormat::Short);
325+
}
326+
"json-render-diagnostics" => {
327+
if message_format.is_none() {
328+
message_format = Some(default_json);
329+
}
330+
match &mut message_format {
331+
Some(MessageFormat::Json {
332+
render_diagnostics, ..
333+
}) => *render_diagnostics = true,
334+
_ => bail!("cannot specify two kinds of `message-format` arguments"),
335+
}
336+
}
337+
"json-diagnostic-short" => {
338+
if message_format.is_none() {
339+
message_format = Some(default_json);
340+
}
341+
match &mut message_format {
342+
Some(MessageFormat::Json { short, .. }) => *short = true,
343+
_ => bail!("cannot specify two kinds of `message-format` arguments"),
344+
}
345+
}
346+
"json-diagnostic-rendered-ansi" => {
347+
if message_format.is_none() {
348+
message_format = Some(default_json);
349+
}
350+
match &mut message_format {
351+
Some(MessageFormat::Json { ansi, .. }) => *ansi = true,
352+
_ => bail!("cannot specify two kinds of `message-format` arguments"),
353+
}
354+
}
355+
s => bail!("invalid message format specifier: `{}`", s),
315356
}
316357
}
317-
};
358+
}
318359

319360
let mut build_config = BuildConfig::new(config, self.jobs()?, &self.target(), mode)?;
320-
build_config.message_format = message_format;
361+
build_config.message_format = message_format.unwrap_or(MessageFormat::Human);
321362
build_config.release = self._is_present("release");
322363
build_config.build_plan = self._is_present("build-plan");
323364
if build_config.build_plan {

0 commit comments

Comments
 (0)