about summary refs log tree commit diff
path: root/src/bootstrap
diff options
context:
space:
mode:
Diffstat (limited to 'src/bootstrap')
-rw-r--r--src/bootstrap/README.md6
-rw-r--r--src/bootstrap/src/bin/main.rs10
-rw-r--r--src/bootstrap/src/bin/rustc.rs7
-rw-r--r--src/bootstrap/src/core/build_steps/check.rs41
-rw-r--r--src/bootstrap/src/core/build_steps/compile.rs33
-rw-r--r--src/bootstrap/src/core/build_steps/doc.rs6
-rw-r--r--src/bootstrap/src/core/build_steps/format.rs1
-rw-r--r--src/bootstrap/src/core/build_steps/llvm.rs38
-rw-r--r--src/bootstrap/src/core/build_steps/setup.rs197
-rw-r--r--src/bootstrap/src/core/build_steps/setup/tests.rs9
-rw-r--r--src/bootstrap/src/core/build_steps/test.rs14
-rw-r--r--src/bootstrap/src/core/build_steps/tool.rs32
-rw-r--r--src/bootstrap/src/core/builder.rs75
-rw-r--r--src/bootstrap/src/core/builder/tests.rs33
-rw-r--r--src/bootstrap/src/core/config/config.rs84
-rw-r--r--src/bootstrap/src/core/config/flags.rs6
-rw-r--r--src/bootstrap/src/core/config/tests.rs68
-rw-r--r--src/bootstrap/src/core/download.rs40
-rw-r--r--src/bootstrap/src/core/sanity.rs7
-rw-r--r--src/bootstrap/src/lib.rs37
-rw-r--r--src/bootstrap/src/utils/change_tracker.rs5
-rw-r--r--src/bootstrap/src/utils/shared_helpers.rs2
22 files changed, 571 insertions, 180 deletions
diff --git a/src/bootstrap/README.md b/src/bootstrap/README.md
index 0ac58645d2d..f036603ee70 100644
--- a/src/bootstrap/README.md
+++ b/src/bootstrap/README.md
@@ -182,10 +182,8 @@ Some general areas that you may be interested in modifying are:
   `Config` struct.
 * Adding a sanity check? Take a look at `bootstrap/src/core/sanity.rs`.
 
-If you make a major change on bootstrap configuration, please remember to:
-
-+ Update `CONFIG_CHANGE_HISTORY` in `src/bootstrap/src/utils/change_tracker.rs`.
-* Update `change-id = {pull-request-id}` in `config.example.toml`.
+If you make a major change on bootstrap configuration, please add a new entry to
+`CONFIG_CHANGE_HISTORY` in `src/bootstrap/src/utils/change_tracker.rs`.
 
 A 'major change' includes
 
diff --git a/src/bootstrap/src/bin/main.rs b/src/bootstrap/src/bin/main.rs
index b9df7336cca..e9ec79e417b 100644
--- a/src/bootstrap/src/bin/main.rs
+++ b/src/bootstrap/src/bin/main.rs
@@ -14,6 +14,7 @@ use bootstrap::{
     Build, CONFIG_CHANGE_HISTORY, Config, Flags, Subcommand, find_recent_config_change_ids,
     human_readable_changes, t,
 };
+use build_helper::ci::CiEnv;
 
 fn main() {
     let args = env::args().skip(1).collect::<Vec<_>>();
@@ -54,9 +55,12 @@ fn main() {
         };
     }
 
-    // check_version warnings are not printed during setup
-    let changelog_suggestion =
-        if matches!(config.cmd, Subcommand::Setup { .. }) { None } else { check_version(&config) };
+    // check_version warnings are not printed during setup, or during CI
+    let changelog_suggestion = if matches!(config.cmd, Subcommand::Setup { .. }) || CiEnv::is_ci() {
+        None
+    } else {
+        check_version(&config)
+    };
 
     // NOTE: Since `./configure` generates a `config.toml`, distro maintainers will see the
     // changelog warning, not the `x.py setup` message.
