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        let test_dir = &self.testpaths.file;
13        if test_dir.join("rmake.rs").exists() {
14            self.run_rmake_v2_test();
15        } else if test_dir.join("Makefile").exists() {
16            self.run_rmake_legacy_test();
17        } else {
18            self.fatal("failed to find either `rmake.rs` or `Makefile`")
19        }
20    }
21
22    fn run_rmake_legacy_test(&self) {
23        let cwd = env::current_dir().unwrap();
24        let src_root = self.config.src_base.parent().unwrap().parent().unwrap();
25        let src_root = cwd.join(&src_root);
26
27        // FIXME(Zalathar): This should probably be `output_base_dir` to avoid
28        // an unnecessary extra subdirectory, but since legacy Makefile tests
29        // are hopefully going away, it seems safer to leave this perilous code
30        // as-is until it can all be deleted.
31        let tmpdir = cwd.join(self.output_base_name());
32        ignore_not_found(|| recursive_remove(&tmpdir)).unwrap();
33
34        fs::create_dir_all(&tmpdir).unwrap();
35
36        let host = &self.config.host;
37        let make = if host.contains("dragonfly")
38            || host.contains("freebsd")
39            || host.contains("netbsd")
40            || host.contains("openbsd")
41            || host.contains("aix")
42        {
43            "gmake"
44        } else {
45            "make"
46        };
47
48        let mut cmd = Command::new(make);
49        cmd.current_dir(&self.testpaths.file)
50            .stdout(Stdio::piped())
51            .stderr(Stdio::piped())
52            .env("TARGET", &self.config.target)
53            .env("PYTHON", &self.config.python)
54            .env("S", src_root)
55            .env("RUST_BUILD_STAGE", &self.config.stage_id)
56            .env("RUSTC", cwd.join(&self.config.rustc_path))
57            .env("TMPDIR", &tmpdir)
58            .env("LD_LIB_PATH_ENVVAR", dylib_env_var())
59            .env("HOST_RPATH_DIR", cwd.join(&self.config.compile_lib_path))
60            .env("TARGET_RPATH_DIR", cwd.join(&self.config.run_lib_path))
61            .env("LLVM_COMPONENTS", &self.config.llvm_components)
62            // We for sure don't want these tests to run in parallel, so make
63            // sure they don't have access to these vars if we run via `make`
64            // at the top level
65            .env_remove("MAKEFLAGS")
66            .env_remove("MFLAGS")
67            .env_remove("CARGO_MAKEFLAGS");
68
69        if let Some(ref cargo) = self.config.cargo_path {
70            cmd.env("CARGO", cwd.join(cargo));
71        }
72
73        if let Some(ref rustdoc) = self.config.rustdoc_path {
74            cmd.env("RUSTDOC", cwd.join(rustdoc));
75        }
76
77        if let Some(ref node) = self.config.nodejs {
78            cmd.env("NODE", node);
79        }
80
81        if let Some(ref linker) = self.config.target_linker {
82            cmd.env("RUSTC_LINKER", linker);
83        }
84
85        if let Some(ref clang) = self.config.run_clang_based_tests_with {
86            cmd.env("CLANG", clang);
87        }
88
89        if let Some(ref filecheck) = self.config.llvm_filecheck {
90            cmd.env("LLVM_FILECHECK", filecheck);
91        }
92
93        if let Some(ref llvm_bin_dir) = self.config.llvm_bin_dir {
94            cmd.env("LLVM_BIN_DIR", llvm_bin_dir);
95        }
96
97        if let Some(ref remote_test_client) = self.config.remote_test_client {
98            cmd.env("REMOTE_TEST_CLIENT", remote_test_client);
99        }
100
101        // We don't want RUSTFLAGS set from the outside to interfere with
102        // compiler flags set in the test cases:
103        cmd.env_remove("RUSTFLAGS");
104
105        // Use dynamic musl for tests because static doesn't allow creating dylibs
106        if self.config.host.contains("musl") {
107            cmd.env("RUSTFLAGS", "-Ctarget-feature=-crt-static").env("IS_MUSL_HOST", "1");
108        }
109
110        if self.config.bless {
111            cmd.env("RUSTC_BLESS_TEST", "--bless");
112            // Assume this option is active if the environment variable is "defined", with _any_ value.
113            // As an example, a `Makefile` can use this option by:
114            //
115            //   ifdef RUSTC_BLESS_TEST
116            //       cp "$(TMPDIR)"/actual_something.ext expected_something.ext
117            //   else
118            //       $(DIFF) expected_something.ext "$(TMPDIR)"/actual_something.ext
119            //   endif
120        }
121
122        if self.config.target.contains("msvc") && !self.config.cc.is_empty() {
123            // We need to pass a path to `lib.exe`, so assume that `cc` is `cl.exe`
124            // and that `lib.exe` lives next to it.
125            let lib = Path::new(&self.config.cc).parent().unwrap().join("lib.exe");
126
127            // MSYS doesn't like passing flags of the form `/foo` as it thinks it's
128            // a path and instead passes `C:\msys64\foo`, so convert all
129            // `/`-arguments to MSVC here to `-` arguments.
130            let cflags = self
131                .config
132                .cflags
133                .split(' ')
134                .map(|s| s.replace("/", "-"))
135                .collect::<Vec<_>>()
136                .join(" ");
137            let cxxflags = self
138                .config
139                .cxxflags
140                .split(' ')
141                .map(|s| s.replace("/", "-"))
142                .collect::<Vec<_>>()
143                .join(" ");
144
145            cmd.env("IS_MSVC", "1")
146                .env("IS_WINDOWS", "1")
147                .env("MSVC_LIB", format!("'{}' -nologo", lib.display()))
148                .env("MSVC_LIB_PATH", format!("{}", lib.display()))
149                .env("CC", format!("'{}' {}", self.config.cc, cflags))
150                .env("CXX", format!("'{}' {}", &self.config.cxx, cxxflags));
151        } else {
152            cmd.env("CC", format!("{} {}", self.config.cc, self.config.cflags))
153                .env("CXX", format!("{} {}", self.config.cxx, self.config.cxxflags))
154                .env("AR", &self.config.ar);
155
156            if self.config.target.contains("windows") {
157                cmd.env("IS_WINDOWS", "1");
158            }
159        }
160
161        let (output, truncated) =
162            self.read2_abbreviated(cmd.spawn().expect("failed to spawn `make`"));
163        if !output.status.success() {
164            let res = ProcRes {
165                status: output.status,
166                stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
167                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
168                truncated,
169                cmdline: format!("{:?}", cmd),
170            };
171            self.fatal_proc_rec("make failed", &res);
172        }
173    }
174
175    fn run_rmake_v2_test(&self) {
176        // For `run-make` V2, we need to perform 2 steps to build and run a `run-make` V2 recipe
177        // (`rmake.rs`) to run the actual tests. The support library is already built as a tool rust
178        // library and is available under `build/$TARGET/stageN-tools-bin/librun_make_support.rlib`.
179        //
180        // 1. We need to build the recipe `rmake.rs` as a binary and link in the `run_make_support`
181        //    library.
182        // 2. We need to run the recipe binary.
183
184        // So we assume the rust-lang/rust project setup looks like the following (our `.` is the
185        // top-level directory, irrelevant entries to our purposes omitted):
186        //
187        // ```
188        // .                               // <- `source_root`
189        // ├── build/                      // <- `build_root`
190        // ├── compiler/
191        // ├── library/
192        // ├── src/
193        // │  └── tools/
194        // │     └── run_make_support/
195        // └── tests
196        //    └── run-make/
197        // ```
198
199        // `source_root` is the top-level directory containing the rust-lang/rust checkout.
200        let source_root =
201            self.config.find_rust_src_root().expect("could not determine rust source root");
202        // `self.config.build_base` is actually the build base folder + "test" + test suite name, it
203        // looks like `build/<host_triple>/test/run-make`. But we want `build/<host_triple>/`. Note
204        // that the `build` directory does not need to be called `build`, nor does it need to be
205        // under `source_root`, so we must compute it based off of `self.config.build_base`.
206        let build_root =
207            self.config.build_base.parent().and_then(Path::parent).unwrap().to_path_buf();
208
209        // We construct the following directory tree for each rmake.rs test:
210        // ```
211        // <base_dir>/
212        //     rmake.exe
213        //     rmake_out/
214        // ```
215        // having the recipe executable separate from the output artifacts directory allows the
216        // recipes to `remove_dir_all($TMPDIR)` without running into issues related trying to remove
217        // a currently running executable because the recipe executable is not under the
218        // `rmake_out/` directory.
219        //
220        // This setup intentionally diverges from legacy Makefile run-make tests.
221        let base_dir = self.output_base_dir();
222        ignore_not_found(|| recursive_remove(&base_dir)).unwrap();
223
224        let rmake_out_dir = base_dir.join("rmake_out");
225        fs::create_dir_all(&rmake_out_dir).unwrap();
226
227        // Copy all input files (apart from rmake.rs) to the temporary directory,
228        // so that the input directory structure from `tests/run-make/<test>` is mirrored
229        // to the `rmake_out` directory.
230        for path in walkdir::WalkDir::new(&self.testpaths.file).min_depth(1) {
231            let path = path.unwrap().path().to_path_buf();
232            if path.file_name().is_some_and(|s| s != "rmake.rs") {
233                let target = rmake_out_dir.join(path.strip_prefix(&self.testpaths.file).unwrap());
234                if path.is_dir() {
235                    copy_dir_all(&path, target).unwrap();
236                } else {
237                    fs::copy(&path, target).unwrap();
238                }
239            }
240        }
241
242        // In order to link in the support library as a rlib when compiling recipes, we need three
243        // paths:
244        // 1. Path of the built support library rlib itself.
245        // 2. Path of the built support library's dependencies directory.
246        // 3. Path of the built support library's dependencies' dependencies directory.
247        //
248        // The paths look like
249        //
250        // ```
251        // build/<target_triple>/
252        // ├── stageN-tools-bin/
253        // │   └── librun_make_support.rlib       // <- support rlib itself
254        // ├── stageN-tools/
255        // │   ├── release/deps/                  // <- deps of deps
256        // │   └── <host_triple>/release/deps/    // <- deps
257        // ```
258        //
259        // FIXME(jieyouxu): there almost certainly is a better way to do this (specifically how the
260        // support lib and its deps are organized, can't we copy them to the tools-bin dir as
261        // well?), but this seems to work for now.
262
263        let stage_number = self.config.stage;
264
265        let stage_tools_bin = build_root.join(format!("stage{stage_number}-tools-bin"));
266        let support_lib_path = stage_tools_bin.join("librun_make_support.rlib");
267
268        let stage_tools = build_root.join(format!("stage{stage_number}-tools"));
269        let support_lib_deps = stage_tools.join(&self.config.host).join("release").join("deps");
270        let support_lib_deps_deps = stage_tools.join("release").join("deps");
271
272        // To compile the recipe with rustc, we need to provide suitable dynamic library search
273        // paths to rustc. This includes both:
274        // 1. The "base" dylib search paths that was provided to compiletest, e.g. `LD_LIBRARY_PATH`
275        //    on some linux distros.
276        // 2. Specific library paths in `self.config.compile_lib_path` needed for running rustc.
277
278        let base_dylib_search_paths =
279            Vec::from_iter(env::split_paths(&env::var(dylib_env_var()).unwrap()));
280
281        let host_dylib_search_paths = {
282            let mut paths = vec![self.config.compile_lib_path.clone()];
283            paths.extend(base_dylib_search_paths.iter().cloned());
284            paths
285        };
286
287        // Calculate the paths of the recipe binary. As previously discussed, this is placed at
288        // `<base_dir>/<bin_name>` with `bin_name` being `rmake` or `rmake.exe` depending on
289        // platform.
290        let recipe_bin = {
291            let mut p = base_dir.join("rmake");
292            p.set_extension(env::consts::EXE_EXTENSION);
293            p
294        };
295
296        let mut rustc = Command::new(&self.config.rustc_path);
297        rustc
298            .arg("-o")
299            .arg(&recipe_bin)
300            // Specify library search paths for `run_make_support`.
301            .arg(format!("-Ldependency={}", &support_lib_path.parent().unwrap().to_string_lossy()))
302            .arg(format!("-Ldependency={}", &support_lib_deps.to_string_lossy()))
303            .arg(format!("-Ldependency={}", &support_lib_deps_deps.to_string_lossy()))
304            // Provide `run_make_support` as extern prelude, so test writers don't need to write
305            // `extern run_make_support;`.
306            .arg("--extern")
307            .arg(format!("run_make_support={}", &support_lib_path.to_string_lossy()))
308            .arg("--edition=2021")
309            .arg(&self.testpaths.file.join("rmake.rs"))
310            .arg("-Cprefer-dynamic")
311            // Provide necessary library search paths for rustc.
312            .env(dylib_env_var(), &env::join_paths(host_dylib_search_paths).unwrap());
313
314        // In test code we want to be very pedantic about values being silently discarded that are
315        // annotated with `#[must_use]`.
316        rustc.arg("-Dunused_must_use");
317
318        // > `cg_clif` uses `COMPILETEST_FORCE_STAGE0=1 ./x.py test --stage 0` for running the rustc
319        // > test suite. With the introduction of rmake.rs this broke. `librun_make_support.rlib` is
320        // > compiled using the bootstrap rustc wrapper which sets `--sysroot
321        // > build/aarch64-unknown-linux-gnu/stage0-sysroot`, but then compiletest will compile
322        // > `rmake.rs` using the sysroot of the bootstrap compiler causing it to not find the
323        // > `libstd.rlib` against which `librun_make_support.rlib` is compiled.
324        //
325        // The gist here is that we have to pass the proper stage0 sysroot if we want
326        //
327        // ```
328        // $ COMPILETEST_FORCE_STAGE0=1 ./x test run-make --stage 0
329        // ```
330        //
331        // to work correctly.
332        //
333        // See <https://github.com/rust-lang/rust/pull/122248> for more background.
334        let stage0_sysroot = build_root.join("stage0-sysroot");
335        if std::env::var_os("COMPILETEST_FORCE_STAGE0").is_some() {
336            rustc.arg("--sysroot").arg(&stage0_sysroot);
337        }
338
339        // Now run rustc to build the recipe.
340        let res = self.run_command_to_procres(&mut rustc);
341        if !res.status.success() {
342            self.fatal_proc_rec("run-make test failed: could not build `rmake.rs` recipe", &res);
343        }
344
345        // To actually run the recipe, we have to provide the recipe with a bunch of information
346        // provided through env vars.
347
348        // Compute stage-specific standard library paths.
349        let stage_std_path = build_root.join(format!("stage{stage_number}")).join("lib");
350
351        // Compute dynamic library search paths for recipes.
352        let recipe_dylib_search_paths = {
353            let mut paths = base_dylib_search_paths.clone();
354
355            // For stage 0, we need to explicitly include the stage0-sysroot libstd dylib.
356            // See <https://github.com/rust-lang/rust/issues/135373>.
357            if std::env::var_os("COMPILETEST_FORCE_STAGE0").is_some() {
358                paths.push(
359                    stage0_sysroot.join("lib").join("rustlib").join(&self.config.host).join("lib"),
360                );
361            }
362
363            paths.push(support_lib_path.parent().unwrap().to_path_buf());
364            paths.push(stage_std_path.join("rustlib").join(&self.config.host).join("lib"));
365            paths
366        };
367
368        // Compute runtime library search paths for recipes. This is target-specific.
369        let target_runtime_dylib_search_paths = {
370            let mut paths = vec![rmake_out_dir.clone()];
371            paths.extend(base_dylib_search_paths.iter().cloned());
372            paths
373        };
374
375        // FIXME(jieyouxu): please rename `TARGET_RPATH_ENV`, `HOST_RPATH_DIR` and
376        // `TARGET_RPATH_DIR`, it is **extremely** confusing!
377        let mut cmd = Command::new(&recipe_bin);
378        cmd.current_dir(&rmake_out_dir)
379            .stdout(Stdio::piped())
380            .stderr(Stdio::piped())
381            // Provide the target-specific env var that is used to record dylib search paths. For
382            // example, this could be `LD_LIBRARY_PATH` on some linux distros but `PATH` on Windows.
383            .env("LD_LIB_PATH_ENVVAR", dylib_env_var())
384            // Provide the dylib search paths.
385            .env(dylib_env_var(), &env::join_paths(recipe_dylib_search_paths).unwrap())
386            // Provide runtime dylib search paths.
387            .env("TARGET_RPATH_ENV", &env::join_paths(target_runtime_dylib_search_paths).unwrap())
388            // Provide the target.
389            .env("TARGET", &self.config.target)
390            // Some tests unfortunately still need Python, so provide path to a Python interpreter.
391            .env("PYTHON", &self.config.python)
392            // Provide path to checkout root. This is the top-level directory containing
393            // rust-lang/rust checkout.
394            .env("SOURCE_ROOT", &source_root)
395            // Path to the build directory. This is usually the same as `source_root.join("build").join("host")`.
396            .env("BUILD_ROOT", &build_root)
397            // Provide path to stage-corresponding rustc.
398            .env("RUSTC", &self.config.rustc_path)
399            // Provide the directory to libraries that are needed to run the *compiler*. This is not
400            // to be confused with `TARGET_RPATH_ENV` or `TARGET_RPATH_DIR`. This is needed if the
401            // recipe wants to invoke rustc.
402            .env("HOST_RPATH_DIR", &self.config.compile_lib_path)
403            // Provide the directory to libraries that might be needed to run compiled binaries
404            // (further compiled by the recipe!).
405            .env("TARGET_RPATH_DIR", &self.config.run_lib_path)
406            // Provide which LLVM components are available (e.g. which LLVM components are provided
407            // through a specific CI runner).
408            .env("LLVM_COMPONENTS", &self.config.llvm_components);
409
410        if let Some(ref cargo) = self.config.cargo_path {
411            cmd.env("CARGO", source_root.join(cargo));
412        }
413
414        if let Some(ref rustdoc) = self.config.rustdoc_path {
415            cmd.env("RUSTDOC", source_root.join(rustdoc));
416        }
417
418        if let Some(ref node) = self.config.nodejs {
419            cmd.env("NODE", node);
420        }
421
422        if let Some(ref linker) = self.config.target_linker {
423            cmd.env("RUSTC_LINKER", linker);
424        }
425
426        if let Some(ref clang) = self.config.run_clang_based_tests_with {
427            cmd.env("CLANG", clang);
428        }
429
430        if let Some(ref filecheck) = self.config.llvm_filecheck {
431            cmd.env("LLVM_FILECHECK", filecheck);
432        }
433
434        if let Some(ref llvm_bin_dir) = self.config.llvm_bin_dir {
435            cmd.env("LLVM_BIN_DIR", llvm_bin_dir);
436        }
437
438        if let Some(ref remote_test_client) = self.config.remote_test_client {
439            cmd.env("REMOTE_TEST_CLIENT", remote_test_client);
440        }
441
442        // We don't want RUSTFLAGS set from the outside to interfere with
443        // compiler flags set in the test cases:
444        cmd.env_remove("RUSTFLAGS");
445
446        // Use dynamic musl for tests because static doesn't allow creating dylibs
447        if self.config.host.contains("musl") {
448            cmd.env("RUSTFLAGS", "-Ctarget-feature=-crt-static").env("IS_MUSL_HOST", "1");
449        }
450
451        if self.config.bless {
452            // If we're running in `--bless` mode, set an environment variable to tell
453            // `run_make_support` to bless snapshot files instead of checking them.
454            //
455            // The value is this test's source directory, because the support code
456            // will need that path in order to bless the _original_ snapshot files,
457            // not the copies in `rmake_out`.
458            // (See <https://github.com/rust-lang/rust/issues/129038>.)
459            cmd.env("RUSTC_BLESS_TEST", &self.testpaths.file);
460        }
461
462        if self.config.target.contains("msvc") && !self.config.cc.is_empty() {
463            // We need to pass a path to `lib.exe`, so assume that `cc` is `cl.exe`
464            // and that `lib.exe` lives next to it.
465            let lib = Path::new(&self.config.cc).parent().unwrap().join("lib.exe");
466
467            // MSYS doesn't like passing flags of the form `/foo` as it thinks it's
468            // a path and instead passes `C:\msys64\foo`, so convert all
469            // `/`-arguments to MSVC here to `-` arguments.
470            let cflags = self
471                .config
472                .cflags
473                .split(' ')
474                .map(|s| s.replace("/", "-"))
475                .collect::<Vec<_>>()
476                .join(" ");
477            let cxxflags = self
478                .config
479                .cxxflags
480                .split(' ')
481                .map(|s| s.replace("/", "-"))
482                .collect::<Vec<_>>()
483                .join(" ");
484
485            cmd.env("IS_MSVC", "1")
486                .env("IS_WINDOWS", "1")
487                .env("MSVC_LIB", format!("'{}' -nologo", lib.display()))
488                .env("MSVC_LIB_PATH", format!("{}", lib.display()))
489                // Note: we diverge from legacy run_make and don't lump `CC` the compiler and
490                // default flags together.
491                .env("CC_DEFAULT_FLAGS", &cflags)
492                .env("CC", &self.config.cc)
493                .env("CXX_DEFAULT_FLAGS", &cxxflags)
494                .env("CXX", &self.config.cxx);
495        } else {
496            cmd.env("CC_DEFAULT_FLAGS", &self.config.cflags)
497                .env("CC", &self.config.cc)
498                .env("CXX_DEFAULT_FLAGS", &self.config.cxxflags)
499                .env("CXX", &self.config.cxx)
500                .env("AR", &self.config.ar);
501
502            if self.config.target.contains("windows") {
503                cmd.env("IS_WINDOWS", "1");
504            }
505        }
506
507        let proc = disable_error_reporting(|| cmd.spawn().expect("failed to spawn `rmake`"));
508        let (Output { stdout, stderr, status }, truncated) = self.read2_abbreviated(proc);
509        let stdout = String::from_utf8_lossy(&stdout).into_owned();
510        let stderr = String::from_utf8_lossy(&stderr).into_owned();
511        // This conditions on `status.success()` so we don't print output twice on error.
512        // NOTE: this code is called from a libtest thread, so it's hidden by default unless --nocapture is passed.
513        self.dump_output(status.success(), &cmd.get_program().to_string_lossy(), &stdout, &stderr);
514        if !status.success() {
515            let res = ProcRes { status, stdout, stderr, truncated, cmdline: format!("{:?}", cmd) };
516            self.fatal_proc_rec("rmake recipe failed to complete", &res);
517        }
518    }
519}