about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-10-23 19:26:00 +0000
committerbors <bors@rust-lang.org>2024-10-23 19:26:00 +0000
commit4f2f477fded0a47b21ed3f6aeddeafa5db8bf518 (patch)
tree604f5084a58bb706c0c69043aee5575db1c1607e /src
parentbe01dabfefd2daa4574b974f571c7852085d60cb (diff)
parent5f0626cdb159569e03390573bbd48b162567a4da (diff)
downloadrust-4f2f477fded0a47b21ed3f6aeddeafa5db8bf518.tar.gz
rust-4f2f477fded0a47b21ed3f6aeddeafa5db8bf518.zip
Auto merge of #132070 - fmease:rollup-4i4k587, r=fmease
Rollup of 5 pull requests

Successful merges:

 - #131043 (Refactor change detection for rustdoc and download-rustc)
 - #131181 (Compiletest: Custom differ)
 - #131487 (Add wasm32v1-none target (compiler-team/#791))
 - #132054 (do not remove `.cargo` directory)
 - #132058 (CI: rfl: use rust-next temporary commit)

r? `@ghost`
`@rustbot` modify labels: rollup
Diffstat (limited to 'src')
-rw-r--r--src/bootstrap/bootstrap.py3
-rw-r--r--src/bootstrap/src/core/build_steps/test.rs3
-rw-r--r--src/bootstrap/src/core/build_steps/tool.rs31
-rw-r--r--src/bootstrap/src/core/config/config.rs64
-rw-r--r--src/bootstrap/src/utils/change_tracker.rs5
-rw-r--r--src/ci/docker/host-x86_64/dist-various-2/Dockerfile1
-rwxr-xr-xsrc/ci/docker/scripts/rfl-build.sh2
-rw-r--r--src/doc/rustc/src/SUMMARY.md1
-rw-r--r--src/doc/rustc/src/platform-support.md1
-rw-r--r--src/doc/rustc/src/platform-support/wasm32-unknown-unknown.md24
-rw-r--r--src/doc/rustc/src/platform-support/wasm32v1-none.md109
-rw-r--r--src/tools/build-manifest/src/main.rs1
-rw-r--r--src/tools/compiletest/src/common.rs3
-rw-r--r--src/tools/compiletest/src/lib.rs7
-rw-r--r--src/tools/compiletest/src/runtest.rs84
15 files changed, 225 insertions, 114 deletions
diff --git a/src/bootstrap/bootstrap.py b/src/bootstrap/bootstrap.py
index 04909cd7921..d7ae0299dd6 100644
--- a/src/bootstrap/bootstrap.py
+++ b/src/bootstrap/bootstrap.py
@@ -1092,9 +1092,6 @@ class RustBuild(object):
             if not os.path.exists(cargo_dir):
                 eprint('ERROR: vendoring required, but .cargo/config does not exist.')
                 raise Exception("{} not found".format(cargo_dir))
-        else:
-            if os.path.exists(cargo_dir):
-                shutil.rmtree(cargo_dir)
 
 def parse_args(args):
     """Parse the command line arguments that the python script needs."""
diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs
index 27acaff8255..2ad1b39a87c 100644
--- a/src/bootstrap/src/core/build_steps/test.rs
+++ b/src/bootstrap/src/core/build_steps/test.rs
@@ -1836,6 +1836,9 @@ NOTE: if you're sure you want to do this, please open an issue as to why. In the
         if builder.config.cmd.only_modified() {
             cmd.arg("--only-modified");
         }
+        if let Some(compiletest_diff_tool) = &builder.config.compiletest_diff_tool {
+            cmd.arg("--compiletest-diff-tool").arg(compiletest_diff_tool);
+        }
 
         let mut flags = if is_rustdoc { Vec::new() } else { vec!["-Crpath".to_string()] };
         flags.push(format!("-Cdebuginfo={}", builder.config.rust_debuginfo_level_tests));
diff --git a/src/bootstrap/src/core/build_steps/tool.rs b/src/bootstrap/src/core/build_steps/tool.rs
index 50f71e167db..bb837eb8137 100644
--- a/src/bootstrap/src/core/build_steps/tool.rs
+++ b/src/bootstrap/src/core/build_steps/tool.rs
@@ -1,8 +1,6 @@
 use std::path::PathBuf;
 use std::{env, fs};
 
