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

Prevent building cargo from invalidating build cache of other tools due to conditionally applied -Zon-broken-pipe=kill via tracked RUSTFLAGS #131155

Merged
merged 2 commits into from
Oct 9, 2024
Merged
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
6 changes: 6 additions & 0 deletions src/bootstrap/src/bin/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ fn main() {
cmd.args(lint_flags.split_whitespace());
}

// Conditionally pass `-Zon-broken-pipe=kill` to underlying rustc. Not all binaries want
// `-Zon-broken-pipe=kill`, which includes cargo itself.
if env::var_os("FORCE_ON_BROKEN_PIPE_KILL").is_some() {
cmd.arg("-Z").arg("on-broken-pipe=kill");
}

if target.is_some() {
// The stage0 compiler has a special sysroot distinct from what we
// actually downloaded, so we just always pass the `--sysroot` option,
Expand Down
15 changes: 13 additions & 2 deletions src/bootstrap/src/core/build_steps/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,8 +1053,19 @@ pub fn rustc_cargo(

cargo.rustdocflag("-Zcrate-attr=warn(rust_2018_idioms)");

// If the rustc output is piped to e.g. `head -n1` we want the process to be
// killed, rather than having an error bubble up and cause a panic.
// If the rustc output is piped to e.g. `head -n1` we want the process to be killed, rather than
// having an error bubble up and cause a panic.
//
// FIXME(jieyouxu): this flag is load-bearing for rustc to not ICE on broken pipes, because
// rustc internally sometimes uses std `println!` -- but std `println!` by default will panic on
// broken pipes, and uncaught panics will manifest as an ICE. The compiler *should* handle this
// properly, but this flag is set in the meantime to paper over the I/O errors.
//
// See <https://github.com/rust-lang/rust/issues/131059> for details.
//
// Also see the discussion for properly handling I/O errors related to broken pipes, i.e. safe
// variants of `println!` in
// <https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/Internal.20lint.20for.20raw.20.60print!.60.20and.20.60println!.60.3F>.
cargo.rustflag("-Zon-broken-pipe=kill");

if builder.config.llvm_enzyme {
Expand Down
25 changes: 21 additions & 4 deletions src/bootstrap/src/core/build_steps/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,28 @@ pub fn prepare_tool_cargo(
// See https://github.com/rust-lang/rust/issues/116538
cargo.rustflag("-Zunstable-options");

// `-Zon-broken-pipe=kill` breaks cargo tests
// NOTE: The root cause of needing `-Zon-broken-pipe=kill` in the first place is because `rustc`
// and `rustdoc` doesn't gracefully handle I/O errors due to usages of raw std `println!` macros
// which panics upon encountering broken pipes. `-Zon-broken-pipe=kill` just papers over that
// and stops rustc/rustdoc ICEing on e.g. `rustc --print=sysroot | false`.
//
// cargo explicitly does not want the `-Zon-broken-pipe=kill` paper because it does actually use
// variants of `println!` that handles I/O errors gracefully. It's also a breaking change for a
// spawn process not written in Rust, especially if the language default handler is not
// `SIG_IGN`. Thankfully cargo tests will break if we do set the flag.
//
// For the cargo discussion, see
// <https://rust-lang.zulipchat.com/#narrow/stream/246057-t-cargo/topic/Applying.20.60-Zon-broken-pipe.3Dkill.60.20flags.20in.20bootstrap.3F>.
//
// For the rustc discussion, see
// <https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/Internal.20lint.20for.20raw.20.60print!.60.20and.20.60println!.60.3F>
// for proper solutions.
if !path.ends_with("cargo") {
// If the output is piped to e.g. `head -n1` we want the process to be killed,
// rather than having an error bubble up and cause a panic.
cargo.rustflag("-Zon-broken-pipe=kill");
// Use an untracked env var `FORCE_ON_BROKEN_PIPE_KILL` here instead of `RUSTFLAGS`.
// `RUSTFLAGS` is tracked by cargo. Conditionally omitting `-Zon-broken-pipe=kill` from
// `RUSTFLAGS` causes unnecessary tool rebuilds due to cache invalidation from building e.g.
// cargo *without* `-Zon-broken-pipe=kill` but then rustdoc *with* `-Zon-broken-pipe=kill`.
cargo.env("FORCE_ON_BROKEN_PIPE_KILL", "-Zon-broken-pipe=kill");
}

cargo
Expand Down
73 changes: 73 additions & 0 deletions tests/run-make/broken-pipe-no-ice/rmake.rs
jieyouxu marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Check that `rustc` and `rustdoc` does not ICE upon encountering a broken pipe due to unhandled
//! panics from raw std `println!` usages.
//!
//! Regression test for <https://github.com/rust-lang/rust/issues/34376>.

//@ ignore-cross-compile (needs to run test binary)

#![feature(anonymous_pipe)]

use std::io::Read;
use std::process::{Command, Stdio};

use run_make_support::env_var;

#[derive(Debug, PartialEq)]
enum Binary {
Rustc,
Rustdoc,
}

fn check_broken_pipe_handled_gracefully(bin: Binary, mut cmd: Command) {
let (reader, writer) = std::pipe::pipe().unwrap();
drop(reader); // close read-end
cmd.stdout(writer).stderr(Stdio::piped());

let mut child = cmd.spawn().unwrap();

let mut stderr = String::new();
child.stderr.as_mut().unwrap().read_to_string(&mut stderr).unwrap();
let status = child.wait().unwrap();

assert!(!status.success(), "{bin:?} unexpectedly succeeded");

const PANIC_ICE_EXIT_CODE: i32 = 101;

#[cfg(not(windows))]
{
// On non-Windows, rustc/rustdoc built with `-Zon-broken-pipe=kill` shouldn't have an exit
// code of 101 because it should have an wait status that corresponds to SIGPIPE signal
// number.
assert_ne!(status.code(), Some(PANIC_ICE_EXIT_CODE), "{bin:?}");
// And the stderr should be empty because rustc/rustdoc should've gotten killed.
assert!(stderr.is_empty(), "{bin:?} stderr:\n{}", stderr);
}

#[cfg(windows)]
{
match bin {
// On Windows, rustc has a paper that propagates the panic exit code of 101 but converts
// broken pipe errors into fatal errors instead of ICEs.
Comment on lines +49 to +50
Copy link
Member

Choose a reason for hiding this comment

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

You could try changing the windows hack to a early_fatal error:

let _ = early_dcx.early_err(msg.clone());
I think that would result in the exit code being 1.

Copy link
Member Author

@jieyouxu jieyouxu Oct 8, 2024

Choose a reason for hiding this comment

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

I'll do that in a follow-up, I would like to stop run-make cargo rebuilds lol.
EDIT: actually I don't want to make the hack even hackier, this should be addressed by fixing the underlying cause and not add paper to the paper.

Copy link
Member

Choose a reason for hiding this comment

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

Fair!

Binary::Rustc => {
assert_eq!(status.code(), Some(PANIC_ICE_EXIT_CODE), "{bin:?}");
// But make sure it doesn't manifest as an ICE.
assert!(!stderr.contains("internal compiler error"), "{bin:?} ICE'd");
}
// On Windows, rustdoc seems to cleanly exit with exit code of 1.
Binary::Rustdoc => {
assert_eq!(status.code(), Some(1), "{bin:?}");
assert!(!stderr.contains("panic"), "{bin:?} stderr contains panic");
}
}
}
}

fn main() {
let mut rustc = Command::new(env_var("RUSTC"));
rustc.arg("--print=sysroot");
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: this is technically not robust because I exploited the fact that --print=sysroot and friends usually use the raw println! which will panic upon encountering a broken pipe when rustc is not built with -Zon-broken-pipe=kill. Some outputs in rustc_driver_impl are correctly guarded by using safe_println! where they won't panic upon encountering a broken pipe even if -Zon-broken-pipe=kill is unset.

Copy link
Member

Choose a reason for hiding this comment

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

but if -Zon-broken-pipe=kill is unset, safe_println will still emit an IO error, right? which is wrong

Copy link
Member Author

@jieyouxu jieyouxu Oct 3, 2024

Choose a reason for hiding this comment

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

but if -Zon-broken-pipe=kill is unset, safe_println will still emit an IO error, right? which is wrong

Yeah idk how specifically that will be handled, but that is left as an exercise for the future reader. Actually I think to clarify, I believe the safe_println in rustc_driver_impl is to convert the panic if -Zon-broken-pipe=kill is unset -> fatal error, just that it avoids an ICE, not necessarily the behavior one would want...

check_broken_pipe_handled_gracefully(Binary::Rustc, rustc);

let mut rustdoc = Command::new(env_var("RUSTDOC"));
rustdoc.arg("--version");
check_broken_pipe_handled_gracefully(Binary::Rustdoc, rustdoc);
}
Loading