blob: 0920c747a8bc3e3ab91393da9008f019eb030699 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001//! A utility for writing scripts for use as test executables intended to match the
2//! subcommands of Bazel build actions so `rustdoc --test`, which builds and tests
3//! code in a single call, can be run as a test target in a hermetic manner.
4
5use std::cmp::Reverse;
6use std::collections::{BTreeMap, BTreeSet};
7use std::env;
8use std::fs;
Brian Silverman5f6f2762022-08-13 19:30:05 -07009use std::io::{BufRead, BufReader};
Brian Silvermancc09f182022-03-09 15:40:20 -080010use std::path::{Path, PathBuf};
11
12#[derive(Debug)]
13struct Options {
14 /// A list of environment variable keys to parse from the build action env.
15 env_keys: BTreeSet<String>,
16
17 /// A list of substrings to strip from [Options::action_argv].
18 strip_substrings: Vec<String>,
19
20 /// The path where the script should be written.
21 output: PathBuf,
22
Brian Silverman5f6f2762022-08-13 19:30:05 -070023 /// If Bazel generated a params file, we may need to strip roots from it.
24 /// This is the path where we will output our stripped params file.
25 optional_output_params_file: PathBuf,
26
Brian Silvermancc09f182022-03-09 15:40:20 -080027 /// The `argv` of the configured rustdoc build action.
28 action_argv: Vec<String>,
29}
30
31/// Parse command line arguments
32fn parse_args() -> Options {
33 let args: Vec<String> = env::args().into_iter().collect();
34 let (writer_args, action_args) = {
35 let split = args
36 .iter()
37 .position(|arg| arg == "--")
38 .expect("Unable to find split identifier `--`");
39
40 // Converting each set into a vector makes them easier to parse in
41 // the absence of nightly features
42 let (writer, action) = args.split_at(split);
43 (writer.to_vec(), action.to_vec())
44 };
45
46 // Remove the leading `--` which is expected to be the first
47 // item in `action_args`
48 debug_assert_eq!(action_args[0], "--");
49 let action_argv = action_args[1..].to_vec();
50
51 let output = writer_args
52 .iter()
53 .find(|arg| arg.starts_with("--output="))
54 .and_then(|arg| arg.splitn(2, '=').last())
55 .map(PathBuf::from)
56 .expect("Missing `--output` argument");
57
Brian Silverman5f6f2762022-08-13 19:30:05 -070058 let optional_output_params_file = writer_args
59 .iter()
60 .find(|arg| arg.starts_with("--optional_test_params="))
61 .and_then(|arg| arg.splitn(2, '=').last())
62 .map(PathBuf::from)
63 .expect("Missing `--optional_test_params` argument");
64
Brian Silvermancc09f182022-03-09 15:40:20 -080065 let (strip_substring_args, writer_args): (Vec<String>, Vec<String>) = writer_args
66 .into_iter()
67 .partition(|arg| arg.starts_with("--strip_substring="));
68
69 let mut strip_substrings: Vec<String> = strip_substring_args
70 .into_iter()
71 .map(|arg| {
72 arg.splitn(2, '=')
73 .last()
74 .expect("--strip_substring arguments must have assignments using `=`")
75 .to_owned()
76 })
77 .collect();
78
79 // Strip substrings should always be in reverse order of the length of each
80 // string so when filtering we know that the longer strings are checked
81 // first in order to avoid cases where shorter strings might match longer ones.
82 strip_substrings.sort_by_key(|b| Reverse(b.len()));
83 strip_substrings.dedup();
84
85 let env_keys = writer_args
86 .into_iter()
87 .filter(|arg| arg.starts_with("--action_env="))
88 .map(|arg| {
89 arg.splitn(2, '=')
90 .last()
91 .expect("--env arguments must have assignments using `=`")
92 .to_owned()
93 })
94 .collect();
95
96 Options {
97 env_keys,
98 strip_substrings,
99 output,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700100 optional_output_params_file,
Brian Silvermancc09f182022-03-09 15:40:20 -0800101 action_argv,
102 }
103}
104
Brian Silverman5f6f2762022-08-13 19:30:05 -0700105/// Expand the Bazel Arg file and write it into our manually defined params file
106fn expand_params_file(mut options: Options) -> Options {
107 let params_extension = if cfg!(target_family = "windows") {
108 ".rustdoc_test.bat-0.params"
109 } else {
110 ".rustdoc_test.sh-0.params"
111 };
112
113 // We always need to produce the params file, we might overwrite this later though
114 fs::write(&options.optional_output_params_file, b"unused")
115 .expect("Failed to write params file");
116
117 // extract the path for the params file, if it exists
118 let params_path = match options.action_argv.pop() {
119 // Found the params file!
120 Some(arg) if arg.starts_with('@') && arg.ends_with(params_extension) => {
121 let path_str = arg
122 .strip_prefix('@')
123 .expect("Checked that there is an @ prefix");
124 PathBuf::from(path_str)
125 }
126 // No params file present, exit early
127 Some(arg) => {
128 options.action_argv.push(arg);
129 return options;
130 }
131 None => return options,
132 };
133
134 // read the params file
135 let params_file = fs::File::open(params_path).expect("Failed to read the rustdoc params file");
136 let content: Vec<_> = BufReader::new(params_file)
137 .lines()
138 .map(|line| line.expect("failed to parse param as String"))
139 // Remove any substrings found in the argument
140 .map(|arg| {
141 let mut stripped_arg = arg;
142 options
143 .strip_substrings
144 .iter()
145 .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
146 stripped_arg
147 })
148 .collect();
149
150 // add all arguments
151 fs::write(&options.optional_output_params_file, content.join("\n"))
152 .expect("Failed to write test runner");
153
154 // append the path of our new params file
155 let formatted_params_path = format!(
156 "@{}",
157 options
158 .optional_output_params_file
159 .to_str()
160 .expect("invalid UTF-8")
161 );
162 options.action_argv.push(formatted_params_path);
163
164 options
165}
166
Brian Silvermancc09f182022-03-09 15:40:20 -0800167/// Write a unix compatible test runner
168fn write_test_runner_unix(
169 path: &Path,
170 env: &BTreeMap<String, String>,
171 argv: &[String],
172 strip_substrings: &[String],
173) {
174 let mut content = vec![
175 "#!/usr/bin/env bash".to_owned(),
176 "".to_owned(),
177 "exec env - \\".to_owned(),
178 ];
179
180 content.extend(env.iter().map(|(key, val)| format!("{}='{}' \\", key, val)));
181
182 let argv_str = argv
183 .iter()
184 // Remove any substrings found in the argument
185 .map(|arg| {
186 let mut stripped_arg = arg.to_owned();
187 strip_substrings
188 .iter()
189 .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
190 stripped_arg
191 })
192 .map(|arg| format!("'{}'", arg))
193 .collect::<Vec<String>>()
194 .join(" ");
195
196 content.extend(vec![argv_str, "".to_owned()]);
197
198 fs::write(path, content.join("\n")).expect("Failed to write test runner");
199}
200
201/// Write a windows compatible test runner
202fn write_test_runner_windows(
203 path: &Path,
204 env: &BTreeMap<String, String>,
205 argv: &[String],
206 strip_substrings: &[String],
207) {
208 let env_str = env
209 .iter()
210 .map(|(key, val)| format!("$env:{}='{}'", key, val))
211 .collect::<Vec<String>>()
212 .join(" ; ");
213
214 let argv_str = argv
215 .iter()
216 // Remove any substrings found in the argument
217 .map(|arg| {
218 let mut stripped_arg = arg.to_owned();
219 strip_substrings
220 .iter()
221 .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
222 stripped_arg
223 })
224 .map(|arg| format!("'{}'", arg))
225 .collect::<Vec<String>>()
226 .join(" ");
227
228 let content = vec![
229 "@ECHO OFF".to_owned(),
230 "".to_owned(),
231 format!("powershell.exe -c \"{} ; & {}\"", env_str, argv_str),
232 "".to_owned(),
233 ];
234
235 fs::write(path, content.join("\n")).expect("Failed to write test runner");
236}
237
238#[cfg(target_family = "unix")]
239fn set_executable(path: &Path) {
240 use std::os::unix::prelude::PermissionsExt;
241
242 let mut perm = fs::metadata(path)
243 .expect("Failed to get test runner metadata")
244 .permissions();
245
246 perm.set_mode(0o755);
247 fs::set_permissions(path, perm).expect("Failed to set permissions on test runner");
248}
249
250#[cfg(target_family = "windows")]
251fn set_executable(_path: &Path) {
252 // Windows determines whether or not a file is executable via the PATHEXT
253 // environment variable. This function is a no-op for this platform.
254}
255
256fn write_test_runner(
257 path: &Path,
258 env: &BTreeMap<String, String>,
259 argv: &[String],
260 strip_substrings: &[String],
261) {
262 if cfg!(target_family = "unix") {
263 write_test_runner_unix(path, env, argv, strip_substrings);
264 } else if cfg!(target_family = "windows") {
265 write_test_runner_windows(path, env, argv, strip_substrings);
266 }
267
268 set_executable(path);
269}
270
271fn main() {
272 let opt = parse_args();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700273 let opt = expand_params_file(opt);
Brian Silvermancc09f182022-03-09 15:40:20 -0800274
275 let env: BTreeMap<String, String> = env::vars()
276 .into_iter()
277 .filter(|(key, _)| opt.env_keys.iter().any(|k| k == key))
278 .collect();
279
280 write_test_runner(&opt.output, &env, &opt.action_argv, &opt.strip_substrings);
281}