Skip to content

Commit d2423a5

Browse files
committed
process_wrapper: add support for terminating rustc after it emits rmeta.
This is a key component of supporting pipelining in rules_rust. The bazel rules will (in a follow-up PR) configure rustc to run either to completion for rules whose dependencies require the full rlib files or until they emit the rmeta files if dependencies only require that. This is safe to commit as there are no changes to user-visible behavior until the new flags are used.
1 parent c078494 commit d2423a5

File tree

11 files changed

+299
-30
lines changed

11 files changed

+299
-30
lines changed

WORKSPACE.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ http_archive(
5353
url = "https://github.com/bazelbuild/continuous-integration/releases/download/rules-1.0.0/bazelci_rules-1.0.0.tar.gz",
5454
)
5555

56+
http_archive(
57+
name = "tinyjson",
58+
build_file = "@//util/process_wrapper:BUILD.tinyjson.bazel",
59+
sha256 = "9c21866c7f051ebcefd028996494a374b7408ef946826cefc9761d58cce0fd36",
60+
strip_prefix = "tinyjson-2.3.0",
61+
url = "https://github.com/rhysd/tinyjson/archive/refs/tags/v2.3.0.zip",
62+
)
63+
5664
# To run with RBE on Bazel CI, uncomment the following lines.
5765
#
5866
# load("@bazelci_rules//:rbe_repo.bzl", "rbe_preconfig")

rust/private/rust.bzl

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -939,8 +939,8 @@ def _fake_out_process_wrapper(attrs):
939939
)
940940
return new_attr
941941

