compiletest/runtest/run_make.rs
1use std::path::Path;
2use std::process::{Command, Output, Stdio};
3use std::{env, fs};
4
5use build_helper::fs::{ignore_not_found, recursive_remove};
6
7use super::{ProcRes, TestCx, disable_error_reporting};
8use crate::util::{copy_dir_all, dylib_env_var};
9
10impl TestCx<'_> {
11 pub(super) fn run_rmake_test(&self) {
12 // For `run-make` V2, we need to perform 2 steps to build and run a `run-make` V2 recipe
13 // (`rmake.rs`) to run the actual tests. The support library is already built as a tool rust
14 // library and is available under
15 // `build/$HOST/stage0-bootstrap-tools/$TARGET/release/librun_make_support.rlib`.
16 //
17 // 1. We need to build the recipe `rmake.rs` as a binary and link in the `run_make_support`
18 // library.
19 // 2. We need to run the recipe binary.
20
21 let host_build_root = self.config.build_root.join(&self.config.host);
22
23 // We construct the following directory tree for each rmake.rs test:
24 // ```
25 // <base_dir>/
26 // rmake.exe
27 // rmake_out/
28 // ```
29 // having the recipe executable separate from the output artifacts directory allows the
30 // recipes to `remove_dir_all($TMPDIR)` without running into issues related trying to remove
31 // a currently running executable because the recipe executable is not under the
32 // `rmake_out/` directory.
33 let base_dir = self.output_base_dir();
34 ignore_not_found(|| recursive_remove(&base_dir)).unwrap();
35
36 let rmake_out_dir = base_dir.join("rmake_out");
37 fs::create_dir_all(&rmake_out_dir).unwrap();
38
39 // Copy all input files (apart from rmake.rs) to the temporary directory,
40 // so that the input directory structure from `tests/run-make/<test>` is mirrored
41 // to the `rmake_out` directory.
42 for path in walkdir::WalkDir::new(&self.testpaths.file).min_depth(1) {
43 let path = path.unwrap().path().to_path_buf();
44 if path.file_name().is_some_and(|s| s != "rmake.rs") {
45 let target = rmake_out_dir.join(path.strip_prefix(&self.testpaths.file).unwrap());
46 if path.is_dir() {
47 copy_dir_all(&path, target).unwrap();
48 } else {
49 fs::copy(&path, target).unwrap();
50 }
51 }
52 }
53
54 // In order to link in the support library as a rlib when compiling recipes, we need three
55 // paths:
56 // 1. Path of the built support library rlib itself.
57 // 2. Path of the built support library's dependencies directory.
58 // 3. Path of the built support library's dependencies' dependencies directory.
59 //
60 // The paths look like
61 //
62 // ```
63 // build/<target_triple>/
64 // ├── stage0-bootstrap-tools/
65 // │ ├── <host_triple>/release/librun_make_support.rlib // <- support rlib itself
66 // │ ├── <host_triple>/release/deps/ // <- deps
67 // │ └── release/deps/ // <- deps of deps
68 // ```
69 //
70 // FIXME(jieyouxu): there almost certainly is a better way to do this (specifically how the
71 // support lib and its deps are organized), but this seems to work for now.
72
73 let tools_bin = host_build_root.join("stage0-bootstrap-tools");
74 let support_host_path = tools_bin.join(&self.config.host).join("release");
75 let support_lib_path = support_host_path.join("librun_make_support.rlib");
76
77 let support_lib_deps = support_host_path.join("deps");
78 let support_lib_deps_deps = tools_bin.join("release").join("deps");
79
80 // To compile the recipe with rustc, we need to provide suitable dynamic library search
81 // paths to rustc. This includes both:
82 // 1. The "base" dylib search paths that was provided to compiletest, e.g. `LD_LIBRARY_PATH`
83 // on some linux distros.
84 // 2. Specific library paths in `self.config.compile_lib_path` needed for running rustc.
85
86 let base_dylib_search_paths =
87 Vec::from_iter(env::split_paths(&env::var(dylib_env_var()).unwrap()));
88
89 // Calculate the paths of the recipe binary. As previously discussed, this is placed at
90 // `<base_dir>/<bin_name>` with `bin_name` being `rmake` or `rmake.exe` depending on
91 // platform.
92 let recipe_bin = {
93 let mut p = base_dir.join("rmake");
94 p.set_extension(env::consts::EXE_EXTENSION);
95 p
96 };
97
98 // run-make-support and run-make tests are compiled using the stage0 compiler
99 // If the stage is 0, then the compiler that we test (either bootstrap or an explicitly
100 // set compiler) is the one that actually compiled run-make-support.
101 let stage0_rustc = self
102 .config
103 .stage0_rustc_path
104 .as_ref()
105 .expect("stage0 rustc is required to run run-make tests");
106 let mut rustc = Command::new(&stage0_rustc);
107 rustc
108 // `rmake.rs` **must** be buildable by a stable compiler, it may not use *any* unstable
109 // library or compiler features. Here, we force the stage 0 rustc to consider itself as
110 // a stable-channel compiler via `RUSTC_BOOTSTRAP=-1` to prevent *any* unstable
111 // library/compiler usages, even if stage 0 rustc is *actually* a nightly rustc.
112 .env("RUSTC_BOOTSTRAP", "-1")
113 .arg("-o")
114 .arg(&recipe_bin)
115 // Specify library search paths for `run_make_support`.
116 .arg(format!("-Ldependency={}", &support_lib_path.parent().unwrap().to_string_lossy()))
117 .arg(format!("-Ldependency={}", &support_lib_deps.to_string_lossy()))
118 .arg(format!("-Ldependency={}", &support_lib_deps_deps.to_string_lossy()))
119 // Provide `run_make_support` as extern prelude, so test writers don't need to write
120 // `extern run_make_support;`.
121 .arg("--extern")
122 .arg(format!("run_make_support={}", &support_lib_path.to_string_lossy()))
123 .arg("--edition=2021")
124 .arg(&self.testpaths.file.join("rmake.rs"))
125 .arg("-Cprefer-dynamic");
126
127 // In test code we want to be very pedantic about values being silently discarded that are
128 // annotated with `#[must_use]`.
129 rustc.arg("-Dunused_must_use");
130
131 // Now run rustc to build the recipe.
132 let res = self.run_command_to_procres(&mut rustc);
133 if !res.status.success() {
134 self.fatal_proc_rec("run-make test failed: could not build `rmake.rs` recipe", &res);
135 }
136
137 // To actually run the recipe, we have to provide the recipe with a bunch of information
138 // provided through env vars.
139
140 // Compute dynamic library search paths for recipes.
141 // These dylib directories are needed to **execute the recipe**.
142 let recipe_dylib_search_paths = {
143 let mut paths = base_dylib_search_paths.clone();
144 paths.push(
145 stage0_rustc
146 .parent()
147 .unwrap()
148 .parent()
149 .unwrap()
150 .join("lib")
151 .join("rustlib")
152 .join(&self.config.host)
153 .join("lib"),
154 );
155 paths
156 };
157
158 let mut cmd = Command::new(&recipe_bin);
159 cmd.current_dir(&rmake_out_dir)
160 .stdout(Stdio::piped())
161 .stderr(Stdio::piped())
162 // Provide the target-specific env var that is used to record dylib search paths. For
163 // example, this could be `LD_LIBRARY_PATH` on some linux distros but `PATH` on Windows.
164 .env("LD_LIB_PATH_ENVVAR", dylib_env_var())
165 // Provide the dylib search paths.
166 // This is required to run the **recipe** itself.
167 .env(dylib_env_var(), &env::join_paths(recipe_dylib_search_paths).unwrap())
168 // Provide the directory to libraries that are needed to run the *compiler* invoked
169 // by the recipe.
170 .env("HOST_RUSTC_DYLIB_PATH", &self.config.compile_lib_path)
171 // Provide the directory to libraries that might be needed to run binaries created
172 // by a compiler invoked by the recipe.
173 .env("TARGET_EXE_DYLIB_PATH", &self.config.run_lib_path)
174 // Provide the target.
175 .env("TARGET", &self.config.target)
176 // Some tests unfortunately still need Python, so provide path to a Python interpreter.
177 .env("PYTHON", &self.config.python)
178 // Provide path to sources root.
179 .env("SOURCE_ROOT", &self.config.src_root)
180 // Path to the host build directory.
181 .env("BUILD_ROOT", &host_build_root)
182 // Provide path to stage-corresponding rustc.
183 .env("RUSTC", &self.config.rustc_path)
184 // Provide which LLVM components are available (e.g. which LLVM components are provided
185 // through a specific CI runner).
186 .env("LLVM_COMPONENTS", &self.config.llvm_components);
187
188 if let Some(ref cargo) = self.config.cargo_path {
189 cmd.env("CARGO", cargo);
190 }
191
192 if let Some(ref rustdoc) = self.config.rustdoc_path {
193 cmd.env("RUSTDOC", rustdoc);
194 }
195
196 if let Some(ref node) = self.config.nodejs {
197 cmd.env("NODE", node);
198 }
199
200 if let Some(ref linker) = self.config.target_linker {
201 cmd.env("RUSTC_LINKER", linker);
202 }
203
204 if let Some(ref clang) = self.config.run_clang_based_tests_with {
205 cmd.env("CLANG", clang);
206 }
207
208 if let Some(ref filecheck) = self.config.llvm_filecheck {
209 cmd.env("LLVM_FILECHECK", filecheck);
210 }
211
212 if let Some(ref llvm_bin_dir) = self.config.llvm_bin_dir {
213 cmd.env("LLVM_BIN_DIR", llvm_bin_dir);
214 }
215
216 if let Some(ref remote_test_client) = self.config.remote_test_client {
217 cmd.env("REMOTE_TEST_CLIENT", remote_test_client);
218 }
219
220 // We don't want RUSTFLAGS set from the outside to interfere with
221 // compiler flags set in the test cases:
222 cmd.env_remove("RUSTFLAGS");
223
224 // Use dynamic musl for tests because static doesn't allow creating dylibs
225 if self.config.host.contains("musl") {
226 cmd.env("RUSTFLAGS", "-Ctarget-feature=-crt-static").env("IS_MUSL_HOST", "1");
227 }
228
229 if self.config.bless {
230 // If we're running in `--bless` mode, set an environment variable to tell
231 // `run_make_support` to bless snapshot files instead of checking them.
232 //
233 // The value is this test's source directory, because the support code
234 // will need that path in order to bless the _original_ snapshot files,
235 // not the copies in `rmake_out`.
236 // (See <https://github.com/rust-lang/rust/issues/129038>.)
237 cmd.env("RUSTC_BLESS_TEST", &self.testpaths.file);
238 }
239
240 if self.config.target.contains("msvc") && !self.config.cc.is_empty() {
241 // We need to pass a path to `lib.exe`, so assume that `cc` is `cl.exe`
242 // and that `lib.exe` lives next to it.
243 let lib = Path::new(&self.config.cc).parent().unwrap().join("lib.exe");
244
245 // MSYS doesn't like passing flags of the form `/foo` as it thinks it's
246 // a path and instead passes `C:\msys64\foo`, so convert all
247 // `/`-arguments to MSVC here to `-` arguments.
248 let cflags = self
249 .config
250 .cflags
251 .split(' ')
252 .map(|s| s.replace("/", "-"))
253 .collect::<Vec<_>>()
254 .join(" ");
255 let cxxflags = self
256 .config
257 .cxxflags
258 .split(' ')
259 .map(|s| s.replace("/", "-"))
260 .collect::<Vec<_>>()
261 .join(" ");
262
263 cmd.env("IS_MSVC", "1")
264 .env("IS_WINDOWS", "1")
265 .env("MSVC_LIB", format!("'{}' -nologo", lib.display()))
266 .env("MSVC_LIB_PATH", format!("{}", lib.display()))
267 // Note: we diverge from legacy run_make and don't lump `CC` the compiler and
268 // default flags together.
269 .env("CC_DEFAULT_FLAGS", &cflags)
270 .env("CC", &self.config.cc)
271 .env("CXX_DEFAULT_FLAGS", &cxxflags)
272 .env("CXX", &self.config.cxx);
273 } else {
274 cmd.env("CC_DEFAULT_FLAGS", &self.config.cflags)
275 .env("CC", &self.config.cc)
276 .env("CXX_DEFAULT_FLAGS", &self.config.cxxflags)
277 .env("CXX", &self.config.cxx)
278 .env("AR", &self.config.ar);
279
280 if self.config.target.contains("windows") {
281 cmd.env("IS_WINDOWS", "1");
282 }
283 }
284
285 let proc = disable_error_reporting(|| cmd.spawn().expect("failed to spawn `rmake`"));
286 let (Output { stdout, stderr, status }, truncated) = self.read2_abbreviated(proc);
287 let stdout = String::from_utf8_lossy(&stdout).into_owned();
288 let stderr = String::from_utf8_lossy(&stderr).into_owned();
289 // This conditions on `status.success()` so we don't print output twice on error.
290 // NOTE: this code is called from a libtest thread, so it's hidden by default unless --nocapture is passed.
291 self.dump_output(status.success(), &cmd.get_program().to_string_lossy(), &stdout, &stderr);
292 if !status.success() {
293 let res = ProcRes { status, stdout, stderr, truncated, cmdline: format!("{:?}", cmd) };
294 self.fatal_proc_rec("rmake recipe failed to complete", &res);
295 }
296 }
297}