-use build_helper::git::get_closest_merge_commit;
-
 use crate::core::build_steps::compile;
 use crate::core::build_steps::toolstate::ToolState;
 use crate::core::builder;
@@ -10,7 +8,7 @@ use crate::core::builder::{Builder, Cargo as CargoCommand, RunConfig, ShouldRun,
 use crate::core::config::TargetSelection;
 use crate::utils::channel::GitInfo;
 use crate::utils::exec::{BootstrapCommand, command};
-use crate::utils::helpers::{add_dylib_path, exe, git, t};
+use crate::utils::helpers::{add_dylib_path, exe, t};
 use crate::{Compiler, Kind, Mode, gha};
 
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
@@ -596,28 +594,11 @@ impl Step for Rustdoc {
             && target_compiler.stage > 0
             && builder.rust_info().is_managed_git_subrepository()
         {
-            let commit = get_closest_merge_commit(
-                Some(&builder.config.src),
-                &builder.config.git_config(),
-                &[],
-            )
-            .unwrap();
-
-            let librustdoc_src = builder.config.src.join("src/librustdoc");
-            let rustdoc_src = builder.config.src.join("src/tools/rustdoc");
-
-            // FIXME: The change detection logic here is quite similar to `Config::download_ci_rustc_commit`.
-            // It would be better to unify them.
-            let has_changes = !git(Some(&builder.config.src))
-                .allow_failure()
-                .run_always()
-                .args(["diff-index", "--quiet", &commit])
-                .arg("--")
-                .arg(librustdoc_src)
-                .arg(rustdoc_src)
-                .run(builder);
-
-            if !has_changes {
+            let files_to_track = &["src/librustdoc", "src/tools/rustdoc"];
+
+            // Check if unchanged
+            if builder.config.last_modified_commit(files_to_track, "download-rustc", true).is_some()
+            {
                 let precompiled_rustdoc = builder
                     .config
                     .ci_rustc_dir()
diff --git a/src/bootstrap/src/core/config/config.rs b/src/bootstrap/src/core/config/config.rs
index aeb81b14638..444c97a2048 100644
--- a/src/bootstrap/src/core/config/config.rs
+++ b/src/bootstrap/src/core/config/config.rs
@@ -368,6 +368,9 @@ pub struct Config {
     /// The paths to work with. For example: with `./x check foo bar` we get
     /// `paths=["foo", "bar"]`.
     pub paths: Vec<PathBuf>,
+
+    /// Command for visual diff display, e.g. `diff-tool --color=always`.
+    pub compiletest_diff_tool: Option<String>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -892,6 +895,7 @@ define_config! {
         android_ndk: Option<PathBuf> = "android-ndk",
         optimized_compiler_builtins: Option<bool> = "optimized-compiler-builtins",
         jobs: Option<u32> = "jobs",
+        compiletest_diff_tool: Option<String> = "compiletest-diff-tool",
     }
 }
 
@@ -1512,6 +1516,7 @@ impl Config {
             android_ndk,
             optimized_compiler_builtins,
             jobs,
+            compiletest_diff_tool,
         } = toml.build.unwrap_or_default();
 
         config.jobs = Some(threads_from_config(flags.jobs.unwrap_or(jobs.unwrap_or(0))));
@@ -2158,6 +2163,7 @@ impl Config {
         config.rust_debuginfo_level_tests = debuginfo_level_tests.unwrap_or(DebuginfoLevel::None);
         config.optimized_compiler_builtins =
             optimized_compiler_builtins.unwrap_or(config.channel != "dev");
+        config.compiletest_diff_tool = compiletest_diff_tool;
 
         let download_rustc = config.download_rustc_commit.is_some();
         // See https://github.com/rust-lang/compiler-team/issues/326
@@ -2754,25 +2760,25 @@ impl Config {
             }
         };
 
-        let files_to_track = &[
-            self.src.join("compiler"),
-            self.src.join("library"),
-            self.src.join("src/version"),
-            self.src.join("src/stage0"),
-            self.src.join("src/ci/channel"),
-        ];
+        let files_to_track =
+            &["compiler", "library", "src/version", "src/stage0", "src/ci/channel"];
 
         // Look for a version to compare to based on the current commit.
         // Only commits merged by bors will have CI artifacts.
-        let commit =
-            get_closest_merge_commit(Some(&self.src), &self.git_config(), files_to_track).unwrap();
-        if commit.is_empty() {
-            println!("ERROR: could not find commit hash for downloading rustc");
-            println!("HELP: maybe your repository history is too shallow?");
-            println!("HELP: consider disabling `download-rustc`");
-            println!("HELP: or fetch enough history to include one upstream commit");
-            crate::exit!(1);
-        }
+        let commit = match self.last_modified_commit(files_to_track, "download-rustc", if_unchanged)
+        {
+            Some(commit) => commit,
+            None => {
+                if if_unchanged {
+                    return None;
+                }
+                println!("ERROR: could not find commit hash for downloading rustc");
+                println!("HELP: maybe your repository history is too shallow?");
+                println!("HELP: consider disabling `download-rustc`");
+                println!("HELP: or fetch enough history to include one upstream commit");
+                crate::exit!(1);
+            }
+        };
 
         if CiEnv::is_ci() && {
             let head_sha =
@@ -2787,31 +2793,7 @@ impl Config {
             return None;
         }
 
-        // Warn if there were changes to the compiler or standard library since the ancestor commit.
-        let has_changes = !t!(helpers::git(Some(&self.src))
-            .args(["diff-index", "--quiet", &commit])
-            .arg("--")
-            .args(files_to_track)
-            .as_command_mut()
-            .status())
-        .success();
-        if has_changes {
-            if if_unchanged {
-                if self.is_verbose() {
-                    println!(
-                        "WARNING: saw changes to compiler/ or library/ since {commit}; \
-                            ignoring `download-rustc`"
-                    );
-                }
-                return None;
-            }
-            println!(
-                "WARNING: `download-rustc` is enabled, but there are changes to \
-                    compiler/ or library/"
-            );
-        }
-
-        Some(commit.to_string())
+        Some(commit)
     }
 
     fn parse_download_ci_llvm(
diff --git a/src/bootstrap/src/utils/change_tracker.rs b/src/bootstrap/src/utils/change_tracker.rs
index 9169bc90a45..fac24572a87 100644
--- a/src/bootstrap/src/utils/change_tracker.rs
+++ b/src/bootstrap/src/utils/change_tracker.rs
@@ -280,4 +280,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
         severity: ChangeSeverity::Info,
         summary: "Allow setting `--jobs` in config.toml with `build.jobs`.",
     },
+    ChangeInfo {
+        change_id: 131181,
+        severity: ChangeSeverity::Info,
+        summary: "New option `build.compiletest-diff-tool` that adds support for a custom differ for compiletest",
+    },
 ];
diff --git a/src/ci/docker/host-x86_64/dist-various-2/Dockerfile b/src/ci/docker/host-x86_64/dist-various-2/Dockerfile
index 410f0f92e60..8aabfaafbab 100644
--- a/src/ci/docker/host-x86_64/dist-various-2/Dockerfile
+++ b/src/ci/docker/host-x86_64/dist-various-2/Dockerfile
@@ -118,6 +118,7 @@ ENV TARGETS=$TARGETS,wasm32-wasi
 ENV TARGETS=$TARGETS,wasm32-wasip1
 ENV TARGETS=$TARGETS,wasm32-wasip1-threads
 ENV TARGETS=$TARGETS,wasm32-wasip2
+ENV TARGETS=$TARGETS,wasm32v1-none
 ENV TARGETS=$TARGETS,sparcv9-sun-solaris
 ENV TARGETS=$TARGETS,x86_64-pc-solaris
 ENV TARGETS=$TARGETS,x86_64-unknown-linux-gnux32
diff --git a/src/ci/docker/scripts/rfl-build.sh b/src/ci/docker/scripts/rfl-build.sh
index 27dbfc6040c..f07515f7784 100755
--- a/src/ci/docker/scripts/rfl-build.sh
+++ b/src/ci/docker/scripts/rfl-build.sh
@@ -2,7 +2,7 @@
 
 set -euo pipefail
 
-LINUX_VERSION=v6.12-rc2
+LINUX_VERSION=28e848386b92645f93b9f2fdba5882c3ca7fb3e2
 
 # Build rustc, rustdoc, cargo, clippy-driver and rustfmt
 ../x.py build --stage 2 library rustdoc clippy rustfmt
diff --git a/src/doc/rustc/src/SUMMARY.md b/src/doc/rustc/src/SUMMARY.md
index 795908b32c0..e05d9a40f00 100644
--- a/src/doc/rustc/src/SUMMARY.md
+++ b/src/doc/rustc/src/SUMMARY.md
@@ -86,6 +86,7 @@
     - [wasm32-wasip2](platform-support/wasm32-wasip2.md)
     - [wasm32-unknown-emscripten](platform-support/wasm32-unknown-emscripten.md)
     - [wasm32-unknown-unknown](platform-support/wasm32-unknown-unknown.md)
+    - [wasm32v1-none](platform-support/wasm32v1-none.md)
     - [wasm64-unknown-unknown](platform-support/wasm64-unknown-unknown.md)
     - [\*-win7-windows-msvc](platform-support/win7-windows-msvc.md)
     - [x86_64-fortanix-unknown-sgx](platform-support/x86_64-fortanix-unknown-sgx.md)
diff --git a/src/doc/rustc/src/platform-support.md b/src/doc/rustc/src/platform-support.md
index e9c73ef1c2d..5da03d26eb4 100644
--- a/src/doc/rustc/src/platform-support.md
+++ b/src/doc/rustc/src/platform-support.md
@@ -195,6 +195,7 @@ target | std | notes
 `wasm32-wasi` | ✓ | WebAssembly with WASI (undergoing a [rename to `wasm32-wasip1`][wasi-rename])
 [`wasm32-wasip1`](platform-support/wasm32-wasip1.md) | ✓ | WebAssembly with WASI
 [`wasm32-wasip1-threads`](platform-support/wasm32-wasip1-threads.md) | ✓ | WebAssembly with WASI Preview 1 and threads
+[`wasm32v1-none`](platform-support/wasm32v1-none.md) | * | WebAssembly limited to 1.0 features and no imports
 [`x86_64-apple-ios`](platform-support/apple-ios.md) | ✓ | 64-bit x86 iOS
 [`x86_64-apple-ios-macabi`](platform-support/apple-ios-macabi.md) | ✓ | Mac Catalyst on x86_64
 [`x86_64-fortanix-unknown-sgx`](platform-support/x86_64-fortanix-unknown-sgx.md) | ✓ | [Fortanix ABI] for 64-bit Intel SGX
diff --git a/src/doc/rustc/src/platform-support/wasm32-unknown-unknown.md b/src/doc/rustc/src/platform-support/wasm32-unknown-unknown.md
index 48a8df0c4a8..73264aba858 100644
--- a/src/doc/rustc/src/platform-support/wasm32-unknown-unknown.md
+++ b/src/doc/rustc/src/platform-support/wasm32-unknown-unknown.md
@@ -132,10 +132,20 @@ As of the time of this writing the proposals that are enabled by default (the
 
 If you're compiling WebAssembly code for an engine that does not support a
 feature in LLVM's default feature set then the feature must be disabled at
-compile time. Note, though, that enabled features may be used in the standard
-library or precompiled libraries shipped via rustup. This means that not only
-does your own code need to be compiled with the correct set of flags but the
-Rust standard library additionally must be recompiled.
+compile time. There are two approaches to choose from:
+
+  - If you are targeting a feature set no smaller than the W3C WebAssembly Core
+    1.0 recommendation -- which is equivalent to the WebAssembly MVP plus the
+    `mutable-globals` feature -- and you are building `no_std`, then you can
+    simply use the [`wasm32v1-none` target](./wasm32v1-none.md) instead of
+    `wasm32-unknown-unknown`, which uses only those minimal features and
+    includes a core and alloc library built with only those minimal features.
+
+  - Otherwise -- if you need std, or if you need to target the ultra-minimal
+    "MVP" feature set, excluding `mutable-globals` -- you will need to manually
+    specify `-Ctarget-cpu=mvp` and also rebuild the stdlib using that target to
+    ensure no features are used in the stdlib. This in turn requires use of a
+    nightly compiler.
 
 Compiling all code for the initial release of WebAssembly looks like:
 
@@ -150,9 +160,9 @@ then used to recompile the standard library in addition to your own code. This
 will produce a binary that uses only the original WebAssembly features by
 default and no proposals since its inception.
 
-To enable individual features it can be done with `-Ctarget-feature=+foo`.
-Available features for Rust code itself are documented in the [reference] and
-can also be found through:
+To enable individual features on either this target or `wasm32v1-none`, pass
+arguments of the form `-Ctarget-feature=+foo`.  Available features for Rust code
+itself are documented in the [reference] and can also be found through:
 
 ```sh
 $ rustc -Ctarget-feature=help --target wasm32-unknown-unknown
diff --git a/src/doc/rustc/src/platform-support/wasm32v1-none.md b/src/doc/rustc/src/platform-support/wasm32v1-none.md
new file mode 100644
index 00000000000..46f89c20113
--- /dev/null
+++ b/src/doc/rustc/src/platform-support/wasm32v1-none.md
@@ -0,0 +1,109 @@
+# `wasm32v1-none`
+
+**Tier: 2**
+
+The `wasm32v1-none` target is a WebAssembly compilation target that:
+
+- Imports nothing from its host environment
+- Enables no proposals / features past the [W3C WebAssembly Core 1.0 spec]
+
+[W3C WebAssembly Core 1.0 spec]: https://www.w3.org/TR/wasm-core-1/
+
+The target is very similar to [`wasm32-unknown-unknown`](./wasm32-unknown-unknown.md) and similarly uses LLVM's `wasm32-unknown-unknown` backend target. It contains only three minor differences:
+
+* Setting the `target-cpu` to `mvp` rather than the default `generic`. Requesting `mvp` disables _all_ WebAssembly proposals / LLVM target feature flags.
+* Enabling the [Import/Export of Mutable Globals] proposal (i.e. the `+mutable-globals` LLVM target feature flag)
+* Not compiling the `std` library at all, rather than compiling it with stubs.
+
+[Import/Export of Mutable Globals]: https://github.com/WebAssembly/mutable-global
+
+## Target maintainers
+
+- Alex Crichton, https://github.com/alexcrichton
+- Graydon Hoare, https://github.com/graydon
+
+## Requirements
+
+This target is cross-compiled. It does not support `std`, only `core` and `alloc`. Since it imports nothing from its environment, any `std` parts that use OS facilities would be stubbed out with functions-that-fail anyways, and the experience of working with the stub `std` in the `wasm32-unknown-unknown` target was deemed not something worth repeating here.
+
+Everything else about this target's requirements, building, usage and testing is the same as what's described in the [`wasm32-unknown-unknown` document](./wasm32-unknown-unknown.md), just using the target string `wasm32v1-none` in place of `wasm32-unknown-unknown`.
+
+## Conditionally compiling code
+
+It's recommended to conditionally compile code for this target with:
+
+```text
+#[cfg(all(target_family = "wasm", target_os = "none"))]
+```
+
+Note that there is no way to tell via `#[cfg]` whether code will be running on
+the web or not.
+
+## Enabled WebAssembly features
+
+As noted above, _no WebAssembly proposals past 1.0_ are enabled on this target by default. Indeed, the entire point of this target is to have a way to compile for a stable "no post-1.0 proposals" subset of WebAssembly _on stable Rust_.
+
+The [W3C WebAssembly Core 1.0 spec] was adopted as a W3C recommendation in December 2019, and includes exactly one "post-MVP" proposal: the [Import/Export of Mutable Globals] proposal.
+
+All subsequent proposals are _disabled_ on this target by default, though they can be individually enabled by passing LLVM target-feature flags.
+
+For reference sake, the set of proposals that LLVM supports at the time of writing, that this target _does not enable by default_, are listed here along with their LLVM target-feature flags:
+
+* Post-1.0 proposals (integrated into the WebAssembly core 2.0 spec):
+    * [Bulk memory] - `+bulk-memory`
+    * [Sign-extending operations] - `+sign-ext`
+    * [Non-trapping fp-to-int operations] - `+nontrapping-fptoint`
+    * [Multi-value] - `+multivalue`
+    * [Reference Types] - `+reference-types`
+    * [Fixed-width SIMD] - `+simd128`
+* Post-2.0 proposals:
+    * [Threads] (supported by atomics) - `+atomics`
+    * [Exception handling]  - `+exception-handling`
+    * [Extended Constant Expressions]  - `+extended-const`
+    * [Half Precision]  - `+half-precision`
+    * [Multiple memories]- `+multimemory`
+    * [Relaxed SIMD] - `+relaxed-simd`
+    * [Tail call] - `+tail-call`
+
+[Bulk memory]: https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md
+[Sign-extending operations]: https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
+[Non-trapping fp-to-int operations]: https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md
+[Multi-value]: https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
+[Reference Types]: https://github.com/WebAssembly/spec/blob/main/proposals/reference-types/Overview.md
+[Fixed-width SIMD]: https://github.com/WebAssembly/spec/blob/main/proposals/simd/SIMD.md
+[Threads]: https://github.com/webassembly/threads
+[Exception handling]: https://github.com/WebAssembly/exception-handling
+[Extended Constant Expressions]: https://github.com/WebAssembly/extended-const
+[Half Precision]: https://github.com/WebAssembly/half-precision
+[Multiple memories]: https://github.com/WebAssembly/multi-memory
+[Relaxed SIMD]: https://github.com/WebAssembly/relaxed-simd
+[Tail call]: https://github.com/WebAssembly/tail-call
+
+Additional proposals in the future are, of course, also not enabled by default.
+
+## Rationale relative to wasm32-unknown-unknown
+
+As noted in the [`wasm32-unknown-unknown` document](./wasm32-unknown-unknown.md), it is possible to compile with `--target wasm32-unknown-unknown` and disable all WebAssembly proposals "by hand", by passing `-Ctarget-cpu=mvp`. Furthermore one can enable proposals one by one by passing LLVM target feature flags, such as `-Ctarget-feature=+mutable-globals`.
+
+Is it therefore reasonable to wonder what the difference is between building with this:
+
+```sh
+$ rustc --target wasm32-unknown-unknown -Ctarget-cpu=mvp -Ctarget-feature=+mutable-globals
+```
+
+and building with this:
+
+```sh
+$ rustc --target wasm32v1-none
+```
+
+The difference is in how the `core` and `alloc` crates are compiled for distribution with the toolchain, and whether it works on _stable_ Rust toolchains or requires _nightly_ ones. Again referring back to the [`wasm32-unknown-unknown` document](./wasm32-unknown-unknown.md), note that to disable all post-MVP proposals on that target one _actually_ has to compile with this:
+
+```sh
+$ export RUSTFLAGS="-Ctarget-cpu=mvp -Ctarget-feature=+mutable-globals"
+$ cargo +nightly build -Zbuild-std=panic_abort,std --target wasm32-unknown-unknown
+```
+
+Which not only rebuilds `std`, `core` and `alloc` (which is somewhat costly and annoying) but more importantly requires the use of nightly Rust toolchains (for the `-Zbuild-std` flag). This is very undesirable for the target audience, which consists of people targeting WebAssembly implementations that prioritize stability, simplicity and/or security over feature support.
+
+This `wasm32v1-none` target exists as an alternative option that works on stable Rust toolchains, without rebuilding the stdlib.
diff --git a/src/tools/build-manifest/src/main.rs b/src/tools/build-manifest/src/main.rs
index 62e1695cbe3..925cbfe09a4 100644
--- a/src/tools/build-manifest/src/main.rs
+++ b/src/tools/build-manifest/src/main.rs
@@ -161,6 +161,7 @@ static TARGETS: &[&str] = &[
     "wasm32-wasip1",
     "wasm32-wasip1-threads",
     "wasm32-wasip2",
+    "wasm32v1-none",
     "x86_64-apple-darwin",
     "x86_64-apple-ios",
     "x86_64-apple-ios-macabi",
diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs
index ff059940f7c..5070f016d3c 100644
--- a/src/tools/compiletest/src/common.rs
+++ b/src/tools/compiletest/src/common.rs
@@ -387,6 +387,9 @@ pub struct Config {
     /// True if the profiler runtime is enabled for this target.
     /// Used by the "needs-profiler-runtime" directive in test files.
     pub profiler_runtime: bool,
+
+    /// Command for visual diff display, e.g. `diff-tool --color=always`.
+    pub diff_command: Option<String>,
 }
 
 impl Config {
diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs
index 7d6ede9bcda..0f514db42bf 100644
--- a/src/tools/compiletest/src/lib.rs
+++ b/src/tools/compiletest/src/lib.rs
@@ -175,6 +175,12 @@ pub fn parse_config(args: Vec<String>) -> Config {
             "git-merge-commit-email",
             "email address used for finding merge commits",
             "EMAIL",
+        )
+        .optopt(
+            "",
+            "compiletest-diff-tool",
+            "What custom diff tool to use for displaying compiletest tests.",
+            "COMMAND",
         );
 
     let (argv0, args_) = args.split_first().unwrap();
@@ -364,6 +370,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
         git_merge_commit_email: matches.opt_str("git-merge-commit-email").unwrap(),
 
         profiler_runtime: matches.opt_present("profiler-runtime"),
+        diff_command: matches.opt_str("compiletest-diff-tool"),
     }
 }
 
diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs
index 36c5106ddad..7db37af60d2 100644
--- a/src/tools/compiletest/src/runtest.rs
+++ b/src/tools/compiletest/src/runtest.rs
@@ -2459,7 +2459,7 @@ impl<'test> TestCx<'test> {
         }
     }
 
-    fn compare_output(&self, kind: &str, actual: &str, expected: &str) -> usize {
+    fn compare_output(&self, stream: &str, actual: &str, expected: &str) -> usize {
         let are_different = match (self.force_color_svg(), expected.find('\n'), actual.find('\n')) {
             // FIXME: We ignore the first line of SVG files
             // because the width parameter is non-deterministic.
@@ -2499,56 +2499,66 @@ impl<'test> TestCx<'test> {
             (expected, actual)
         };
 
+        // Write the actual output to a file in build/
+        let test_name = self.config.compare_mode.as_ref().map_or("", |m| m.to_str());
+        let actual_path = self
+            .output_base_name()
+            .with_extra_extension(self.revision.unwrap_or(""))
+            .with_extra_extension(test_name)
+            .with_extra_extension(stream);
+
+        if let Err(err) = fs::write(&actual_path, &actual) {
+            self.fatal(&format!("failed to write {stream} to `{actual_path:?}`: {err}",));
+        }
+        println!("Saved the actual {stream} to {actual_path:?}");
+
+        let expected_path =
+            expected_output_path(self.testpaths, self.revision, &self.config.compare_mode, stream);
+
         if !self.config.bless {
             if expected.is_empty() {
-                println!("normalized {}:\n{}\n", kind, actual);
+                println!("normalized {}:\n{}\n", stream, actual);
             } else {
-                println!("diff of {}:\n", kind);
-                print!("{}", write_diff(expected, actual, 3));
+                println!("diff of {stream}:\n");
+                if let Some(diff_command) = self.config.diff_command.as_deref() {
+                    let mut args = diff_command.split_whitespace();
+                    let name = args.next().unwrap();
+                    match Command::new(name)
+                        .args(args)
+                        .args([&expected_path, &actual_path])
+                        .output()
+                    {
+                        Err(err) => {
+                            self.fatal(&format!(
+                                "failed to call custom diff command `{diff_command}`: {err}"
+                            ));
+                        }
+                        Ok(output) => {
+                            let output = String::from_utf8_lossy(&output.stdout);
+                            print!("{output}");
+                        }
+                    }
+                } else {
+                    print!("{}", write_diff(expected, actual, 3));
+                }
             }
-        }
-
-        let mode = self.config.compare_mode.as_ref().map_or("", |m| m.to_str());
-        let output_file = self
-            .output_base_name()
-            .with_extra_extension(self.revision.unwrap_or(""))
-            .with_extra_extension(mode)
-            .with_extra_extension(kind);
-
-        let mut files = vec![output_file];
-        if self.config.bless {
+        } else {
             // Delete non-revision .stderr/.stdout file if revisions are used.
             // Without this, we'd just generate the new files and leave the old files around.
             if self.revision.is_some() {
                 let old =
-                    expected_output_path(self.testpaths, None, &self.config.compare_mode, kind);
+                    expected_output_path(self.testpaths, None, &self.config.compare_mode, stream);
                 self.delete_file(&old);
             }
-            files.push(expected_output_path(
-                self.testpaths,
-                self.revision,
-                &self.config.compare_mode,
-                kind,
-            ));
-        }
 
-        for output_file in &files {
-            if actual.is_empty() {
-                self.delete_file(output_file);
-            } else if let Err(err) = fs::write(&output_file, &actual) {
-                self.fatal(&format!(
-                    "failed to write {} to `{}`: {}",
-                    kind,
-                    output_file.display(),
-                    err,
-                ));
+            if let Err(err) = fs::write(&expected_path, &actual) {
+                self.fatal(&format!("failed to write {stream} to `{expected_path:?}`: {err}"));
             }
+            println!("Blessing the {stream} of {test_name} in {expected_path:?}");
         }
 
-        println!("\nThe actual {0} differed from the expected {0}.", kind);
-        for output_file in files {
-            println!("Actual {} saved to {}", kind, output_file.display());
-        }
+        println!("\nThe actual {0} differed from the expected {0}.", stream);
+
         if self.config.bless { 0 } else { 1 }
     }