942-
# Provides an internal rust_binary to use that we can use to build the process
943-
# wrapper, this breaks the dependency of rust_binary on the process wrapper by
942+
# Provides an internal rust_{binary,library} to use that we can use to build the process
943+
# wrapper, this breaks the dependency of rust_* on the process wrapper by
944944
# setting it to None, which the functions in rustc detect and build accordingly.
945945
rust_binary_without_process_wrapper = rule(
946946
implementation = _rust_binary_impl,
@@ -956,6 +956,19 @@ rust_binary_without_process_wrapper = rule(
956956
incompatible_use_toolchain_transition = True,
957957
)
958958

959+
rust_library_without_process_wrapper = rule(
960+
implementation = _rust_library_impl,
961+
provides = _common_providers,
962+
attrs = dict(_fake_out_process_wrapper(_common_attrs).items()),
963+
fragments = ["cpp"],
964+
host_fragments = ["cpp"],
965+
toolchains = [
966+
str(Label("//rust:toolchain")),
967+
"@bazel_tools//tools/cpp:toolchain_type",
968+
],
969+
incompatible_use_toolchain_transition = True,
970+
)
971+
959972
rust_test = rule(
960973
implementation = _rust_test_impl,
961974
provides = _common_providers,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("//rust:defs.bzl", "rust_test")
2+
3+
sh_binary(
4+
name = "fake_rustc",
5+
srcs = ["fake_rustc.sh"],
6+
)
7+
8+
rust_test(
9+
name = "rustc_quit_on_rmeta",
10+
srcs = ["rustc_quit_on_rmeta.rs"],
11+
data = [
12+
":fake_rustc",
13+
"//util/process_wrapper",
14+
],
15+
deps = ["//tools/runfiles"],
16+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
# Redirect stdout to stderr
3+
exec >&2
4+
5+
echo '{"rendered": "I am a fake rustc\nvery very fake"}'
6+
echo '{"emit": "metadata"}'
7+
sleep 1s
8+
echo '{"rendered": "I should not print this"}'
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use std::env;
2+
use std::fs::read_to_string;
3+
use std::path::Path;
4+
use std::process::Command;
5+
use std::str;
6+
7+
use runfiles::Runfiles;
8+
9+
fn fake_rustc(process_wrapper_args: &[&'static str]) -> String {
10+
let r = Runfiles::create().unwrap();
11+
let fake_rustc = r.rlocation("rules_rust/test/rustc_quit_on_rmeta/fake_rustc");
12+
let process_wrapper = r.rlocation("rules_rust/util/process_wrapper/process_wrapper");
13+
let stderr_file_path = Path::new(&env::var("TEST_TMPDIR").unwrap()).join("stdout.txt");
14+
15+
let output = Command::new(process_wrapper)
16+
.args(process_wrapper_args)
17+
.arg("--stderr-file")
18+
.arg(&stderr_file_path)
19+
.arg("--")
20+
.arg(fake_rustc)
21+
.output().unwrap();
22+
23+
assert!(
24+
output.status.success(),
25+
"unable to run process_wrapper: {} {}",
26+
str::from_utf8(&output.stdout).unwrap(),
27+
str::from_utf8(&output.stderr).unwrap(),
28+
);
29+
30+
read_to_string(stderr_file_path).unwrap()
31+
}
32+
33+
#[test]
34+
fn test_rustc_quit_on_rmeta_quits() {
35+
let out_content = fake_rustc(&["--rustc-quit-on-rmeta", "true"]);
36+
assert!(!out_content.contains("I should not print this"), "output should not contain 'I should not print this' but did: {}", out_content);
37+
}
38+
39+
#[test]
40+
fn test_rustc_quit_on_rmeta_output_format() {
41+
let json_content = fake_rustc(&["--rustc-quit-on-rmeta", "true", "--rustc-output-format", "json"]);
42+
assert_eq!(json_content, concat!(r#"{"rendered": "I am a fake rustc\nvery very fake"}"#, "\n"));
43+
let rendered_content = fake_rustc(&["--rustc-quit-on-rmeta", "true", "--rustc-output-format", "rendered"]);
44+
assert_eq!(rendered_content, "I am a fake rustc\nvery very fake\n");
45+
}

util/process_wrapper/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ rust_binary_without_process_wrapper(
77
name = "process_wrapper",
88
srcs = glob(["*.rs"]),
99
visibility = ["//visibility:public"],
10+
deps = [
11+
"@tinyjson",
12+
],
1013
)
1114

1215
rust_test(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# buildifier: disable=bzl-visibility
2+
load("@//rust/private:rust.bzl", "rust_library_without_process_wrapper")
3+
4+
rust_library_without_process_wrapper(
5+
name = "tinyjson",
6+
srcs = glob(["src/*.rs"]),
7+
visibility = ["@//util/process_wrapper:__pkg__"],
8+
)

util/process_wrapper/main.rs

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414

1515
mod flags;
1616
mod options;
17+
mod output;
18+
mod rustc;
1719
mod util;
1820

1921
use std::fs::{copy, OpenOptions};
22+
use std::io;
2023
use std::process::{exit, Command, Stdio};
24+
use std::sync::mpsc::sync_channel;
25+
26+
use output::process_output;
2127

2228
use crate::options::options;
2329

@@ -26,38 +32,83 @@ fn main() {
2632
Err(err) => panic!("process wrapper error: {}", err),
2733
Ok(v) => v,
2834
};
29-
let stdout = if let Some(stdout_file) = opts.stdout_file {
30-
OpenOptions::new()
31-
.create(true)
32-
.truncate(true)
33-
.write(true)
34-
.open(stdout_file)
35-
.expect("process wrapper error: unable to open stdout file")
36-
.into()
37-
} else {
38-
Stdio::inherit()
39-
};
40-
let stderr = if let Some(stderr_file) = opts.stderr_file {
41-
OpenOptions::new()
42-
.create(true)
43-
.truncate(true)
44-
.write(true)
45-
.open(stderr_file)
46-
.expect("process wrapper error: unable to open stderr file")
47-
.into()
48-
} else {
49-
Stdio::inherit()
50-
};
51-
let status = Command::new(opts.executable)
35+
36+
let mut child = Command::new(opts.executable)
5237
.args(opts.child_arguments)
5338
.env_clear()
5439
.envs(opts.child_environment)
55-
.stdout(stdout)
56-
.stderr(stderr)
57-
.status()
40+
.stdout(Stdio::piped())
41+
.stderr(Stdio::piped())
42+
.spawn()
5843
.expect("process wrapper error: failed to spawn child process");
5944

60-
if status.success() {
45+
let stdout: Box<dyn io::Write + Send> = if let Some(stdout_file) = opts.stdout_file {
46+
Box::new(
47+
OpenOptions::new()
48+
.create(true)
49+
.truncate(true)
50+
.write(true)
51+
.open(stdout_file)
52+
.expect("process wrapper error: unable to open stdout file"),
53+
)
54+
} else {
55+
Box::new(io::stdout())
56+
};
57+
let stderr: Box<dyn io::Write + Send> = if let Some(stderr_file) = opts.stderr_file {
58+
Box::new(
59+
OpenOptions::new()
60+
.create(true)
61+
.truncate(true)
62+
.write(true)
63+
.open(stderr_file)
64+
.expect("process wrapper error: unable to open stderr file"),
65+
)
66+
} else {
67+
Box::new(io::stderr())
68+
};
69+
70+
let child_stdout = Box::new(child.stdout.take().unwrap());
71+
let child_stderr = Box::new(child.stderr.take().unwrap());
72+
73+
if !opts.rustc_quit_on_rmeta {
74+
// Process output normally by forwarding stdout and stderr
75+
let stdout_thread = process_output(child_stdout, stdout, |line| Some(line));
76+
let stderr_thread = process_output(child_stderr, stderr, |line| Some(line));
77+
stdout_thread.join().unwrap().unwrap();
78+
stderr_thread.join().unwrap().unwrap();
79+
} else {
80+
let format = opts.rustc_output_format;
81+
// Process json rustc output and kill the subprocess when we get a signal
82+
// that we emitted a metadata file.
83+
// This receiver will block until a corresponding send happens.
84+
let (stop_sender_stdout, stop) = sync_channel(0);
85+
let stop_sender_stderr = stop_sender_stdout.clone();
86+
let stdout_thread = process_output(child_stdout, stdout, move |line| {
87+
rustc::process_message(line, format, &stop_sender_stdout)
88+
});
89+
let stderr_thread = process_output(child_stderr, stderr, move |line| {
90+
rustc::process_message(line, format, &stop_sender_stderr)
91+
});
92+
if let Ok(_) = stop.recv() {
93+
// If recv returns Ok(), a signal was sent in this channel so we should terminate the child process.
94+
// We can safely ignore the Result from kill() as we don't care if the process already terminated.
95+
let _ = child.kill();
96+
}
97+
98+
stdout_thread.join().unwrap().unwrap();
99+
stderr_thread.join().unwrap().unwrap();
100+
}
101+
102+
let status = child
103+
.wait()
104+
.expect("process wrapper error: failed to wait for child process");
105+
// If the child process is rustc and is killed after metadata generation, that's also a success.
106+
let (code, success) = match status.code() {
107+
// According to rust docs: Signal termination is not considered a success, and success is defined as a zero exit status.
108+
Some(code) => (code, code == 0),
109+
None => (0, true),
110+
};
111+
if success {
61112
if let Some(tf) = opts.touch_file {
62113
OpenOptions::new()
63114
.create(true)
@@ -75,5 +126,5 @@ fn main() {
75126
}
76127
}
77128

78-
exit(status.code().unwrap())
129+
exit(code)
79130
}

util/process_wrapper/options.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::fmt;
44
use std::process::exit;
55

66
use crate::flags::{FlagParseError, Flags, ParseOutcome};
7+
use crate::rustc;
78
use crate::util::*;
89

910
#[derive(Debug)]
@@ -38,6 +39,12 @@ pub(crate) struct Options {
3839
pub(crate) stdout_file: Option<String>,
3940
// If set, redirects the child process stderr to this file.
4041
pub(crate) stderr_file: Option<String>,
42+
// If set, it configures rustc to emit an rmeta file and then
43+
// quit.
44+
pub(crate) rustc_quit_on_rmeta: bool,
45+
// If rustc_quit_on_rmeta is set to true, this controls the
46+
// output format of rustc messages.
47+
pub(crate) rustc_output_format: rustc::Output,
4148
}
4249

4350
pub(crate) fn options() -> Result<Options, OptionError> {
@@ -51,6 +58,8 @@ pub(crate) fn options() -> Result<Options, OptionError> {
5158
let mut copy_output_raw = None;
5259
let mut stdout_file = None;
5360
let mut stderr_file = None;
61+
let mut rustc_quit_on_rmeta_raw = None;
62+
let mut rustc_output_format_raw = None;
5463
let mut flags = Flags::new();
5564
flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw);
5665
flags.define_flag("--volatile-status-file", "", &mut volatile_status_file_raw);
@@ -80,6 +89,19 @@ pub(crate) fn options() -> Result<Options, OptionError> {
8089
"Redirect subprocess stderr in this file.",
8190
&mut stderr_file,
8291
);
92+
flags.define_flag(
93+
"--rustc-quit-on-rmeta",
94+
"If enabled, this wrapper will terminate rustc after rmeta has been emitted.",
95+
&mut rustc_quit_on_rmeta_raw,
96+
);
97+
flags.define_flag(
98+
"--rustc-output-format",
99+
"Controls the rustc output format if --rustc-quit-on-rmeta is set.\n\
100+
'json' will cause the json output to be output, \
101+
'rendered' will extract the rendered message and print that.\n\
102+
Default: `rendered`",
103+
&mut rustc_output_format_raw,
104+
);
83105

84106
let mut child_args = match flags
85107
.parse(env::args().collect())
@@ -138,6 +160,19 @@ pub(crate) fn options() -> Result<Options, OptionError> {
138160
})
139161
.transpose()?;
140162

163+
let rustc_quit_on_rmeta = rustc_quit_on_rmeta_raw.map_or(false, |s| s == "true");
164+
let rustc_output_format = rustc_output_format_raw
165+
.map(|v| match v.as_str() {
166+
"json" => Ok(rustc::Output::Json),
167+
"rendered" => Ok(rustc::Output::Rendered),
168+
_ => Err(OptionError::Generic(format!(
169+
"invalid --rustc-output-format '{}'",
170+
v
171+
))),
172+
})
173+
.transpose()?
174+
.unwrap_or_default();
175+
141176
// Prepare the environment variables, unifying those read from files with the ones
142177
// of the current process.
143178
let vars = environment_block(environment_file_block, &stamp_mappings, &subst_mappings);
@@ -159,6 +194,8 @@ pub(crate) fn options() -> Result<Options, OptionError> {
159194
copy_output,
160195
stdout_file,
161196
stderr_file,
197+
rustc_quit_on_rmeta,
198+
rustc_output_format,
162199
})
163200
}
164201

util/process_wrapper/output.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::io::{self, prelude::*};
2+
use std::thread;
3+
4+
pub(crate) fn process_output<F>(
5+
read_end: Box<dyn Read + Send>,
6+
write_end: Box<dyn Write + Send>,
7+
mut process_line: F,
8+
) -> thread::JoinHandle<io::Result<()>>
9+
where
10+
F: FnMut(String) -> Option<String> + Send + 'static,
11+
{
12+
thread::spawn(move || {
13+
let mut reader = io::BufReader::new(read_end);
14+
let mut writer = io::LineWriter::new(write_end);
15+
loop {
16+
let mut line = String::new();
17+
let read_bytes = reader.read_line(&mut line)?;
18+
if read_bytes == 0 {
19+
break;
20+
}
21+
if let Some(to_write) = process_line(line) {
22+
writer.write_all(&to_write.into_bytes())?;
23+
}
24+
}
25+
Ok(())
26+
})
27+
}

0 commit comments

Comments
 (0)