-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Emit errors with cargo:error= #11312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ use crate::core::{profiles::ProfileRoot, PackageId, Target}; | |
use crate::util::errors::CargoResult; | ||
use crate::util::machine_message::{self, Message}; | ||
use crate::util::{internal, profile}; | ||
use crate::VerboseError; | ||
use anyhow::{bail, Context as _}; | ||
use cargo_platform::Cfg; | ||
use cargo_util::paths; | ||
|
@@ -16,7 +17,7 @@ use std::path::{Path, PathBuf}; | |
use std::str; | ||
use std::sync::{Arc, Mutex}; | ||
|
||
const CARGO_WARNING: &str = "cargo:warning="; | ||
const CARGO_LINE_PREFIX: &str = "cargo:"; | ||
|
||
/// Contains the parsed output of a custom build script. | ||
#[derive(Clone, Debug, Hash, Default)] | ||
|
@@ -40,11 +41,21 @@ pub struct BuildOutput { | |
pub rerun_if_changed: Vec<PathBuf>, | ||
/// Environment variables which, when changed, will cause a rebuild. | ||
pub rerun_if_env_changed: Vec<String>, | ||
/// Warnings generated by this build. | ||
/// Warnings and errors generated by this build. | ||
/// | ||
/// These are only displayed if this is a "local" package, `-vv` is used, | ||
/// Warnings are only displayed if this is a "local" package, `-vv` is used, | ||
/// or there is a build error for any target in this package. | ||
pub warnings: Vec<String>, | ||
/// | ||
/// Fatal errors are treated as the root cause of the build script failure, | ||
/// and suppress other output unless `-vv` is used. | ||
pub diagnostics: Vec<Diagnostic>, | ||
} | ||
|
||
/// Warnings or errors in `BuildOutput` | ||
#[derive(Clone, Debug, Hash)] | ||
pub enum Diagnostic { | ||
Warning(String), | ||
Error(String), | ||
} | ||
|
||
/// Map of packages to build script output. | ||
|
@@ -299,6 +310,7 @@ fn build_work(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Job> { | |
}) | ||
.collect::<Vec<_>>(); | ||
let library_name = unit.pkg.library().map(|t| t.crate_name()); | ||
let package_name = unit.pkg.name(); | ||
let pkg_descr = unit.pkg.to_string(); | ||
let build_script_outputs = Arc::clone(&cx.build_script_outputs); | ||
let id = unit.pkg.package_id(); | ||
|
@@ -385,39 +397,82 @@ fn build_work(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Job> { | |
state.running(&cmd); | ||
let timestamp = paths::set_invocation_time(&script_run_dir)?; | ||
let prefix = format!("[{} {}] ", id.name(), id.version()); | ||
let mut warnings_in_case_of_panic = Vec::new(); | ||
let output = cmd | ||
.exec_with_streaming( | ||
&mut |stdout| { | ||
if let Some(warning) = stdout.strip_prefix(CARGO_WARNING) { | ||
warnings_in_case_of_panic.push(warning.to_owned()); | ||
} | ||
if extra_verbose { | ||
state.stdout(format!("{}{}", prefix, stdout))?; | ||
} | ||
Ok(()) | ||
}, | ||
&mut |stderr| { | ||
if extra_verbose { | ||
state.stderr(format!("{}{}", prefix, stderr))?; | ||
let mut diagnostics = Vec::new(); | ||
let script_result = cmd.exec_with_streaming( | ||
&mut |stdout| { | ||
let kv = stdout.strip_prefix(CARGO_LINE_PREFIX).and_then(|kv| { | ||
let mut kv = kv.splitn(2, '='); | ||
Some((kv.next()?, kv.next()?)) | ||
}); | ||
if let Some((key, value)) = kv { | ||
match key { | ||
"warning" => diagnostics.push(Diagnostic::Warning(value.to_owned())), | ||
"error" => diagnostics.push(Diagnostic::Error(value.to_owned())), | ||
"warning+" => { | ||
if let Some(Diagnostic::Warning(msg)) = diagnostics.last_mut() { | ||
msg.push_str("\n "); | ||
msg.push_str(value); | ||
} | ||
} | ||
"error+" => { | ||
if let Some(Diagnostic::Error(msg)) = diagnostics.last_mut() { | ||
msg.push_str("\n "); | ||
msg.push_str(value); | ||
} | ||
} | ||
_ => {} | ||
} | ||
Ok(()) | ||
}, | ||
true, | ||
) | ||
.with_context(|| format!("failed to run custom build command for `{}`", pkg_descr)); | ||
epage marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if let Err(error) = output { | ||
insert_warnings_in_build_outputs( | ||
build_script_outputs, | ||
id, | ||
metadata_hash, | ||
warnings_in_case_of_panic, | ||
); | ||
return Err(error); | ||
} | ||
} | ||
if extra_verbose { | ||
state.stdout(format!("{}{}", prefix, stdout))?; | ||
} | ||
Ok(()) | ||
}, | ||
&mut |stderr| { | ||
if extra_verbose { | ||
state.stderr(format!("{}{}", prefix, stderr))?; | ||
} | ||
Ok(()) | ||
}, | ||
true, | ||
); | ||
|
||
let output = match script_result { | ||
Ok(output) => output, | ||
Err(mut error) => { | ||
let errors_seen = diagnostics | ||
.iter() | ||
.filter(|diag| matches!(diag, Diagnostic::Error { .. })) | ||
.count(); | ||
let warnings_seen = diagnostics.len() - errors_seen; | ||
if errors_seen > 0 { | ||
// Hides the full stdout/stderr printout unless verbose flag is used | ||
error = anyhow::Error::from(VerboseError::new(error)); | ||
} | ||
|
||
let errors = match errors_seen { | ||
0 => " due to a custom build script failure".to_string(), | ||
1 => " due to the previous error".to_string(), | ||
count => format!(" due to {count} previous errors"), | ||
}; | ||
let warnings = match warnings_seen { | ||
0 => String::new(), | ||
1 => "; 1 warning emitted".to_string(), | ||
count => format!("; {count} warnings emitted"), | ||
}; | ||
Comment on lines
+453
to
+462
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be covering more of these cases in tests? |
||
|
||
insert_warnings_in_build_outputs( | ||
build_script_outputs, | ||
id, | ||
metadata_hash, | ||
diagnostics, | ||
); | ||
|
||
let output = output.unwrap(); | ||
return Err(error.context(format!( | ||
"could not build `{package_name}`{errors}{warnings}" | ||
))); | ||
} | ||
}; | ||
|
||
// After the build command has finished running, we need to be sure to | ||
// remember all of its output so we can later discover precisely what it | ||
|
@@ -500,10 +555,10 @@ fn insert_warnings_in_build_outputs( | |
build_script_outputs: Arc<Mutex<BuildScriptOutputs>>, | ||
id: PackageId, | ||
metadata_hash: Metadata, | ||
warnings: Vec<String>, | ||
diagnostics: Vec<Diagnostic>, | ||
) { | ||
let build_output_with_only_warnings = BuildOutput { | ||
warnings, | ||
diagnostics, | ||
..BuildOutput::default() | ||
}; | ||
build_script_outputs | ||
|
@@ -559,7 +614,7 @@ impl BuildOutput { | |
let mut metadata = Vec::new(); | ||
let mut rerun_if_changed = Vec::new(); | ||
let mut rerun_if_env_changed = Vec::new(); | ||
let mut warnings = Vec::new(); | ||
let mut diagnostics = Vec::new(); | ||
let whence = format!("build script of `{}`", pkg_descr); | ||
|
||
for line in input.split(|b| *b == b'\n') { | ||
|
@@ -624,16 +679,15 @@ impl BuildOutput { | |
"rustc-link-search" => library_paths.push(PathBuf::from(value)), | ||
"rustc-link-arg-cdylib" | "rustc-cdylib-link-arg" => { | ||
if !targets.iter().any(|target| target.is_cdylib()) { | ||
warnings.push(format!( | ||
"cargo:{} was specified in the build script of {}, \ | ||
diagnostics.push(Diagnostic::Warning(format!( | ||
"cargo:{key} was specified in the build script of {pkg_descr}, \ | ||
but that package does not contain a cdylib target\n\ | ||
\n\ | ||
Allowing this was an unintended change in the 1.50 \ | ||
release, and may become an error in the future. \ | ||
For more information, see \ | ||
<https://github.com/rust-lang/cargo/issues/9562>.", | ||
key, pkg_descr | ||
)); | ||
))); | ||
} | ||
linker_args.push((LinkType::Cdylib, value)) | ||
} | ||
|
@@ -685,7 +739,9 @@ impl BuildOutput { | |
if extra_check_cfg { | ||
check_cfgs.push(value.to_string()); | ||
} else { | ||
warnings.push(format!("cargo:{} requires -Zcheck-cfg=output flag", key)); | ||
diagnostics.push(Diagnostic::Warning(format!( | ||
"cargo:{key} requires -Zcheck-cfg=output flag", | ||
))); | ||
} | ||
} | ||
"rustc-env" => { | ||
|
@@ -716,10 +772,9 @@ impl BuildOutput { | |
if nightly_features_allowed | ||
|| rustc_bootstrap_allows(library_name.as_deref()) | ||
{ | ||
warnings.push(format!("Cannot set `RUSTC_BOOTSTRAP={}` from {}.\n\ | ||
diagnostics.push(Diagnostic::Warning(format!("Cannot set `RUSTC_BOOTSTRAP={val}` from {whence}.\n\ | ||
note: Crates cannot set `RUSTC_BOOTSTRAP` themselves, as doing so would subvert the stability guarantees of Rust for your project.", | ||
val, whence | ||
)); | ||
))); | ||
epage marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
// Setting RUSTC_BOOTSTRAP would change the behavior of the crate. | ||
// Abort with an error. | ||
|
@@ -735,7 +790,14 @@ impl BuildOutput { | |
env.push((key, val)); | ||
} | ||
} | ||
"warning" => warnings.push(value.to_string()), | ||
// "error" is not parsed here for backwards compatibility, and because this function is for successful outputs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is referring to
See https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script That is a bit of a compatibility trap as theoretically any new directive is a breaking change. The previous direction from the cargo team was
I feel like not doing this is a behavior trap for users. I would recommend re-raising this in the Issue and for us to come up with a compatible solution that allows erroring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've elaborated on the design here: #10159 (comment) |
||
"warning" => diagnostics.push(Diagnostic::Warning(value.to_string())), | ||
"warning+" => { | ||
if let Some(Diagnostic::Warning(msg)) = diagnostics.last_mut() { | ||
msg.push_str("\n "); | ||
msg.push_str(&value); | ||
} | ||
} | ||
"rerun-if-changed" => rerun_if_changed.push(PathBuf::from(value)), | ||
"rerun-if-env-changed" => rerun_if_env_changed.push(value.to_string()), | ||
_ => metadata.push((key.to_string(), value.to_string())), | ||
|
@@ -752,7 +814,7 @@ impl BuildOutput { | |
metadata, | ||
rerun_if_changed, | ||
rerun_if_env_changed, | ||
warnings, | ||
diagnostics, | ||
}) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,7 @@ use log::{debug, trace}; | |
use semver::Version; | ||
|
||
use super::context::OutputFile; | ||
use super::custom_build::Diagnostic; | ||
use super::job::{ | ||
Freshness::{self, Dirty, Fresh}, | ||
Job, | ||
|
@@ -783,8 +784,7 @@ impl<'cfg> DrainState<'cfg> { | |
match result { | ||
Ok(()) => self.finish(id, &unit, artifact, cx)?, | ||
Err(error) => { | ||
let msg = "The following warnings were emitted during compilation:"; | ||
self.emit_warnings(Some(msg), &unit, cx)?; | ||
Comment on lines
-786
to
-787
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: this is a change of behavior There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary? AFAIK there's no such message for regular (rustc) compilation errors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My intention wasn't to say "this is a bad change of behavior" but to just raise awareness of how the message is changing. We used to have an introductory statement in this case but will no longer. I felt this would be worth highlighting when this gets a wider view during FCP |
||
self.emit_diagnostics(&unit, cx)?; | ||
self.back_compat_notice(cx, &unit)?; | ||
return Err(ErrorToHandle { | ||
error, | ||
|
@@ -1153,31 +1153,19 @@ impl<'cfg> DrainState<'cfg> { | |
} | ||
} | ||
|
||
fn emit_warnings( | ||
&mut self, | ||
msg: Option<&str>, | ||
unit: &Unit, | ||
cx: &mut Context<'_, '_>, | ||
) -> CargoResult<()> { | ||
/// Print warnings and errors | ||
fn emit_diagnostics(&mut self, unit: &Unit, cx: &mut Context<'_, '_>) -> CargoResult<()> { | ||
let outputs = cx.build_script_outputs.lock().unwrap(); | ||
let metadata = match cx.find_build_script_metadata(unit) { | ||
Some(metadata) => metadata, | ||
None => return Ok(()), | ||
}; | ||
let bcx = &mut cx.bcx; | ||
if let Some(output) = outputs.get(metadata) { | ||
if !output.warnings.is_empty() { | ||
if let Some(msg) = msg { | ||
writeln!(bcx.config.shell().err(), "{}\n", msg)?; | ||
} | ||
|
||
for warning in output.warnings.iter() { | ||
bcx.config.shell().warn(warning)?; | ||
} | ||
|
||
if msg.is_some() { | ||
// Output an empty line. | ||
writeln!(bcx.config.shell().err())?; | ||
for diag in output.diagnostics.iter() { | ||
match diag { | ||
Diagnostic::Warning(warning) => bcx.config.shell().warn(warning)?, | ||
Diagnostic::Error(error) => bcx.config.shell().error(error)?, | ||
} | ||
} | ||
} | ||
|
@@ -1278,7 +1266,7 @@ impl<'cfg> DrainState<'cfg> { | |
cx: &mut Context<'_, '_>, | ||
) -> CargoResult<()> { | ||
if unit.mode.is_run_custom_build() && unit.show_warnings(cx.bcx.config) { | ||
self.emit_warnings(None, unit, cx)?; | ||
self.emit_diagnostics(unit, cx)?; | ||
} | ||
let unlocked = self.queue.finish(unit, &artifact); | ||
match artifact { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,14 +38,60 @@ fn custom_build_script_failed() { | |
[COMPILING] foo v0.5.0 ([CWD]) | ||
[RUNNING] `rustc --crate-name build_script_build build.rs [..]--crate-type bin [..]` | ||
[RUNNING] `[..]/build-script-build` | ||
[ERROR] failed to run custom build command for `foo v0.5.0 ([CWD])` | ||
[ERROR] could not build `foo` due to a custom build script failure | ||
|
||
Caused by: | ||
process didn't exit successfully: `[..]/build-script-build` (exit [..]: 101)", | ||
) | ||
.run(); | ||
} | ||
|
||
#[cargo_test] | ||
fn custom_build_script_failed_custom_error() { | ||
let p = project() | ||
.file( | ||
"Cargo.toml", | ||
r#" | ||
[package] | ||
|
||
name = "foo" | ||
version = "0.5.0" | ||
authors = ["[email protected]"] | ||
build = "build.rs" | ||
"#, | ||
) | ||
.file("src/main.rs", "fn main() {}") | ||
.file( | ||
"build.rs", | ||
r#"fn main() { | ||
println!("cargo:error=failed"); | ||
println!("cargo:error+=because oops"); | ||
println!("cargo:warning=multi"); | ||
println!("cargo:warning+=line"); | ||
println!("cargo:warning+=warning"); | ||
println!("cargo:error=error2"); | ||
println!("cargo:warning+=this one has nothing to append to"); | ||
std::process::exit(101); | ||
}"#, | ||
) | ||
.build(); | ||
p.cargo("build") | ||
.with_status(101) | ||
.with_stderr( | ||
"\ | ||
[COMPILING] foo v0.5.0 ([CWD]) | ||
[ERROR] failed | ||
because oops | ||
[WARNING] multi | ||
line | ||
warning | ||
[ERROR] error2 | ||
[ERROR] could not build `foo` due to 2 previous errors; 1 warning emitted | ||
[NOTE] for more details, run again with '--verbose'", | ||
) | ||
.run(); | ||
} | ||
|
||
#[cargo_test] | ||
fn custom_build_env_vars() { | ||
let p = project() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we split this out into a separate Issue/PR so we can make sure we are agreed on the semantics we want for this before going to the review stage and so we don't block your other work on it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean just the
+=
syntax?I've considered alternatives:
do nothing. Authors of crates like openssl-sys have a lot of explaining to do, so they will either have to cram everything into a one long line, or they will emit lots of lines with
cargo:error=
orcargo:warning=
. Long lines are unclear, and repeatedcargo:warning=
directives give noisy formatting that is inconsistent with rustc error output.support line breaks with a character sequence inside a one-liner message. No obvious sequence comes to my mind. Something recognizable like
\n
would be ambiguous with line breaks in string literals and require double-escapingprintln!("\\n")
and escaping of backslashes inside the error message. It's a hassle.there could be some other line prefix syntax for "append to previous line" instead of
error+=
, e.g. start next line with a tab character (like HTTP/1.1 headers)"\tsecond line"
or some other syntax like"...second line"
, but keepingcargo:
prefix seems safe, and=
vs+=
should be intuitive to Rust programmers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes,
+=
syntax. While discussing alternatives can be interesting, again I feel its best that we have a dedicated discussion on this, so please split this out. This is not strictly required for merging error messages.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was discussed in the cargo team meeting. We came up with several concerns and it would be best to have a dedicated Issue for finishing the conversation on them.