Skip to content
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

Emit errors with cargo:error= #11312

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 110 additions & 48 deletions src/cargo/core/compiler/custom_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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+" => {
Comment on lines +409 to +411
Copy link
Contributor

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?

Copy link
Contributor Author

@kornelski kornelski Nov 1, 2022

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= or cargo:warning=. Long lines are unclear, and repeated cargo: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-escaping println!("\\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 keeping cargo: prefix seems safe, and = vs += should be intuitive to Rust programmers.

Copy link
Contributor

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.

Copy link
Contributor

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.

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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" => {
Expand Down Expand Up @@ -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.
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't made cargo:error fail the compilation, which remains dependent on the script's exit code. This way this feature is technically backwards-compatible with crates that might have used cargo:error as their custom key-value metadata.

This is referring to

cargo:KEY=VALUE — Metadata, used by links scripts.

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

Treat the script as having errored whether or not it returns an error code.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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())),
Expand All @@ -752,7 +814,7 @@ impl BuildOutput {
metadata,
rerun_if_changed,
rerun_if_env_changed,
warnings,
diagnostics,
})
}

Expand Down
30 changes: 9 additions & 21 deletions src/cargo/core/compiler/job_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is a change of behavior

Copy link
Contributor Author

@kornelski kornelski Nov 1, 2022

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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)?,
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions src/cargo/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,8 @@ fn _display_error(err: &Error, shell: &mut Shell, as_err: bool) -> bool {
for (i, err) in err.chain().enumerate() {
// If we're not in verbose mode then only print cause chain until one
// marked as `VerboseError` appears.
//
// Generally the top error shouldn't be verbose, but check it anyways.
if shell.verbosity() != Verbose && err.is::<VerboseError>() {
drop(shell.note("for more details, run again with '--verbose'"));
return true;
}
if err.is::<AlreadyPrintedError>() {
Expand Down
48 changes: 47 additions & 1 deletion tests/testsuite/build_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down