Skip to content

Commit 1d273f2

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 1d273f2

File tree

12 files changed

+377
-31
lines changed

12 files changed

+377
-31
lines changed

WORKSPACE.bazel

+8
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,11 @@ http_archive(
7070
load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
7171

7272
rules_pkg_dependencies()
73+
74+
http_archive(
75+
name = "tinyjson",
76+
build_file = "@//util/process_wrapper:BUILD.tinyjson.bazel",
77+
sha256 = "9c21866c7f051ebcefd028996494a374b7408ef946826cefc9761d58cce0fd36",
78+
strip_prefix = "tinyjson-2.3.0",
79+
url = "https://github.com/rhysd/tinyjson/archive/refs/tags/v2.3.0.zip",
80+
)

rust/private/rust.bzl

+15-2
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,

rust/repositories.bzl

+10
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ def rules_rust_dependencies():
5959
url = "https://github.com/bazelbuild/apple_support/releases/download/0.11.0/apple_support.0.11.0.tar.gz",
6060
)
6161

62+
# process_wrapper needs a low-dependency way to process json.
63+
maybe(
64+
http_archive,
65+
name = "tinyjson",
66+
sha256 = "9c21866c7f051ebcefd028996494a374b7408ef946826cefc9761d58cce0fd36",
67+
url = "https://github.com/rhysd/tinyjson/archive/refs/tags/v2.3.0.zip",
68+
strip_prefix = "tinyjson-2.3.0",
69+
build_file = "@//util/process_wrapper:BUILD.tinyjson.bazel",
70+
)
71+
6272
# buildifier: disable=unnamed-macro
6373
def rust_register_toolchains(
6474
dev_components = False,

test/process_wrapper/BUILD.bazel

+16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
load("@bazel_skylib//rules:build_test.bzl", "build_test")
22
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
33
load("@rules_cc//cc:defs.bzl", "cc_binary")
4+
load("//rust:defs.bzl", "rust_binary", "rust_test")
45
load("//test/process_wrapper:process_wrapper_tester.bzl", "process_wrapper_tester")
56

67
cc_binary(
@@ -148,3 +149,18 @@ build_test(
148149
":process_wrapper_combined",
149150
],
150151
)
152+
153+
rust_binary(
154+
name = "fake_rustc",
155+
srcs = ["fake_rustc.rs"],
156+
)
157+
158+
rust_test(
159+
name = "rustc_quit_on_rmeta",
160+
srcs = ["rustc_quit_on_rmeta.rs"],
161+
data = [
162+
":fake_rustc",
163+
"//util/process_wrapper",
164+
],
165+
deps = ["//tools/runfiles"],
166+
)

test/process_wrapper/fake_rustc.rs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fn main() {
2+
eprintln!(r#"{{"rendered": "I am a fake rustc\nvery very fake"}}"#);
3+
eprintln!(r#"{{"emit": "metadata"}}"#);
4+
std::thread::sleep(std::time::Duration::from_secs(1));
5+
eprintln!(r#"{{"rendered": "I should not print this"}}"#);
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use std::path::PathBuf;
2+
use std::process::Command;
3+
use std::str;
4+
5+
use runfiles::Runfiles;
6+
7+
fn fake_rustc(process_wrapper_args: &[&'static str]) -> String {
8+
let r = Runfiles::create().unwrap();
9+
let fake_rustc = r.rlocation(
10+
&[
11+
"rules_rust",
12+
"test",
13+
"process_wrapper",
14+
if cfg!(unix) {
15+
"fake_rustc"
16+
} else {
17+
"fake_rustc.exe"
18+
},
19+
]
20+
.iter()
21+
.collect::<PathBuf>(),
22+
);
23+
24+
let process_wrapper = r.rlocation(
25+
&[
26+
"rules_rust",
27+
"util",
28+
"process_wrapper",
29+
if cfg!(unix) {
30+
"process_wrapper"
31+
} else {
32+
"process_wrapper.exe"
33+
},
34+
]
35+
.iter()
36+
.collect::<PathBuf>(),
37+
);
38+
39+
let output = Command::new(process_wrapper)
40+
.args(process_wrapper_args)
41+
.arg("--")
42+
.arg(fake_rustc)
43+
.output()
44+
.unwrap();
45+
46+
assert!(
47+
output.status.success(),
48+
"unable to run process_wrapper: {} {}",
49+
str::from_utf8(&output.stdout).unwrap(),
50+
str::from_utf8(&output.stderr).unwrap(),
51+
);
52+
53+
String::from_utf8(output.stderr).unwrap()
54+
}
55+
56+
#[test]
57+
fn test_rustc_quit_on_rmeta_quits() {
58+
let out_content = fake_rustc(&["--rustc-quit-on-rmeta", "true"]);
59+
assert!(
60+
!out_content.contains("I should not print this"),
61+
"output should not contain 'I should not print this' but did: {}",
62+
out_content
63+
);
64+
}
65+
66+
#[test]
67+
fn test_rustc_quit_on_rmeta_output_json() {
68+
let json_content = fake_rustc(&[
69+
"--rustc-quit-on-rmeta",
70+
"true",
71+
"--rustc-output-format",
72+
"json",
73+
]);
74+
assert_eq!(
75+
json_content,
76+
concat!(r#"{"rendered": "I am a fake rustc\nvery very fake"}"#, "\n")
77+
);
78+
}
79+
80+
#[test]
81+
fn test_rustc_quit_on_rmeta_output_rendered() {
82+
let rendered_content = fake_rustc(&[
83+
"--rustc-quit-on-rmeta",
84+
"true",
85+
"--rustc-output-format",
86+
"rendered",
87+
]);
88+
assert_eq!(rendered_content, "I am a fake rustc\nvery very fake");
89+
}

util/process_wrapper/BUILD.bazel

+3
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(
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

+105-29
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,126 @@
1414

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

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

2228
use crate::options::options;
2329

30+
#[cfg(windows)]
31+
fn status_code(status: ExitStatus, was_killed: bool) -> i32 {
32+
// On windows, there's no good way to know if the process was killed by a signal.
33+
// If we killed the process, we override the code to signal success.
34+
if was_killed {
35+
0
36+
} else {
37+
status.code().unwrap_or(1)
38+
}
39+
}
40+
41+
#[cfg(not(windows))]
42+
fn status_code(status: ExitStatus, was_killed: bool) -> i32 {
43+
// On unix, if code is None it means that the process was killed by a signal.
44+
// https://doc.rust-lang.org/std/process/struct.ExitStatus.html#method.success
45+
match status.code() {
46+
Some(code) => code,
47+
// If we killed the process, we expect None here
48+
None if was_killed => 0,
49+
// Otherwise it's some unexpected signal
50+
None => 1,
51+
}
52+
}
53+
2454
fn main() {
2555
let opts = match options() {
2656
Err(err) => panic!("process wrapper error: {}", err),
2757
Ok(v) => v,
2858
};
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)
59+
60+
let mut child = Command::new(opts.executable)
5261
.args(opts.child_arguments)
5362
.env_clear()
5463
.envs(opts.child_environment)
55-
.stdout(stdout)
56-
.stderr(stderr)
57-
.status()
64+
.stdout(Stdio::piped())
65+
.stderr(Stdio::piped())
66+
.spawn()
5867
.expect("process wrapper error: failed to spawn child process");
5968

60-
if status.success() {
69+
let stdout: Box<dyn io::Write + Send> = if let Some(stdout_file) = opts.stdout_file {
70+
Box::new(
71+
OpenOptions::new()
72+
.create(true)
73+
.truncate(true)
74+
.write(true)
75+
.open(stdout_file)
76+
.expect("process wrapper error: unable to open stdout file"),
77+
)
78+
} else {
79+
Box::new(io::stdout())
80+
};
81+
let stderr: Box<dyn io::Write + Send> = if let Some(stderr_file) = opts.stderr_file {
82+
Box::new(
83+
OpenOptions::new()
84+
.create(true)
85+
.truncate(true)
86+
.write(true)
87+
.open(stderr_file)
88+
.expect("process wrapper error: unable to open stderr file"),
89+
)
90+
} else {
91+
Box::new(io::stderr())
92+
};
93+
94+
let child_stdout = Box::new(child.stdout.take().unwrap());
95+
let child_stderr = Box::new(child.stderr.take().unwrap());
96+
97+
let was_killed = if !opts.rustc_quit_on_rmeta {
98+
// Process output normally by forwarding stdout and stderr
99+
let stdout_thread = process_output(child_stdout, stdout, Some);
100+
let stderr_thread = process_output(child_stderr, stderr, Some);
101+
stdout_thread.join().unwrap().unwrap();
102+
stderr_thread.join().unwrap().unwrap();
103+
false
104+
} else {
105+
let mut was_killed = false;
106+
let format = opts.rustc_output_format;
107+
// Process json rustc output and kill the subprocess when we get a signal
108+
// that we emitted a metadata file.
109+
// This receiver will block until a corresponding send happens.
110+
let (stop_sender_stdout, stop) = sync_channel(0);
111+
let stop_sender_stderr = stop_sender_stdout.clone();
112+
let stdout_thread = process_output(child_stdout, stdout, move |line| {
113+
rustc::process_message(line, format, &stop_sender_stdout)
114+
});
115+
let stderr_thread = process_output(child_stderr, stderr, move |line| {
116+
rustc::process_message(line, format, &stop_sender_stderr)
117+
});
118+
if stop.recv().is_ok() {
119+
// If recv returns Ok(), a signal was sent in this channel so we should terminate the child process.
120+
// We can safely ignore the Result from kill() as we don't care if the process already terminated.
121+
let _ = child.kill();
122+
was_killed = true;
123+
}
124+
125+
stdout_thread.join().unwrap().unwrap();
126+
stderr_thread.join().unwrap().unwrap();
127+
was_killed
128+
};
129+
130+
let status = child
131+
.wait()
132+
.expect("process wrapper error: failed to wait for child process");
133+
// If the child process is rustc and is killed after metadata generation, that's also a success.
134+
let code = status_code(status, was_killed);
135+
let success = code == 0;
136+
if success {
61137
if let Some(tf) = opts.touch_file {
62138
OpenOptions::new()
63139
.create(true)
@@ -75,5 +151,5 @@ fn main() {
75151
}
76152
}
77153

78-
exit(status.code().unwrap())
154+
exit(code)
79155
}

0 commit comments

Comments
 (0)