blob: 6775cee93622590ab9eb001d89dc21257222d85d [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 {
Adam Snaider1c095c92023-07-08 02:09:58 -040033 let args: Vec<String> = env::args().collect();
Brian Silvermancc09f182022-03-09 15:40:20 -080034 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(),
Adam Snaider1c095c92023-07-08 02:09:58 -0400177 // TODO: Instead of creating a symlink to mimic the behavior of
178 // --legacy_external_runfiles, this rule should be able to correcrtly
179 // sanitize the action args to run in a runfiles without this link.
180 "if [[ ! -e 'external' ]]; then ln -s ../ external ; fi".to_owned(),
181 "".to_owned(),
Brian Silvermancc09f182022-03-09 15:40:20 -0800182 "exec env - \\".to_owned(),
183 ];
184
Adam Snaider1c095c92023-07-08 02:09:58 -0400185 content.extend(env.iter().map(|(key, val)| format!("{key}='{val}' \\")));
Brian Silvermancc09f182022-03-09 15:40:20 -0800186
187 let argv_str = argv
188 .iter()
189 // Remove any substrings found in the argument
190 .map(|arg| {
191 let mut stripped_arg = arg.to_owned();
192 strip_substrings
193 .iter()
194 .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
195 stripped_arg
196 })
Adam Snaider1c095c92023-07-08 02:09:58 -0400197 .map(|arg| format!("'{arg}'"))
Brian Silvermancc09f182022-03-09 15:40:20 -0800198 .collect::<Vec<String>>()
199 .join(" ");
200
201 content.extend(vec![argv_str, "".to_owned()]);
202
203 fs::write(path, content.join("\n")).expect("Failed to write test runner");
204}
205
206/// Write a windows compatible test runner
207fn write_test_runner_windows(
208 path: &Path,
209 env: &BTreeMap<String, String>,
210 argv: &[String],
211 strip_substrings: &[String],
212) {
213 let env_str = env
214 .iter()
Adam Snaider1c095c92023-07-08 02:09:58 -0400215 .map(|(key, val)| format!("$env:{key}='{val}'"))
Brian Silvermancc09f182022-03-09 15:40:20 -0800216 .collect::<Vec<String>>()
217 .join(" ; ");
218
219 let argv_str = argv
220 .iter()
221 // Remove any substrings found in the argument
222 .map(|arg| {
223 let mut stripped_arg = arg.to_owned();
224 strip_substrings
225 .iter()
226 .for_each(|substring| stripped_arg = stripped_arg.replace(substring, ""));
227 stripped_arg
228 })
Adam Snaider1c095c92023-07-08 02:09:58 -0400229 .map(|arg| format!("'{arg}'"))
Brian Silvermancc09f182022-03-09 15:40:20 -0800230 .collect::<Vec<String>>()
231 .join(" ");
232
233 let content = vec![
234 "@ECHO OFF".to_owned(),
235 "".to_owned(),
Adam Snaider1c095c92023-07-08 02:09:58 -0400236 // TODO: Instead of creating a symlink to mimic the behavior of
237 // --legacy_external_runfiles, this rule should be able to correcrtly
238 // sanitize the action args to run in a runfiles without this link.
239 "powershell.exe -c \"if (!(Test-Path .\\external)) { New-Item -Path .\\external -ItemType SymbolicLink -Value ..\\ }\""
240 .to_owned(),
241 "".to_owned(),
242 format!("powershell.exe -c \"{env_str} ; & {argv_str}\""),
Brian Silvermancc09f182022-03-09 15:40:20 -0800243 "".to_owned(),
244 ];
245
246 fs::write(path, content.join("\n")).expect("Failed to write test runner");
247}
248
249#[cfg(target_family = "unix")]
250fn set_executable(path: &Path) {
251 use std::os::unix::prelude::PermissionsExt;
252
253 let mut perm = fs::metadata(path)
254 .expect("Failed to get test runner metadata")
255 .permissions();
256
257 perm.set_mode(0o755);
258 fs::set_permissions(path, perm).expect("Failed to set permissions on test runner");
259}
260
261#[cfg(target_family = "windows")]
262fn set_executable(_path: &Path) {
263 // Windows determines whether or not a file is executable via the PATHEXT
264 // environment variable. This function is a no-op for this platform.
265}
266
267fn write_test_runner(
268 path: &Path,
269 env: &BTreeMap<String, String>,
270 argv: &[String],
271 strip_substrings: &[String],
272) {
273 if cfg!(target_family = "unix") {
274 write_test_runner_unix(path, env, argv, strip_substrings);
275 } else if cfg!(target_family = "windows") {
276 write_test_runner_windows(path, env, argv, strip_substrings);
277 }
278
279 set_executable(path);
280}
281
282fn main() {
283 let opt = parse_args();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700284 let opt = expand_params_file(opt);
Brian Silvermancc09f182022-03-09 15:40:20 -0800285
286 let env: BTreeMap<String, String> = env::vars()
Brian Silvermancc09f182022-03-09 15:40:20 -0800287 .filter(|(key, _)| opt.env_keys.iter().any(|k| k == key))
288 .collect();
289
290 write_test_runner(&opt.output, &env, &opt.action_argv, &opt.strip_substrings);
291}