Skip to content

Commit 0343ba7

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 41b39f0 commit 0343ba7

File tree

11 files changed

+355
-29
lines changed

11 files changed

+355
-29
lines changed

rust/private/rust.bzl

+15-2
Original file line numberDiff line numberDiff line change
@@ -952,8 +952,8 @@ def _common_attrs_for_binary_without_process_wrapper(attrs):
952952

953953
return new_attr
954954

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

972+
rust_library_without_process_wrapper = rule(
973+
implementation = _rust_library_impl,
974+
provides = _common_providers,
975+
attrs = dict(_common_attrs_for_binary_without_process_wrapper(_common_attrs).items()),
976+
fragments = ["cpp"],
977+
host_fragments = ["cpp"],
978+
toolchains = [
979+
str(Label("//rust:toolchain")),
980+
"@bazel_tools//tools/cpp:toolchain_type",
981+
],
982+
incompatible_use_toolchain_transition = True,
983+
)
984+
972985
rust_test = rule(
973986
implementation = _rust_test_impl,
974987
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 = "@rules_rust//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("@rules_rust//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 = ["@rules_rust//util/process_wrapper:__pkg__"],
8+
)

util/process_wrapper/main.rs

+91-27
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,114 @@
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()
59+
60+
let stderr: Box<dyn io::Write + Send> = if let Some(stderr_file) = opts.stderr_file {
61+
Box::new(
62+
OpenOptions::new()
63+
.create(true)
64+
.truncate(true)
65+
.write(true)
66+
.open(stderr_file)
67+
.expect("process wrapper error: unable to open stderr file"),
68+
)
4869
} else {
49-
Stdio::inherit()
70+
Box::new(io::stderr())
5071
};
51-
let status = Command::new(opts.executable)
72+
73+
let mut child = Command::new(opts.executable)
5274
.args(opts.child_arguments)
5375
.env_clear()
5476
.envs(opts.child_environment)
55-
.stdout(stdout)
56-
.stderr(stderr)
57-
.status()
77+
.stdout(if let Some(stdout_file) = opts.stdout_file {
78+
OpenOptions::new()
79+
.create(true)
80+
.truncate(true)
81+
.write(true)
82+
.open(stdout_file)
83+
.expect("process wrapper error: unable to open stdout file")
84+
.into()
85+
} else {
86+
Stdio::inherit()
87+
})
88+
.stderr(Stdio::piped())
89+
.spawn()
5890
.expect("process wrapper error: failed to spawn child process");
5991

60-
if status.success() {
92+
93+
let child_stderr = Box::new(child.stderr.take().unwrap());
94+
95+
let mut was_killed = false;
96+
let stderr_thread = if !opts.rustc_quit_on_rmeta {
97+
// Process output normally by forwarding stderr
98+
process_output(child_stderr, stderr, Some)
99+
} else {
100+
let format = opts.rustc_output_format;
101+
// Process json rustc output and kill the subprocess when we get a signal
102+
// that we emitted a metadata file.
103+
// This receiver will block until a corresponding send happens.
104+
let (stop_sender, stop) = sync_channel(0);
105+
let thread = process_output(child_stderr, stderr, move |line| {
106+
rustc::stop_on_rmeta_completion(line, format, &stop_sender)
107+
});
108+
if stop.recv().is_ok() {
109+
// If recv returns Ok(), a signal was sent in this channel so we should terminate the child process.
110+
// We can safely ignore the Result from kill() as we don't care if the process already terminated.
111+
let _ = child.kill();
112+
was_killed = true;
113+
}
114+
thread
115+
};
116+
stderr_thread.join().unwrap().unwrap();
117+
118+
let status = child
119+
.wait()
120+
.expect("process wrapper error: failed to wait for child process");
121+
// If the child process is rustc and is killed after metadata generation, that's also a success.
122+
let code = status_code(status, was_killed);
123+
let success = code == 0;
124+
if success {
61125
if let Some(tf) = opts.touch_file {
62126
OpenOptions::new()
63127
.create(true)
@@ -75,5 +139,5 @@ fn main() {
75139
}
76140
}
77141

78-
exit(status.code().unwrap())
142+
exit(code)
79143
}

0 commit comments

Comments
 (0)