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