compiletest/runtest/
run_make.rs

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