diff --git a/src/bootstrap/src/bin/rustc.rs b/src/bootstrap/src/bin/rustc.rs
index d04e2fbeb78..18f5a1a58db 100644
--- a/src/bootstrap/src/bin/rustc.rs
+++ b/src/bootstrap/src/bin/rustc.rs
@@ -95,7 +95,6 @@ fn main() {
     // When statically linking `std` into `rustc_driver`, remove `-C prefer-dynamic`
     if env::var("RUSTC_LINK_STD_INTO_RUSTC_DRIVER").unwrap() == "1"
         && crate_name == Some("rustc_driver")
-        && stage != "0"
     {
         if let Some(pos) = args.iter().enumerate().position(|(i, a)| {
             a == "-C" && args.get(i + 1).map(|a| a == "prefer-dynamic").unwrap_or(false)
@@ -137,6 +136,12 @@ fn main() {
         cmd.args(lint_flags.split_whitespace());
     }
 
+    // Conditionally pass `-Zon-broken-pipe=kill` to underlying rustc. Not all binaries want
+    // `-Zon-broken-pipe=kill`, which includes cargo itself.
+    if env::var_os("FORCE_ON_BROKEN_PIPE_KILL").is_some() {
+        cmd.arg("-Z").arg("on-broken-pipe=kill");
+    }
+
     if target.is_some() {
         // The stage0 compiler has a special sysroot distinct from what we
         // actually downloaded, so we just always pass the `--sysroot` option,
diff --git a/src/bootstrap/src/core/build_steps/check.rs b/src/bootstrap/src/core/build_steps/check.rs
index 7671fc7e013..f419bebdc12 100644
--- a/src/bootstrap/src/core/build_steps/check.rs
+++ b/src/bootstrap/src/core/build_steps/check.rs
@@ -398,7 +398,14 @@ impl Step for RustAnalyzer {
 }
 
 macro_rules! tool_check_step {
-    ($name:ident, $path:literal, $($alias:literal, )* $source_type:path $(, $default:literal )?) => {
+    (
+        $name:ident,
+        $display_name:literal,
+        $path:literal,
+        $($alias:literal, )*
+        $source_type:path
+        $(, $default:literal )?
+    ) => {
         #[derive(Debug, Clone, PartialEq, Eq, Hash)]
         pub struct $name {
             pub target: TargetSelection,
@@ -441,7 +448,7 @@ macro_rules! tool_check_step {
                     cargo.arg("--all-targets");
                 }
 
-                let _guard = builder.msg_check(&concat!(stringify!($name), " artifacts").to_lowercase(), target);
+                let _guard = builder.msg_check(&format!("{} artifacts", $display_name), target);
                 run_cargo(
                     builder,
                     cargo,
@@ -468,20 +475,30 @@ macro_rules! tool_check_step {
     };
 }
 
-tool_check_step!(Rustdoc, "src/tools/rustdoc", "src/librustdoc", SourceType::InTree);
+tool_check_step!(Rustdoc, "rustdoc", "src/tools/rustdoc", "src/librustdoc", SourceType::InTree);
 // Clippy, miri and Rustfmt are hybrids. They are external tools, but use a git subtree instead
 // of a submodule. Since the SourceType only drives the deny-warnings
 // behavior, treat it as in-tree so that any new warnings in clippy will be
 // rejected.
-tool_check_step!(Clippy, "src/tools/clippy", SourceType::InTree);
-tool_check_step!(Miri, "src/tools/miri", SourceType::InTree);
-tool_check_step!(CargoMiri, "src/tools/miri/cargo-miri", SourceType::InTree);
-tool_check_step!(Rls, "src/tools/rls", SourceType::InTree);
-tool_check_step!(Rustfmt, "src/tools/rustfmt", SourceType::InTree);
-tool_check_step!(MiroptTestTools, "src/tools/miropt-test-tools", SourceType::InTree);
-tool_check_step!(TestFloatParse, "src/etc/test-float-parse", SourceType::InTree);
-
-tool_check_step!(Bootstrap, "src/bootstrap", SourceType::InTree, false);
+tool_check_step!(Clippy, "clippy", "src/tools/clippy", SourceType::InTree);
+tool_check_step!(Miri, "miri", "src/tools/miri", SourceType::InTree);
+tool_check_step!(CargoMiri, "cargo-miri", "src/tools/miri/cargo-miri", SourceType::InTree);
+tool_check_step!(Rls, "rls", "src/tools/rls", SourceType::InTree);
+tool_check_step!(Rustfmt, "rustfmt", "src/tools/rustfmt", SourceType::InTree);
+tool_check_step!(
+    MiroptTestTools,
+    "miropt-test-tools",
+    "src/tools/miropt-test-tools",
+    SourceType::InTree
+);
+tool_check_step!(
+    TestFloatParse,
+    "test-float-parse",
+    "src/etc/test-float-parse",
+    SourceType::InTree
+);
+
+tool_check_step!(Bootstrap, "bootstrap", "src/bootstrap", SourceType::InTree, false);
 
 /// Cargo's output path for the standard library in a given stage, compiled
 /// by a particular compiler for the specified target.
diff --git a/src/bootstrap/src/core/build_steps/compile.rs b/src/bootstrap/src/core/build_steps/compile.rs
index eaa982d4e2b..27bbc8bd8ff 100644
--- a/src/bootstrap/src/core/build_steps/compile.rs
+++ b/src/bootstrap/src/core/build_steps/compile.rs
@@ -1053,8 +1053,19 @@ pub fn rustc_cargo(
 
     cargo.rustdocflag("-Zcrate-attr=warn(rust_2018_idioms)");
 
-    // If the rustc output is piped to e.g. `head -n1` we want the process to be
-    // killed, rather than having an error bubble up and cause a panic.
+    // If the rustc output is piped to e.g. `head -n1` we want the process to be killed, rather than
+    // having an error bubble up and cause a panic.
+    //
+    // FIXME(jieyouxu): this flag is load-bearing for rustc to not ICE on broken pipes, because
+    // rustc internally sometimes uses std `println!` -- but std `println!` by default will panic on
+    // broken pipes, and uncaught panics will manifest as an ICE. The compiler *should* handle this
+    // properly, but this flag is set in the meantime to paper over the I/O errors.
+    //
+    // See <https://github.com/rust-lang/rust/issues/131059> for details.
+    //
+    // Also see the discussion for properly handling I/O errors related to broken pipes, i.e. safe
+    // variants of `println!` in
+    // <https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/Internal.20lint.20for.20raw.20.60print!.60.20and.20.60println!.60.3F>.
     cargo.rustflag("-Zon-broken-pipe=kill");
 
     if builder.config.llvm_enzyme {
@@ -1923,8 +1934,24 @@ impl Step for Assemble {
         let src_libdir = builder.sysroot_libdir(build_compiler, host);
         for f in builder.read_dir(&src_libdir) {
             let filename = f.file_name().into_string().unwrap();
-            if (is_dylib(&filename) || is_debug_info(&filename)) && !proc_macros.contains(&filename)
+
+            let is_proc_macro = proc_macros.contains(&filename);
+            let is_dylib_or_debug = is_dylib(&filename) || is_debug_info(&filename);
+
+            // If we link statically to stdlib, do not copy the libstd dynamic library file
+            // FIXME: Also do this for Windows once incremental post-optimization stage0 tests
+            // work without std.dll (see https://github.com/rust-lang/rust/pull/131188).
+            let can_be_rustc_dynamic_dep = if builder
+                .link_std_into_rustc_driver(target_compiler.host)
+                && !target_compiler.host.is_windows()
             {
+                let is_std = filename.starts_with("std-") || filename.starts_with("libstd-");
+                !is_std
+            } else {
+                true
+            };
+
+            if is_dylib_or_debug && can_be_rustc_dynamic_dep && !is_proc_macro {
                 builder.copy_link(&f.path(), &rustc_libdir.join(&filename));
             }
         }
diff --git a/src/bootstrap/src/core/build_steps/doc.rs b/src/bootstrap/src/core/build_steps/doc.rs
index 3d504c3771f..ca2b8742647 100644
--- a/src/bootstrap/src/core/build_steps/doc.rs
+++ b/src/bootstrap/src/core/build_steps/doc.rs
@@ -82,7 +82,7 @@ book!(
     EditionGuide, "src/doc/edition-guide", "edition-guide", &[], submodule;
     EmbeddedBook, "src/doc/embedded-book", "embedded-book", &[], submodule;
     Nomicon, "src/doc/nomicon", "nomicon", &[], submodule;
-    RustByExample, "src/doc/rust-by-example", "rust-by-example", &["ja"], submodule;
+    RustByExample, "src/doc/rust-by-example", "rust-by-example", &["ja", "zh"], submodule;
     RustdocBook, "src/doc/rustdoc", "rustdoc", &[];
     StyleGuide, "src/doc/style-guide", "style-guide", &[];
 );
@@ -718,6 +718,10 @@ fn doc_std(
         .arg("--target-dir")
         .arg(&*target_dir.to_string_lossy())
         .arg("-Zskip-rustdoc-fingerprint")
+        .arg("-Zrustdoc-map")
+        .rustdocflag("--extern-html-root-url")
+        .rustdocflag("std_detect=https://docs.rs/std_detect/latest/")
+        .rustdocflag("--extern-html-root-takes-precedence")
         .rustdocflag("--resource-suffix")
         .rustdocflag(&builder.version);
     for arg in extra_args {
diff --git a/src/bootstrap/src/core/build_steps/format.rs b/src/bootstrap/src/core/build_steps/format.rs
index 952d8d73328..5ca4321d855 100644
--- a/src/bootstrap/src/core/build_steps/format.rs
+++ b/src/bootstrap/src/core/build_steps/format.rs
@@ -93,6 +93,7 @@ fn get_modified_rs_files(build: &Builder<'_>) -> Result<Option<Vec<String>>, Str
     if !verify_rustfmt_version(build) {
         return Ok(None);
     }
+
     get_git_modified_files(&build.config.git_config(), Some(&build.config.src), &["rs"])
 }
 
diff --git a/src/bootstrap/src/core/build_steps/llvm.rs b/src/bootstrap/src/core/build_steps/llvm.rs
index 30213584157..a2d40f6fbd8 100644
--- a/src/bootstrap/src/core/build_steps/llvm.rs
+++ b/src/bootstrap/src/core/build_steps/llvm.rs
@@ -20,10 +20,9 @@ use build_helper::git::get_closest_merge_commit;
 
 use crate::core::builder::{Builder, RunConfig, ShouldRun, Step};
 use crate::core::config::{Config, TargetSelection};
-use crate::utils::channel;
 use crate::utils::exec::command;
 use crate::utils::helpers::{
-    self, HashStamp, exe, get_clang_cl_resource_dir, output, t, unhashed_basename, up_to_date,
+    self, HashStamp, exe, get_clang_cl_resource_dir, t, unhashed_basename, up_to_date,
 };
 use crate::{CLang, GitRepo, Kind, generate_smart_stamp_hash};
 
@@ -166,7 +165,7 @@ pub(crate) fn detect_llvm_sha(config: &Config, is_git: bool) -> String {
             config.src.join("src/version"),
         ])
         .unwrap()
-    } else if let Some(info) = channel::read_commit_info_file(&config.src) {
+    } else if let Some(info) = crate::utils::channel::read_commit_info_file(&config.src) {
         info.sha.trim().to_owned()
     } else {
         "".to_owned()
@@ -242,15 +241,29 @@ pub(crate) fn is_ci_llvm_available(config: &Config, asserts: bool) -> bool {
 
 /// Returns true if we're running in CI with modified LLVM (and thus can't download it)
 pub(crate) fn is_ci_llvm_modified(config: &Config) -> bool {
-    CiEnv::is_rust_lang_managed_ci_job() && config.rust_info.is_managed_git_subrepository() && {
-        // We assume we have access to git, so it's okay to unconditionally pass
-        // `true` here.
-        let llvm_sha = detect_llvm_sha(config, true);
-        let head_sha =
-            output(helpers::git(Some(&config.src)).arg("rev-parse").arg("HEAD").as_command_mut());
-        let head_sha = head_sha.trim();
-        llvm_sha == head_sha
+    // If not running in a CI environment, return false.
+    if !CiEnv::is_ci() {
+        return false;
+    }
+
+    // In rust-lang/rust managed CI, assert the existence of the LLVM submodule.
+    if CiEnv::is_rust_lang_managed_ci_job() {
+        assert!(
+            config.in_tree_llvm_info.is_managed_git_subrepository(),
+            "LLVM submodule must be fetched in rust-lang/rust managed CI builders."
+        );
     }
+    // If LLVM submodule isn't present, skip the change check as it won't work.
+    else if !config.in_tree_llvm_info.is_managed_git_subrepository() {
+        return false;
+    }
+
+    let llvm_sha = detect_llvm_sha(config, true);
+    let head_sha = crate::output(
+        helpers::git(Some(&config.src)).arg("rev-parse").arg("HEAD").as_command_mut(),
+    );
+    let head_sha = head_sha.trim();
+    llvm_sha == head_sha
 }
 
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
@@ -1215,6 +1228,9 @@ fn supported_sanitizers(
         "aarch64-unknown-linux-ohos" => {
             common_libs("linux", "aarch64", &["asan", "lsan", "msan", "tsan", "hwasan"])
         }
+        "loongarch64-unknown-linux-gnu" | "loongarch64-unknown-linux-musl" => {
+            common_libs("linux", "loongarch64", &["asan", "lsan", "msan", "tsan"])
+        }
         "x86_64-apple-darwin" => darwin_libs("osx", &["asan", "lsan", "tsan"]),
         "x86_64-unknown-fuchsia" => common_libs("fuchsia", "x86_64", &["asan"]),
         "x86_64-apple-ios" => darwin_libs("iossim", &["asan", "tsan"]),
diff --git a/src/bootstrap/src/core/build_steps/setup.rs b/src/bootstrap/src/core/build_steps/setup.rs
index 0ee2cb451f3..51964977933 100644
--- a/src/bootstrap/src/core/build_steps/setup.rs
+++ b/src/bootstrap/src/core/build_steps/setup.rs
@@ -35,21 +35,6 @@ pub enum Profile {
 
 static PROFILE_DIR: &str = "src/bootstrap/defaults";
 
-/// A list of historical hashes of `src/etc/rust_analyzer_settings.json`.
-/// New entries should be appended whenever this is updated so we can detect
-/// outdated vs. user-modified settings files.
-static SETTINGS_HASHES: &[&str] = &[
-    "ea67e259dedf60d4429b6c349a564ffcd1563cf41c920a856d1f5b16b4701ac8",
-    "56e7bf011c71c5d81e0bf42e84938111847a810eee69d906bba494ea90b51922",
-    "af1b5efe196aed007577899db9dae15d6dbc923d6fa42fa0934e68617ba9bbe0",
-    "3468fea433c25fff60be6b71e8a215a732a7b1268b6a83bf10d024344e140541",
-    "47d227f424bf889b0d899b9cc992d5695e1b78c406e183cd78eafefbe5488923",
-    "b526bd58d0262dd4dda2bff5bc5515b705fb668a46235ace3e057f807963a11a",
-    "828666b021d837a33e78d870b56d34c88a5e2c85de58b693607ec574f0c27000",
-    "811fb3b063c739d261fd8590dd30242e117908f5a095d594fa04585daa18ec4d",
-];
-static RUST_ANALYZER_SETTINGS: &str = include_str!("../../../../etc/rust_analyzer_settings.json");
-
 impl Profile {
     fn include_path(&self, src_path: &Path) -> PathBuf {
         PathBuf::from(format!("{}/{PROFILE_DIR}/config.{}.toml", src_path.display(), self))
@@ -533,46 +518,162 @@ undesirable, simply delete the `pre-push` file from .git/hooks."
     Ok(())
 }
 
-/// Sets up or displays `src/etc/rust_analyzer_settings.json`
+/// Handles editor-specific setup differences
+#[derive(Clone, Debug, Eq, PartialEq)]
+enum EditorKind {
+    Vscode,
+    Vim,
+    Emacs,
+    Helix,
+}
+
+impl EditorKind {
+    fn prompt_user() -> io::Result<Option<EditorKind>> {
+        let prompt_str = "Available editors:
+1. vscode
+2. vim
+3. emacs
+4. helix
+
+Select which editor you would like to set up [default: None]: ";
+
+        let mut input = String::new();
+        loop {
+            print!("{}", prompt_str);
+            io::stdout().flush()?;
+            input.clear();
+            io::stdin().read_line(&mut input)?;
+            match input.trim().to_lowercase().as_str() {
+                "1" | "vscode" => return Ok(Some(EditorKind::Vscode)),
+                "2" | "vim" => return Ok(Some(EditorKind::Vim)),
+                "3" | "emacs" => return Ok(Some(EditorKind::Emacs)),
+                "4" | "helix" => return Ok(Some(EditorKind::Helix)),
+                "" => return Ok(None),
+                _ => {
+                    eprintln!("ERROR: unrecognized option '{}'", input.trim());
+                    eprintln!("NOTE: press Ctrl+C to exit");
+                }
+            };
+        }
+    }
+
+    /// A list of historical hashes of each LSP settings file
+    /// New entries should be appended whenever this is updated so we can detect
+    /// outdated vs. user-modified settings files.
+    fn hashes(&self) -> Vec<&str> {
+        match self {
+            EditorKind::Vscode | EditorKind::Vim => vec![
+                "ea67e259dedf60d4429b6c349a564ffcd1563cf41c920a856d1f5b16b4701ac8",
+                "56e7bf011c71c5d81e0bf42e84938111847a810eee69d906bba494ea90b51922",
+                "af1b5efe196aed007577899db9dae15d6dbc923d6fa42fa0934e68617ba9bbe0",
+                "3468fea433c25fff60be6b71e8a215a732a7b1268b6a83bf10d024344e140541",
+                "47d227f424bf889b0d899b9cc992d5695e1b78c406e183cd78eafefbe5488923",
+                "b526bd58d0262dd4dda2bff5bc5515b705fb668a46235ace3e057f807963a11a",
+                "828666b021d837a33e78d870b56d34c88a5e2c85de58b693607ec574f0c27000",
+                "811fb3b063c739d261fd8590dd30242e117908f5a095d594fa04585daa18ec4d",
+            ],
+            EditorKind::Emacs => vec![
+                "51068d4747a13732440d1a8b8f432603badb1864fa431d83d0fd4f8fa57039e0",
+                "d29af4d949bbe2371eac928a3c31cf9496b1701aa1c45f11cd6c759865ad5c45",
+            ],
+            EditorKind::Helix => {
+                vec!["2d3069b8cf1b977e5d4023965eb6199597755e6c96c185ed5f2854f98b83d233"]
+            }
+        }
+    }
+
+    fn settings_path(&self, config: &Config) -> PathBuf {
+        config.src.join(self.settings_short_path())
+    }
+
+    fn settings_short_path(&self) -> PathBuf {
+        self.settings_folder().join(match self {
+            EditorKind::Vscode => "settings.json",
+            EditorKind::Vim => "coc-settings.json",
+            EditorKind::Emacs => ".dir-locals.el",
+            EditorKind::Helix => "languages.toml",
+        })
+    }
+
+    fn settings_folder(&self) -> PathBuf {
+        match self {
+            EditorKind::Vscode => PathBuf::from(".vscode"),
+            EditorKind::Vim => PathBuf::from(".vim"),
+            EditorKind::Emacs => PathBuf::new(),
+            EditorKind::Helix => PathBuf::from(".helix"),
+        }
+    }
+
+    fn settings_template(&self) -> &str {
+        match self {
+            EditorKind::Vscode | EditorKind::Vim => {
+                include_str!("../../../../etc/rust_analyzer_settings.json")
+            }
+            EditorKind::Emacs => include_str!("../../../../etc/rust_analyzer_eglot.el"),
+            EditorKind::Helix => include_str!("../../../../etc/rust_analyzer_helix.toml"),
+        }
+    }
+
+    fn backup_extension(&self) -> String {
+        format!("{}.bak", self.settings_short_path().extension().unwrap().to_str().unwrap())
+    }
+}
+
+/// Sets up or displays the LSP config for one of the supported editors
 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub struct Vscode;
+pub struct Editor;
 
-impl Step for Vscode {
+impl Step for Editor {
     type Output = ();
     const DEFAULT: bool = true;
+
     fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
-        run.alias("vscode")
+        run.alias("editor")
     }
+
     fn make_run(run: RunConfig<'_>) {
         if run.builder.config.dry_run() {
             return;
         }
         if let [cmd] = &run.paths[..] {
-            if cmd.assert_single_path().path.as_path().as_os_str() == "vscode" {
-                run.builder.ensure(Vscode);
+            if cmd.assert_single_path().path.as_path().as_os_str() == "editor" {
+                run.builder.ensure(Editor);
             }
         }
     }
+
     fn run(self, builder: &Builder<'_>) -> Self::Output {
         let config = &builder.config;
         if config.dry_run() {
             return;
         }
-        while !t!(create_vscode_settings_maybe(config)) {}
+        match EditorKind::prompt_user() {
+            Ok(editor_kind) => {
+                if let Some(editor_kind) = editor_kind {
+                    while !t!(create_editor_settings_maybe(config, editor_kind.clone())) {}
+                } else {
+                    println!("Ok, skipping editor setup!");
+                }
+            }
+            Err(e) => eprintln!("Could not determine the editor: {e}"),
+        }
     }
 }
 
-/// Create a `.vscode/settings.json` file for rustc development, or just print it
+/// Create the recommended editor LSP config file for rustc development, or just print it
 /// If this method should be re-called, it returns `false`.
-fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
-    let (current_hash, historical_hashes) = SETTINGS_HASHES.split_last().unwrap();
-    let vscode_settings = config.src.join(".vscode").join("settings.json");
-    // If None, no settings.json exists
+fn create_editor_settings_maybe(config: &Config, editor: EditorKind) -> io::Result<bool> {
+    let hashes = editor.hashes();
+    let (current_hash, historical_hashes) = hashes.split_last().unwrap();
+    let settings_path = editor.settings_path(config);
+    let settings_short_path = editor.settings_short_path();
+    let settings_filename = settings_short_path.to_str().unwrap();
+    // If None, no settings file exists
     // If Some(true), is a previous version of settings.json
     // If Some(false), is not a previous version (i.e. user modified)
     // If it's up to date we can just skip this
     let mut mismatched_settings = None;
-    if let Ok(current) = fs::read_to_string(&vscode_settings) {
+    if let Ok(current) = fs::read_to_string(&settings_path) {
         let mut hasher = sha2::Sha256::new();
         hasher.update(&current);
         let hash = hex_encode(hasher.finalize().as_slice());
@@ -585,20 +686,21 @@ fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
         }
     }
     println!(
-        "\nx.py can automatically install the recommended `.vscode/settings.json` file for rustc development"
+        "\nx.py can automatically install the recommended `{settings_filename}` file for rustc development"
     );
+
     match mismatched_settings {
-        Some(true) => eprintln!(
-            "WARNING: existing `.vscode/settings.json` is out of date, x.py will update it"
-        ),
+        Some(true) => {
+            eprintln!("WARNING: existing `{settings_filename}` is out of date, x.py will update it")
+        }
         Some(false) => eprintln!(
-            "WARNING: existing `.vscode/settings.json` has been modified by user, x.py will back it up and replace it"
+            "WARNING: existing `{settings_filename}` has been modified by user, x.py will back it up and replace it"
         ),
         _ => (),
     }
-    let should_create = match prompt_user(
-        "Would you like to create/update settings.json? (Press 'p' to preview values): [y/N]",
-    )? {
+    let should_create = match prompt_user(&format!(
+        "Would you like to create/update `{settings_filename}`? (Press 'p' to preview values): [y/N]"
+    ))? {
         Some(PromptResult::Yes) => true,
         Some(PromptResult::Print) => false,
         _ => {
@@ -607,9 +709,9 @@ fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
         }
     };
     if should_create {
-        let path = config.src.join(".vscode");
-        if !path.exists() {
-            fs::create_dir(&path)?;
+        let settings_folder_path = config.src.join(editor.settings_folder());
+        if !settings_folder_path.exists() {
+            fs::create_dir(settings_folder_path)?;
         }
         let verb = match mismatched_settings {
             // exists but outdated, we can replace this
@@ -617,18 +719,21 @@ fn create_vscode_settings_maybe(config: &Config) -> io::Result<bool> {
             // exists but user modified, back it up
             Some(false) => {
                 // exists and is not current version or outdated, so back it up
-                let mut backup = vscode_settings.clone();
-                backup.set_extension("json.bak");
-                eprintln!("WARNING: copying `settings.json` to `settings.json.bak`");
-                fs::copy(&vscode_settings, &backup)?;
+                let backup = settings_path.clone().with_extension(editor.backup_extension());
+                eprintln!(
+                    "WARNING: copying `{}` to `{}`",
+                    settings_path.file_name().unwrap().to_str().unwrap(),
+                    backup.file_name().unwrap().to_str().unwrap(),
+                );
+                fs::copy(&settings_path, &backup)?;
                 "Updated"
             }
             _ => "Created",
         };
-        fs::write(&vscode_settings, RUST_ANALYZER_SETTINGS)?;
-        println!("{verb} `.vscode/settings.json`");
+        fs::write(&settings_path, editor.settings_template())?;
+        println!("{verb} `{}`", settings_filename);
     } else {
-        println!("\n{RUST_ANALYZER_SETTINGS}");
+        println!("\n{}", editor.settings_template());
     }
     Ok(should_create)
 }
diff --git a/src/bootstrap/src/core/build_steps/setup/tests.rs b/src/bootstrap/src/core/build_steps/setup/tests.rs
index 3552224f33b..f3d4b6aa4db 100644
--- a/src/bootstrap/src/core/build_steps/setup/tests.rs
+++ b/src/bootstrap/src/core/build_steps/setup/tests.rs
@@ -1,16 +1,17 @@
 use sha2::Digest;
 
-use super::{RUST_ANALYZER_SETTINGS, SETTINGS_HASHES};
+use super::EditorKind;
 use crate::utils::helpers::hex_encode;
 
 #[test]
 fn check_matching_settings_hash() {
+    let editor = EditorKind::Vscode;
     let mut hasher = sha2::Sha256::new();
-    hasher.update(&RUST_ANALYZER_SETTINGS);
+    hasher.update(&editor.settings_template());
     let hash = hex_encode(hasher.finalize().as_slice());
     assert_eq!(
         &hash,
-        SETTINGS_HASHES.last().unwrap(),
-        "Update `SETTINGS_HASHES` with the new hash of `src/etc/rust_analyzer_settings.json`"
+        editor.hashes().last().unwrap(),
+        "Update `EditorKind::hashes()` with the new hash of `src/etc/rust_analyzer_settings.json`"
     );
 }
diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs
index 7283b0e9574..e25b571acba 100644
--- a/src/bootstrap/src/core/build_steps/test.rs
+++ b/src/bootstrap/src/core/build_steps/test.rs
@@ -1394,12 +1394,6 @@ default_test!(Ui { path: "tests/ui", mode: "ui", suite: "ui" });
 
 default_test!(Crashes { path: "tests/crashes", mode: "crashes", suite: "crashes" });
 
-default_test!(RunPassValgrind {
-    path: "tests/run-pass-valgrind",
-    mode: "run-pass-valgrind",
-    suite: "run-pass-valgrind"
-});
-
 default_test!(Codegen { path: "tests/codegen", mode: "codegen", suite: "codegen" });
 
 default_test!(CodegenUnits {
@@ -1703,7 +1697,11 @@ NOTE: if you're sure you want to do this, please open an issue as to why. In the
         builder.ensure(TestHelpers { target: compiler.host });
 
         // ensure that `libproc_macro` is available on the host.
-        builder.ensure(compile::Std::new(compiler, compiler.host));
+        if suite == "mir-opt" {
+            builder.ensure(compile::Std::new_for_mir_opt_tests(compiler, compiler.host));
+        } else {
+            builder.ensure(compile::Std::new(compiler, compiler.host));
+        }
 
         // As well as the target
         if suite != "mir-opt" {
@@ -2088,7 +2086,7 @@ NOTE: if you're sure you want to do this, please open an issue as to why. In the
         }
 
         if builder.config.profiler_enabled(target) {
-            cmd.arg("--profiler-support");
+            cmd.arg("--profiler-runtime");
         }
 
         cmd.env("RUST_TEST_TMPDIR", builder.tempdir());
diff --git a/src/bootstrap/src/core/build_steps/tool.rs b/src/bootstrap/src/core/build_steps/tool.rs
index 64dfe054d9c..a01497c2bb9 100644
--- a/src/bootstrap/src/core/build_steps/tool.rs
+++ b/src/bootstrap/src/core/build_steps/tool.rs
@@ -209,11 +209,28 @@ pub fn prepare_tool_cargo(
     // See https://github.com/rust-lang/rust/issues/116538
     cargo.rustflag("-Zunstable-options");
 
-    // `-Zon-broken-pipe=kill` breaks cargo tests
+    // NOTE: The root cause of needing `-Zon-broken-pipe=kill` in the first place is because `rustc`
+    // and `rustdoc` doesn't gracefully handle I/O errors due to usages of raw std `println!` macros
+    // which panics upon encountering broken pipes. `-Zon-broken-pipe=kill` just papers over that
+    // and stops rustc/rustdoc ICEing on e.g. `rustc --print=sysroot | false`.
+    //
+    // cargo explicitly does not want the `-Zon-broken-pipe=kill` paper because it does actually use
+    // variants of `println!` that handles I/O errors gracefully. It's also a breaking change for a
+    // spawn process not written in Rust, especially if the language default handler is not
+    // `SIG_IGN`. Thankfully cargo tests will break if we do set the flag.
+    //
+    // For the cargo discussion, see
+    // <https://rust-lang.zulipchat.com/#narrow/stream/246057-t-cargo/topic/Applying.20.60-Zon-broken-pipe.3Dkill.60.20flags.20in.20bootstrap.3F>.
+    //
+    // For the rustc discussion, see
+    // <https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/Internal.20lint.20for.20raw.20.60print!.60.20and.20.60println!.60.3F>
+    // for proper solutions.
     if !path.ends_with("cargo") {
-        // If the output is piped to e.g. `head -n1` we want the process to be killed,
-        // rather than having an error bubble up and cause a panic.
-        cargo.rustflag("-Zon-broken-pipe=kill");
+        // Use an untracked env var `FORCE_ON_BROKEN_PIPE_KILL` here instead of `RUSTFLAGS`.
+        // `RUSTFLAGS` is tracked by cargo. Conditionally omitting `-Zon-broken-pipe=kill` from
+        // `RUSTFLAGS` causes unnecessary tool rebuilds due to cache invalidation from building e.g.
+        // cargo *without* `-Zon-broken-pipe=kill` but then rustdoc *with* `-Zon-broken-pipe=kill`.
+        cargo.env("FORCE_ON_BROKEN_PIPE_KILL", "-Zon-broken-pipe=kill");
     }
 
     cargo
@@ -872,8 +889,11 @@ impl Step for LlvmBitcodeLinker {
     fn run(self, builder: &Builder<'_>) -> PathBuf {
         let bin_name = "llvm-bitcode-linker";
 
-        builder.ensure(compile::Std::new(self.compiler, self.compiler.host));
-        builder.ensure(compile::Rustc::new(self.compiler, self.target));
+        // If enabled, use ci-rustc and skip building the in-tree compiler.
+        if !builder.download_rustc() {
+            builder.ensure(compile::Std::new(self.compiler, self.compiler.host));
+            builder.ensure(compile::Rustc::new(self.compiler, self.target));
+        }
 
         let cargo = prepare_tool_cargo(
             builder,
diff --git a/src/bootstrap/src/core/builder.rs b/src/bootstrap/src/core/builder.rs
index c35384ce3c0..9ac0b0a01f7 100644
--- a/src/bootstrap/src/core/builder.rs
+++ b/src/bootstrap/src/core/builder.rs
@@ -72,36 +72,40 @@ impl<'a> Deref for Builder<'a> {
 }
 
 pub trait Step: 'static + Clone + Debug + PartialEq + Eq + Hash {
-    /// `PathBuf` when directories are created or to return a `Compiler` once
-    /// it's been assembled.
+    /// Result type of `Step::run`.
     type Output: Clone;
 
-    /// Whether this step is run by default as part of its respective phase.
-    /// `true` here can still be overwritten by `should_run` calling `default_condition`.
+    /// Whether this step is run by default as part of its respective phase, as defined by the `describe`
+    /// macro in [`Builder::get_step_descriptions`].
+    ///
+    /// Note: Even if set to `true`, it can still be overridden with [`ShouldRun::default_condition`]
+    /// by `Step::should_run`.
     const DEFAULT: bool = false;
 
     /// If true, then this rule should be skipped if --target was specified, but --host was not
     const ONLY_HOSTS: bool = false;
 
-    /// Primary function to execute this rule. Can call `builder.ensure()`
-    /// with other steps to run those.
+    /// Primary function to implement `Step` logic.
+    ///
+    /// This function can be triggered in two ways:
+    ///     1. Directly from [`Builder::execute_cli`].
+    ///     2. Indirectly by being called from other `Step`s using [`Builder::ensure`].
     ///
-    /// This gets called twice during a normal `./x.py` execution: first
-    /// with `dry_run() == true`, and then for real.
+    /// When called with [`Builder::execute_cli`] (as done by `Build::build`), this function executed twice:
+    ///     - First in "dry-run" mode to validate certain things (like cyclic Step invocations,
+    ///         directory creation, etc) super quickly.
+    ///     - Then it's called again to run the actual, very expensive process.
+    ///
+    /// When triggered indirectly from other `Step`s, it may still run twice (as dry-run and real mode)
+    /// depending on the `Step::run` implementation of the caller.
     fn run(self, builder: &Builder<'_>) -> Self::Output;
 
-    /// When bootstrap is passed a set of paths, this controls whether this rule
-    /// will execute. However, it does not get called in a "default" context
-    /// when we are not passed any paths; in that case, `make_run` is called
-    /// directly.
+    /// Determines if this `Step` should be run when given specific paths (e.g., `x build $path`).
     fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_>;
 
-    /// Builds up a "root" rule, either as a default rule or from a path passed
-    /// to us.
-    ///
-    /// When path is `None`, we are executing in a context where no paths were
-    /// passed. When `./x.py build` is run, for example, this rule could get
-    /// called if it is in the correct list below with a path of `None`.
+    /// Called directly by the bootstrap `Step` handler when not triggered indirectly by other `Step`s using [`Builder::ensure`].
+    /// For example, `./x.py test bootstrap` runs this for `test::Bootstrap`. Similarly, `./x.py test` runs it for every step
+    /// that is listed by the `describe` macro in [`Builder::get_step_descriptions`].
     fn make_run(_run: RunConfig<'_>) {
         // It is reasonable to not have an implementation of make_run for rules
         // who do not want to get called from the root context. This means that
@@ -327,7 +331,6 @@ const PATH_REMAP: &[(&str, &[&str])] = &[
         "tests/mir-opt",
         "tests/pretty",
         "tests/run-make",
-        "tests/run-pass-valgrind",
         "tests/rustdoc",
         "tests/rustdoc-gui",
         "tests/rustdoc-js",
@@ -415,6 +418,15 @@ impl StepDescription {
             .map(|desc| (desc.should_run)(ShouldRun::new(builder, desc.kind)))
             .collect::<Vec<_>>();
 
+        if builder.download_rustc() && (builder.kind == Kind::Dist || builder.kind == Kind::Install)
+        {
+            eprintln!(
+                "ERROR: '{}' subcommand is incompatible with `rust.download-rustc`.",
+                builder.kind.as_str()
+            );
+            crate::exit!(1);
+        }
+
         // sanity checks on rules
         for (desc, should_run) in v.iter().zip(&should_runs) {
             assert!(
@@ -852,7 +864,6 @@ impl<'a> Builder<'a> {
                 test::Tidy,
                 test::Ui,
                 test::Crashes,
-                test::RunPassValgrind,
                 test::Coverage,
                 test::CoverageMap,
                 test::CoverageRun,
@@ -1000,7 +1011,9 @@ impl<'a> Builder<'a> {
                 run::GenerateWindowsSys,
                 run::GenerateCompletions,
             ),
-            Kind::Setup => describe!(setup::Profile, setup::Hook, setup::Link, setup::Vscode),
+            Kind::Setup => {
+                describe!(setup::Profile, setup::Hook, setup::Link, setup::Editor)
+            }
             Kind::Clean => describe!(clean::CleanAll, clean::Rustc, clean::Std),
             Kind::Vendor => describe!(vendor::Vendor),
             // special-cased in Build::build()
@@ -1685,10 +1698,24 @@ impl<'a> Builder<'a> {
         match mode {
             Mode::Std | Mode::ToolBootstrap | Mode::ToolStd => {}
             Mode::Rustc | Mode::Codegen | Mode::ToolRustc => {
-                // Build proc macros both for the host and the target
+                // Build proc macros both for the host and the target unless proc-macros are not
+                // supported by the target.
                 if target != compiler.host && cmd_kind != Kind::Check {
-                    cargo.arg("-Zdual-proc-macros");
-                    rustflags.arg("-Zdual-proc-macros");
+                    let error = command(self.rustc(compiler))
+                        .arg("--target")
+                        .arg(target.rustc_target_arg())
+                        .arg("--print=file-names")
+                        .arg("--crate-type=proc-macro")
+                        .arg("-")
+                        .run_capture(self)
+                        .stderr();
+                    let not_supported = error
+                        .lines()
+                        .any(|line| line.contains("unsupported crate type `proc-macro`"));
+                    if !not_supported {
+                        cargo.arg("-Zdual-proc-macros");
+                        rustflags.arg("-Zdual-proc-macros");
+                    }
                 }
             }
         }
diff --git a/src/bootstrap/src/core/builder/tests.rs b/src/bootstrap/src/core/builder/tests.rs
index bd81dc930be..695a66834d4 100644
--- a/src/bootstrap/src/core/builder/tests.rs
+++ b/src/bootstrap/src/core/builder/tests.rs
@@ -212,6 +212,39 @@ fn alias_and_path_for_library() {
     assert_eq!(first(cache.all::<doc::Std>()), &[doc_std!(A => A, stage = 0)]);
 }
 
+#[test]
+fn ci_rustc_if_unchanged_logic() {
+    let config = Config::parse_inner(
+        Flags::parse(&[
+            "build".to_owned(),
+            "--dry-run".to_owned(),
+            "--set=rust.download-rustc='if-unchanged'".to_owned(),
+        ]),
+        |&_| Ok(Default::default()),
+    );
+
+    let build = Build::new(config.clone());
+    let builder = Builder::new(&build);
+
+    if config.out.exists() {
+        fs::remove_dir_all(&config.out).unwrap();
+    }
+
+    builder.run_step_descriptions(&Builder::get_step_descriptions(config.cmd.kind()), &[]);
+
+    // Make sure "if-unchanged" logic doesn't try to use CI rustc while there are changes
+    // in compiler and/or library.
+    if config.download_rustc_commit.is_some() {
+        let has_changes =
+            config.last_modified_commit(&["compiler", "library"], "download-rustc", true).is_none();
+
+        assert!(
+            !has_changes,
+            "CI-rustc can't be used with 'if-unchanged' while there are changes in compiler and/or library."
+        );
+    }
+}
+
 mod defaults {
     use pretty_assertions::assert_eq;
 
diff --git a/src/bootstrap/src/core/config/config.rs b/src/bootstrap/src/core/config/config.rs
index 07460b81412..f768470c4ff 100644
--- a/src/bootstrap/src/core/config/config.rs
+++ b/src/bootstrap/src/core/config/config.rs
@@ -4,7 +4,7 @@
 //! how the build runs.
 
 use std::cell::{Cell, RefCell};
-use std::collections::{HashMap, HashSet};
+use std::collections::{BTreeSet, HashMap, HashSet};
 use std::fmt::{self, Display};
 use std::io::IsTerminal;
 use std::path::{Path, PathBuf, absolute};
@@ -13,6 +13,7 @@ use std::str::FromStr;
 use std::sync::OnceLock;
 use std::{cmp, env, fs};
 
+use build_helper::ci::CiEnv;
 use build_helper::exit;
 use build_helper::git::{GitConfig, get_closest_merge_commit, output_result};
 use serde::{Deserialize, Deserializer};
@@ -22,6 +23,7 @@ use crate::core::build_steps::compile::CODEGEN_BACKEND_PREFIX;
 use crate::core::build_steps::llvm;
 pub use crate::core::config::flags::Subcommand;
 use crate::core::config::flags::{Color, Flags, Warnings};
+use crate::core::download::is_download_ci_available;
 use crate::utils::cache::{INTERNER, Interned};
 use crate::utils::channel::{self, GitInfo};
 use crate::utils::helpers::{self, exe, output, t};
@@ -287,6 +289,7 @@ pub struct Config {
     pub rust_profile_generate: Option<String>,
     pub rust_lto: RustcLto,
     pub rust_validate_mir_opts: Option<u32>,
+    pub rust_std_features: BTreeSet<String>,
     pub llvm_profile_use: Option<String>,
     pub llvm_profile_generate: bool,
     pub llvm_libunwind_default: Option<LlvmLibunwind>,
@@ -1152,6 +1155,7 @@ define_config! {
         download_rustc: Option<StringOrBool> = "download-rustc",
         lto: Option<String> = "lto",
         validate_mir_opts: Option<u32> = "validate-mir-opts",
+        std_features: Option<BTreeSet<String>> = "std-features",
     }
 }
 
@@ -1625,9 +1629,11 @@ impl Config {
             config.mandir = mandir.map(PathBuf::from);
         }
 
+        config.llvm_assertions =
+            toml.llvm.as_ref().map_or(false, |llvm| llvm.assertions.unwrap_or(false));
+
         // Store off these values as options because if they're not provided
         // we'll infer default values for them later
-        let mut llvm_assertions = None;
         let mut llvm_tests = None;
         let mut llvm_enzyme = None;
         let mut llvm_plugins = None;
@@ -1645,6 +1651,7 @@ impl Config {
         let mut optimize = None;
         let mut omit_git_hash = None;
         let mut lld_enabled = None;
+        let mut std_features = None;
 
         let mut is_user_configured_rust_channel = false;
 
@@ -1703,12 +1710,14 @@ impl Config {
                 stack_protector,
                 strip,
                 lld_mode,
+                std_features: std_features_toml,
             } = rust;
 
             is_user_configured_rust_channel = channel.is_some();
             set(&mut config.channel, channel.clone());
 
-            config.download_rustc_commit = config.download_ci_rustc_commit(download_rustc);
+            config.download_rustc_commit =
+                config.download_ci_rustc_commit(download_rustc, config.llvm_assertions);
 
             debug = debug_toml;
             debug_assertions = debug_assertions_toml;
@@ -1722,6 +1731,7 @@ impl Config {
             debuginfo_level_tools = debuginfo_level_tools_toml;
             debuginfo_level_tests = debuginfo_level_tests_toml;
             lld_enabled = lld_enabled_toml;
+            std_features = std_features_toml;
 
             optimize = optimize_toml;
             omit_git_hash = omit_git_hash_toml;
@@ -1843,7 +1853,7 @@ impl Config {
                 optimize: optimize_toml,
                 thin_lto,
                 release_debuginfo,
-                assertions,
+                assertions: _,
                 tests,
                 enzyme,
                 plugins,
@@ -1877,7 +1887,6 @@ impl Config {
                 Some(StringOrBool::Bool(false)) | None => {}
             }
             set(&mut config.ninja_in_file, ninja);
-            llvm_assertions = assertions;
             llvm_tests = tests;
             llvm_enzyme = enzyme;
             llvm_plugins = plugins;
@@ -1906,8 +1915,8 @@ impl Config {
             config.llvm_enable_warnings = enable_warnings.unwrap_or(false);
             config.llvm_build_config = build_config.clone().unwrap_or(Default::default());
 
-            let asserts = llvm_assertions.unwrap_or(false);
-            config.llvm_from_ci = config.parse_download_ci_llvm(download_ci_llvm, asserts);
+            config.llvm_from_ci =
+                config.parse_download_ci_llvm(download_ci_llvm, config.llvm_assertions);
 
             if config.llvm_from_ci {
                 let warn = |option: &str| {
@@ -2075,7 +2084,6 @@ impl Config {
         // Now that we've reached the end of our configuration, infer the
         // default values for all options that we haven't otherwise stored yet.
 
-        config.llvm_assertions = llvm_assertions.unwrap_or(false);
         config.llvm_tests = llvm_tests.unwrap_or(false);
         config.llvm_enzyme = llvm_enzyme.unwrap_or(false);
         config.llvm_plugins = llvm_plugins.unwrap_or(false);
@@ -2118,6 +2126,9 @@ impl Config {
             );
         }
 
+        let default_std_features = BTreeSet::from([String::from("panic-unwind")]);
+        config.rust_std_features = std_features.unwrap_or(default_std_features);
+
         let default = debug == Some(true);
         config.rust_debug_assertions = debug_assertions.unwrap_or(default);
         config.rust_debug_assertions_std =
@@ -2388,6 +2399,20 @@ impl Config {
                 Some(commit) => {
                     self.download_ci_rustc(commit);
 
+                    // CI-rustc can't be used without CI-LLVM. If `self.llvm_from_ci` is false, it means the "if-unchanged"
+                    // logic has detected some changes in the LLVM submodule (download-ci-llvm=false can't happen here as
+                    // we don't allow it while parsing the configuration).
+                    if !self.llvm_from_ci {
+                        // This happens when LLVM submodule is updated in CI, we should disable ci-rustc without an error
+                        // to not break CI. For non-CI environments, we should return an error.
+                        if CiEnv::is_ci() {
+                            println!("WARNING: LLVM submodule has changes, `download-rustc` will be disabled.");
+                            return None;
+                        } else {
+                            panic!("ERROR: LLVM submodule has changes, `download-rustc` can't be used.");
+                        }
+                    }
+
                     if let Some(config_path) = &self.config {
                         let ci_config_toml = match self.get_builder_toml("ci-rustc") {
                             Ok(ci_config_toml) => ci_config_toml,
@@ -2411,8 +2436,9 @@ impl Config {
                             ci_config_toml,
                         );
 
-                        let disable_ci_rustc_if_incompatible =
-                            env::var_os("DISABLE_CI_RUSTC_IF_INCOMPATIBLE")
+                        // Primarily used by CI runners to avoid handling download-rustc incompatible
+                        // options one by one on shell scripts.
+                        let disable_ci_rustc_if_incompatible = env::var_os("DISABLE_CI_RUSTC_IF_INCOMPATIBLE")
                             .is_some_and(|s| s == "1" || s == "true");
 
                         if disable_ci_rustc_if_incompatible && res.is_err() {
@@ -2703,7 +2729,15 @@ impl Config {
     }
 
     /// Returns the commit to download, or `None` if we shouldn't download CI artifacts.
-    fn download_ci_rustc_commit(&self, download_rustc: Option<StringOrBool>) -> Option<String> {
+    fn download_ci_rustc_commit(
+        &self,
+        download_rustc: Option<StringOrBool>,
+        llvm_assertions: bool,
+    ) -> Option<String> {
+        if !is_download_ci_available(&self.build.triple, llvm_assertions) {
+            return None;
+        }
+
         // If `download-rustc` is not set, default to rebuilding.
         let if_unchanged = match download_rustc {
             None | Some(StringOrBool::Bool(false)) => return None,
@@ -2714,9 +2748,18 @@ 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"),
+        ];
+
         // 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(), &[]).unwrap();
+        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?");
@@ -2725,11 +2768,24 @@ impl Config {
             crate::exit!(1);
         }
 
+        if CiEnv::is_ci() && {
+            let head_sha =
+                output(helpers::git(Some(&self.src)).arg("rev-parse").arg("HEAD").as_command_mut());
+            let head_sha = head_sha.trim();
+            commit == head_sha
+        } {
+            eprintln!("CI rustc commit matches with HEAD and we are in CI.");
+            eprintln!(
+                "`rustc.download-ci` functionality will be skipped as artifacts are not available."
+            );
+            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([self.src.join("compiler"), self.src.join("library")])
+            .args(files_to_track)
             .as_command_mut()
             .status())
         .success();
@@ -3016,6 +3072,7 @@ fn check_incompatible_options_for_ci_rustc(
         description,
         incremental,
         default_linker,
+        std_features,
 
         // Rest of the options can simply be ignored.
         debug: _,
@@ -3077,6 +3134,7 @@ fn check_incompatible_options_for_ci_rustc(
     err!(current_rust_config.default_linker, default_linker);
     err!(current_rust_config.stack_protector, stack_protector);
     err!(current_rust_config.lto, lto);
+    err!(current_rust_config.std_features, std_features);
 
     warn!(current_rust_config.channel, channel);
     warn!(current_rust_config.description, description);
diff --git a/src/bootstrap/src/core/config/flags.rs b/src/bootstrap/src/core/config/flags.rs
index 87db5f93fb0..3aefe517a5b 100644
--- a/src/bootstrap/src/core/config/flags.rs
+++ b/src/bootstrap/src/core/config/flags.rs
@@ -447,14 +447,14 @@ Arguments:
     The profile is optional and you will be prompted interactively if it is not given.
     The following profiles are available:
 {}
-    To only set up the git hook, VS Code config or toolchain link, you may use
+    To only set up the git hook, editor config or toolchain link, you may use
         ./x.py setup hook
-        ./x.py setup vscode
+        ./x.py setup editor
         ./x.py setup link", Profile::all_for_help("        ").trim_end()))]
     Setup {
         /// Either the profile for `config.toml` or another setup action.
         /// May be omitted to set up interactively
-        #[arg(value_name = "<PROFILE>|hook|vscode|link")]
+        #[arg(value_name = "<PROFILE>|hook|editor|link")]
         profile: Option<PathBuf>,
     },
     /// Suggest a subset of tests to run, based on modified files
diff --git a/src/bootstrap/src/core/config/tests.rs b/src/bootstrap/src/core/config/tests.rs
index e38d4eac051..2611b6cf51b 100644
--- a/src/bootstrap/src/core/config/tests.rs
+++ b/src/bootstrap/src/core/config/tests.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
 use std::env;
 use std::fs::{File, remove_file};
 use std::io::Write;
@@ -9,9 +10,10 @@ use serde::Deserialize;
 use super::flags::Flags;
 use super::{ChangeIdWrapper, Config};
 use crate::core::build_steps::clippy::get_clippy_rules_in_order;
+use crate::core::build_steps::llvm;
 use crate::core::config::{LldMode, Target, TargetSelection, TomlConfig};
 
-fn parse(config: &str) -> Config {
+pub(crate) fn parse(config: &str) -> Config {
     Config::parse_inner(
         Flags::parse(&["check".to_string(), "--config=/does/not/exist".to_string()]),
         |&_| toml::from_str(&config),
@@ -20,29 +22,32 @@ fn parse(config: &str) -> Config {
 
 #[test]
 fn download_ci_llvm() {
-    if crate::core::build_steps::llvm::is_ci_llvm_modified(&parse("")) {
-        eprintln!("Detected LLVM as non-available: running in CI and modified LLVM in this change");
-        return;
+    let config = parse("");
+    let is_available = llvm::is_ci_llvm_available(&config, config.llvm_assertions);
+    if is_available {
+        assert!(config.llvm_from_ci);
     }
 
-    let parse_llvm = |s| parse(s).llvm_from_ci;
-    let if_unchanged = parse_llvm("llvm.download-ci-llvm = \"if-unchanged\"");
+    let config = parse("llvm.download-ci-llvm = true");
+    let is_available = llvm::is_ci_llvm_available(&config, config.llvm_assertions);
+    if is_available {
+        assert!(config.llvm_from_ci);
+    }
 
-    assert!(parse_llvm("llvm.download-ci-llvm = true"));
-    assert!(!parse_llvm("llvm.download-ci-llvm = false"));
-    assert_eq!(parse_llvm(""), if_unchanged);
-    assert_eq!(parse_llvm("rust.channel = \"dev\""), if_unchanged);
-    assert!(parse_llvm("rust.channel = \"stable\""));
-    assert_eq!(parse_llvm("build.build = \"x86_64-unknown-linux-gnu\""), if_unchanged);
-    assert_eq!(
-        parse_llvm(
-            "llvm.assertions = true \r\n build.build = \"x86_64-unknown-linux-gnu\" \r\n llvm.download-ci-llvm = \"if-unchanged\""
-        ),
-        if_unchanged
-    );
-    assert!(!parse_llvm(
-        "llvm.assertions = true \r\n build.build = \"aarch64-apple-darwin\" \r\n llvm.download-ci-llvm = \"if-unchanged\""
-    ));
+    let config = parse("llvm.download-ci-llvm = false");
+    assert!(!config.llvm_from_ci);
+
+    let if_unchanged_config = parse("llvm.download-ci-llvm = \"if-unchanged\"");
+    if if_unchanged_config.llvm_from_ci {
+        let has_changes = if_unchanged_config
+            .last_modified_commit(&["src/llvm-project"], "download-ci-llvm", true)
+            .is_none();
+
+        assert!(
+            !has_changes,
+            "CI LLVM can't be enabled with 'if-unchanged' while there are changes in LLVM submodule."
+        );
+    }
 }
 
 // FIXME(onur-ozkan): extend scope of the test
@@ -326,3 +331,24 @@ fn verbose_tests_default_value() {
     let config = Config::parse(Flags::parse(&["build".into(), "compiler".into(), "-v".into()]));
     assert_eq!(config.verbose_tests, true);
 }
+
+#[test]
+fn parse_rust_std_features() {
+    let config = parse("rust.std-features = [\"panic-unwind\", \"backtrace\"]");
+    let expected_features: BTreeSet<String> =
+        ["panic-unwind", "backtrace"].into_iter().map(|s| s.to_string()).collect();
+    assert_eq!(config.rust_std_features, expected_features);
+}
+
+#[test]
+fn parse_rust_std_features_empty() {
+    let config = parse("rust.std-features = []");
+    let expected_features: BTreeSet<String> = BTreeSet::new();
+    assert_eq!(config.rust_std_features, expected_features);
+}
+
+#[test]
+#[should_panic]
+fn parse_rust_std_features_invalid() {
+    parse("rust.std-features = \"backtrace\"");
+}
diff --git a/src/bootstrap/src/core/download.rs b/src/bootstrap/src/core/download.rs
index 444b75876f2..db1f5b08338 100644
--- a/src/bootstrap/src/core/download.rs
+++ b/src/bootstrap/src/core/download.rs
@@ -832,3 +832,43 @@ fn path_is_dylib(path: &Path) -> bool {
     // The .so is not necessarily the extension, it might be libLLVM.so.18.1
     path.to_str().map_or(false, |path| path.contains(".so"))
 }
+
+/// Checks whether the CI rustc is available for the given target triple.
+pub(crate) fn is_download_ci_available(target_triple: &str, llvm_assertions: bool) -> bool {
+    // All tier 1 targets and tier 2 targets with host tools.
+    const SUPPORTED_PLATFORMS: &[&str] = &[
+        "aarch64-apple-darwin",
+        "aarch64-pc-windows-msvc",
+        "aarch64-unknown-linux-gnu",
+        "aarch64-unknown-linux-musl",
+        "arm-unknown-linux-gnueabi",
+        "arm-unknown-linux-gnueabihf",
+        "armv7-unknown-linux-gnueabihf",
+        "i686-pc-windows-gnu",
+        "i686-pc-windows-msvc",
+        "i686-unknown-linux-gnu",
+        "loongarch64-unknown-linux-gnu",
+        "powerpc-unknown-linux-gnu",
+        "powerpc64-unknown-linux-gnu",
+        "powerpc64le-unknown-linux-gnu",
+        "riscv64gc-unknown-linux-gnu",
+        "s390x-unknown-linux-gnu",
+        "x86_64-apple-darwin",
+        "x86_64-pc-windows-gnu",
+        "x86_64-pc-windows-msvc",
+        "x86_64-unknown-freebsd",
+        "x86_64-unknown-illumos",
+        "x86_64-unknown-linux-gnu",
+        "x86_64-unknown-linux-musl",
+        "x86_64-unknown-netbsd",
+    ];
+
+    const SUPPORTED_PLATFORMS_WITH_ASSERTIONS: &[&str] =
+        &["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"];
+
+    if llvm_assertions {
+        SUPPORTED_PLATFORMS_WITH_ASSERTIONS.contains(&target_triple)
+    } else {
+        SUPPORTED_PLATFORMS.contains(&target_triple)
+    }
+}
diff --git a/src/bootstrap/src/core/sanity.rs b/src/bootstrap/src/core/sanity.rs
index 888ba8e2a3f..6fbdd76ed5b 100644
--- a/src/bootstrap/src/core/sanity.rs
+++ b/src/bootstrap/src/core/sanity.rs
@@ -13,8 +13,6 @@ use std::ffi::{OsStr, OsString};
 use std::path::PathBuf;
 use std::{env, fs};
 
-use build_helper::git::warn_old_master_branch;
-
 use crate::Build;
 #[cfg(not(feature = "bootstrap-self-test"))]
 use crate::builder::Builder;
@@ -37,6 +35,9 @@ pub struct Finder {
 const STAGE0_MISSING_TARGETS: &[&str] = &[
     // just a dummy comment so the list doesn't get onelined
     "armv7-rtems-eabihf",
+    "riscv32e-unknown-none-elf",
+    "riscv32em-unknown-none-elf",
+    "riscv32emc-unknown-none-elf",
 ];
 
 /// Minimum version threshold for libstdc++ required when using prebuilt LLVM
@@ -379,6 +380,4 @@ $ pacman -R cmake && pacman -S mingw-w64-x86_64-cmake
     if let Some(ref s) = build.config.ccache {
         cmd_finder.must_have(s);
     }
-
-    warn_old_master_branch(&build.config.git_config(), &build.config.src);
 }
diff --git a/src/bootstrap/src/lib.rs b/src/bootstrap/src/lib.rs
index 75659f46431..3924a6d714e 100644
--- a/src/bootstrap/src/lib.rs
+++ b/src/bootstrap/src/lib.rs
@@ -17,7 +17,7 @@
 //! also check out the `src/bootstrap/README.md` file for more information.
 
 use std::cell::{Cell, RefCell};
-use std::collections::{HashMap, HashSet};
+use std::collections::{BTreeSet, HashMap, HashSet};
 use std::fmt::Display;
 use std::fs::{self, File};
 use std::path::{Path, PathBuf};
@@ -470,7 +470,7 @@ impl Build {
             crate::core::metadata::build(&mut build);
         }
 
-        // Make a symbolic link so we can use a consistent directory in the documentation.
+        // Create symbolic link to use host sysroot from a consistent path (e.g., in the rust-analyzer config file).
         let build_triple = build.out.join(build.build);
         t!(fs::create_dir_all(&build_triple));
         let host = build.out.join("host");
@@ -645,28 +645,31 @@ impl Build {
         &self.config.rust_info
     }
 
-    /// Gets the space-separated set of activated features for the standard
-    /// library.
+    /// Gets the space-separated set of activated features for the standard library.
+    /// This can be configured with the `std-features` key in config.toml.
     fn std_features(&self, target: TargetSelection) -> String {
-        let mut features = " panic-unwind".to_string();
+        let mut features: BTreeSet<&str> =
+            self.config.rust_std_features.iter().map(|s| s.as_str()).collect();
 
         match self.config.llvm_libunwind(target) {
-            LlvmLibunwind::InTree => features.push_str(" llvm-libunwind"),
-            LlvmLibunwind::System => features.push_str(" system-llvm-libunwind"),
-            LlvmLibunwind::No => {}
-        }
+            LlvmLibunwind::InTree => features.insert("llvm-libunwind"),
+            LlvmLibunwind::System => features.insert("system-llvm-libunwind"),
+            LlvmLibunwind::No => false,
+        };
+
         if self.config.backtrace {
-            features.push_str(" backtrace");
+            features.insert("backtrace");
         }
         if self.config.profiler_enabled(target) {
-            features.push_str(" profiler");
+            features.insert("profiler");
         }
         // Generate memcpy, etc.  FIXME: Remove this once compiler-builtins
         // automatically detects this target.
         if target.contains("zkvm") {
-            features.push_str(" compiler-builtins-mem");
+            features.insert("compiler-builtins-mem");
         }
-        features
+
+        features.into_iter().collect::<Vec<_>>().join(" ")
     }
 
     /// Gets the space-separated set of activated features for the compiler.
@@ -1572,9 +1575,11 @@ Executed at: {executed_at}"#,
     fn rust_version(&self) -> String {
         let mut version = self.rust_info().version(self, &self.version);
         if let Some(ref s) = self.config.description {
-            version.push_str(" (");
-            version.push_str(s);
-            version.push(')');
+            if !s.is_empty() {
+                version.push_str(" (");
+                version.push_str(s);
+                version.push(')');
+            }
         }
         version
     }
diff --git a/src/bootstrap/src/utils/change_tracker.rs b/src/bootstrap/src/utils/change_tracker.rs
index e6f7f105fa2..b37786496cb 100644
--- a/src/bootstrap/src/utils/change_tracker.rs
+++ b/src/bootstrap/src/utils/change_tracker.rs
@@ -270,4 +270,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
         severity: ChangeSeverity::Info,
         summary: "If `llvm.download-ci-llvm` is not defined, it defaults to `true`.",
     },
+    ChangeInfo {
+        change_id: 131075,
+        severity: ChangeSeverity::Info,
+        summary: "New option `./x setup editor` added, replacing `./x setup vscode` and adding support for vim, emacs and helix.",
+    },
 ];
diff --git a/src/bootstrap/src/utils/shared_helpers.rs b/src/bootstrap/src/utils/shared_helpers.rs
index 7150c84313c..6d3c276cc05 100644
--- a/src/bootstrap/src/utils/shared_helpers.rs
+++ b/src/bootstrap/src/utils/shared_helpers.rs
@@ -49,6 +49,8 @@ pub fn exe(name: &str, target: &str) -> String {
         format!("{name}.exe")
     } else if target.contains("uefi") {
         format!("{name}.efi")
+    } else if target.contains("wasm") {
+        format!("{name}.wasm")
     } else {
         name.to_string()
     }