Skip to content

Commit 189dfbc

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 b778fca commit 189dfbc

File tree

11 files changed

+410
-29
lines changed

11 files changed

+410
-29
lines changed

rust/private/rust.bzl

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -953,8 +953,8 @@ def _common_attrs_for_binary_without_process_wrapper(attrs):
953953

954954
return new_attr
955955

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

973+
rust_library_without_process_wrapper = rule(
974+
implementation = _rust_library_impl,
975+
provides = _common_providers,
976+
attrs = dict(_common_attrs_for_binary_without_process_wrapper(_common_attrs).items()),
977+
fragments = ["cpp"],
978+
host_fragments = ["cpp"],
979+
toolchains = [
980+
str(Label("//rust:toolchain")),
981+
"@bazel_tools//tools/cpp:toolchain_type",
982+
],
983+
incompatible_use_toolchain_transition = True,
984+
)
985+
973986
rust_test = rule(
974987
implementation = _rust_test_impl,
975988
provides = _common_providers,

rust/repositories.bzl

Lines changed: 10 additions & 0 deletions
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 = "rules_rust_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

Lines changed: 16 additions & 0 deletions
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

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

util/process_wrapper/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ rust_binary_without_process_wrapper(
88
srcs = glob(["*.rs"]),
99
edition = "2018",
1010
visibility = ["//visibility:public"],
11+
deps = [
12+
"@rules_rust_tinyjson//:tinyjson",
13+
],
1114
)
1215

1316
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("@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

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,108 @@
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};
2124

2225
use crate::options::options;
26+
use crate::output::{process_output, LineOutput};
27+
28+
#[cfg(windows)]
29+
fn status_code(status: ExitStatus, was_killed: bool) -> i32 {
30+
// On windows, there's no good way to know if the process was killed by a signal.
31+
// If we killed the process, we override the code to signal success.
32+
if was_killed {
33+
0
34+
} else {
35+
status.code().unwrap_or(1)
36+
}
37+
}
38+
39+
#[cfg(not(windows))]
40+
fn status_code(status: ExitStatus, was_killed: bool) -> i32 {
41+
// On unix, if code is None it means that the process was killed by a signal.
42+
// https://doc.rust-lang.org/std/process/struct.ExitStatus.html#method.success
43+
match status.code() {
44+
Some(code) => code,
45+
// If we killed the process, we expect None here
46+
None if was_killed => 0,
47+
// Otherwise it's some unexpected signal
48+
None => 1,
49+
}
50+
}
2351

2452
fn main() {
2553
let opts = match options() {
2654
Err(err) => panic!("process wrapper error: {}", err),
2755
Ok(v) => v,
2856
};
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()
57+
58+
let stderr: Box<dyn io::Write + Send> = if let Some(stderr_file) = opts.stderr_file {
59+
Box::new(
60+
OpenOptions::new()
61+
.create(true)
62+
.truncate(true)
63+
.write(true)
64+
.open(stderr_file)
65+
.expect("process wrapper error: unable to open stderr file"),
66+
)
4867
} else {
49-
Stdio::inherit()
68+
Box::new(io::stderr())
5069
};
51-
let status = Command::new(opts.executable)
70+
71+
let mut child = Command::new(opts.executable)
5272
.args(opts.child_arguments)
5373
.env_clear()
5474
.envs(opts.child_environment)
55-
.stdout(stdout)
56-
.stderr(stderr)
57-
.status()
75+
.stdout(if let Some(stdout_file) = opts.stdout_file {
76+
OpenOptions::new()
77+
.create(true)
78+
.truncate(true)
79+
.write(true)
80+
.open(stdout_file)
81+
.expect("process wrapper error: unable to open stdout file")
82+
.into()
83+
} else {
84+
Stdio::inherit()
85+
})
86+
.stderr(Stdio::piped())
87+
.spawn()
5888
.expect("process wrapper error: failed to spawn child process");
5989

60-
if status.success() {
90+
let child_stderr = Box::new(child.stderr.take().unwrap());
91+
92+
let mut was_killed = false;
93+
let result = if !opts.rustc_quit_on_rmeta {
94+
// Process output normally by forwarding stderr
95+
process_output(child_stderr, stderr, LineOutput::Message)
96+
} else {
97+
let format = opts.rustc_output_format;
98+
let mut kill = false;
99+
let result = process_output(child_stderr, stderr, |line| {
100+
rustc::stop_on_rmeta_completion(line, format, &mut kill)
101+
});
102+
if kill {
103+
// If recv returns Ok(), a signal was sent in this channel so we should terminate the child process.
104+
// We can safely ignore the Result from kill() as we don't care if the process already terminated.
105+
let _ = child.kill();
106+
was_killed = true;
107+
}
108+
result
109+
};
110+
result.expect("process wrapper error: failed to process stderr");
111+
112+
let status = child
113+
.wait()
114+
.expect("process wrapper error: failed to wait for child process");
115+
// If the child process is rustc and is killed after metadata generation, that's also a success.
116+
let code = status_code(status, was_killed);
117+
let success = code == 0;
118+
if success {
61119
if let Some(tf) = opts.touch_file {
62120
OpenOptions::new()
63121
.create(true)
@@ -75,5 +133,5 @@ fn main() {
75133
}
76134
}
77135

78-
exit(status.code().unwrap())
136+
exit(code)
79137
}

0 commit comments

Comments
 (0)