From 33049f24242e6335764049e5501d301c0e02ca71 Mon Sep 17 00:00:00 2001 From: INTFRAME Date: Sun, 20 Sep 2026 02:38:10 +0700 Subject: [PATCH] cli: replay hotpatch links with the env rustc gave the linker (fixes #4911) rustc hands its linker child env vars the rustc process itself never has: `LIB`/`INCLUDE`/`PATH` from MSVC tool discovery on windows, the sysroot tool dirs on `PATH`, `LC_ALL`/`VSLANG`. dx replayed fat/thin links with the env captured by the rustc wrapper, so outside a VS developer prompt `LIB` was missing and rust-lld could not open `kernel32.lib` and friends. Capture `std::env::vars()` in the linker interception next to the link args (`link_env.json`, `DX_LINK_ENV_FILE`) and prefer it when replaying. The linux-only `PATH` patch stays as the fallback for caches without a capture. --- packages/cli/src/build/link.rs | 98 ++++++++++++++++++++++++++----- packages/cli/src/build/request.rs | 10 ++++ packages/cli/src/cli/link.rs | 43 +++++++++++++- packages/cli/src/rustcwrapper.rs | 4 ++ 4 files changed, 140 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/build/link.rs b/packages/cli/src/build/link.rs index 8107fe62e0..95019b02f8 100644 --- a/packages/cli/src/build/link.rs +++ b/packages/cli/src/build/link.rs @@ -274,13 +274,8 @@ impl BuildRequest { out_args = vec![format!("@{}", self.windows_command_file().display()).into()]; } - // Add more search paths for the linker - let mut command_envs = args.envs.clone(); - - // On linux, we need to set a more complete PATH for the linker to find its libraries - if cfg!(target_os = "linux") { - command_envs.push(("PATH".to_string(), std::env::var("PATH").unwrap())); - } + // Replay with the env rustc gave the linker, falling back to the rustc env + let command_envs = link_replay_envs(&artifacts.workspace_rustc, &args); // Run the linker directly! // @@ -1150,13 +1145,8 @@ impl BuildRequest { out_args = vec![format!("@{}", self.windows_command_file().display())]; } - // Add more search paths for the linker - let mut command_envs = rustc_args.envs.clone(); - - // On linux, we need to set a more complete PATH for the linker to find its libraries - if cfg!(target_os = "linux") { - command_envs.push(("PATH".to_string(), std::env::var("PATH").unwrap())); - } + // Replay with the env rustc gave the linker, falling back to the rustc env + let command_envs = link_replay_envs(set, rustc_args); // Run the linker directly! let res = Command::new(linker) @@ -1534,3 +1524,83 @@ fn dep_info_path_for_rustc_args(args: &[String]) -> Option { let crate_name = crate_name?; Some(PathBuf::from(out_dir).join(format!("{crate_name}{extra}.d"))) } + +/// The env to replay the tip crate's link with. +/// +/// rustc gives its linker child vars that the rustc process itself never sees: `LIB`/`INCLUDE`/`PATH` +/// from MSVC tool discovery on windows, the sysroot tool dirs on `PATH`, `LC_ALL`/`VSLANG`. Prefer +/// that captured env. Fall back to the rustc env (plus our own `PATH` on linux) for caches written +/// before the link env was recorded. +fn link_replay_envs(set: &WorkspaceRustcArgs, rustc_args: &RustcArgs) -> Vec<(String, String)> { + if !set.link_envs.is_empty() { + return set.link_envs.clone(); + } + + let mut command_envs = rustc_args.envs.clone(); + + // On linux, we need to set a more complete PATH for the linker to find its libraries + if cfg!(target_os = "linux") { + command_envs.push(("PATH".to_string(), std::env::var("PATH").unwrap())); + } + + command_envs +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rustc_args(envs: &[(&str, &str)]) -> RustcArgs { + RustcArgs { + args: vec![], + envs: envs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + cwd: PathBuf::new(), + } + } + + #[test] + fn link_replay_prefers_env_captured_from_the_linker_child() { + // Outside a VS developer prompt, `LIB` only ever exists in the env rustc gives its linker. + let rustc = rustc_args(&[("PATH", "C:\\rust\\bin")]); + let mut set = WorkspaceRustcArgs::new(vec!["kernel32.lib".into()]); + set.link_envs = vec![ + ("PATH".into(), "C:\\msvc\\bin;C:\\rust\\bin".into()), + ("LIB".into(), "C:\\msvc\\lib;C:\\sdk\\lib".into()), + ]; + + let envs = link_replay_envs(&set, &rustc); + + assert_eq!(envs, set.link_envs); + assert!(envs.iter().any(|(k, v)| k == "LIB" && v.contains("sdk"))); + } + + #[test] + fn link_replay_falls_back_to_rustc_env_without_a_capture() { + let rustc = rustc_args(&[("CARGO", "cargo"), ("PATH", "/usr/bin")]); + let set = WorkspaceRustcArgs::new(vec![]); + + let envs = link_replay_envs(&set, &rustc); + + assert_eq!(&envs[..2], &rustc.envs[..]); + assert!(!envs.iter().any(|(k, _)| k == "LIB")); + } + + #[test] + fn link_envs_round_trip_and_empty_file_means_no_capture() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("link_env.json"); + + std::fs::write(&path, "").unwrap(); + assert!(crate::read_link_envs(&path).is_empty()); + + let envs = vec![ + ("LIB".to_string(), "C:\\sdk\\lib".to_string()), + ("VSLANG".to_string(), "1033".to_string()), + ]; + crate::write_link_envs(&path, envs.clone()).unwrap(); + assert_eq!(crate::read_link_envs(&path), envs); + } +} diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 97987531f9..38688386d7 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -934,6 +934,7 @@ impl BuildRequest { _ = std::fs::create_dir_all(self.rustc_wrapper_args_scope_dir(&ctx.mode)?); _ = std::fs::File::create(self.link_err_file()); _ = std::fs::File::create(self.link_args_file()); + _ = std::fs::File::create(self.link_env_file()); _ = std::fs::File::create(self.windows_command_file()); if !matches!(ctx.mode, BuildMode::Thin { .. }) { @@ -1955,6 +1956,7 @@ impl BuildRequest { linker: self.custom_linker.clone(), link_err_file: dunce::canonicalize(self.link_err_file())?, link_args_file: dunce::canonicalize(self.link_args_file())?, + link_env_file: dunce::canonicalize(self.link_env_file())?, } .write_env_vars(&mut env_vars)?; } @@ -2445,6 +2447,7 @@ impl BuildRequest { .collect(); let mut workspace_rustc_args = WorkspaceRustcArgs::new(link_args); + workspace_rustc_args.link_envs = crate::read_link_envs(&self.link_env_file()); // Always read from the fat build's scope dir — the rustc wrapper only captures // args during fat/base builds, not thin builds. @@ -2666,6 +2669,13 @@ impl BuildRequest { self.session_cache_dir().join("link_args.json") } + /// The env captured from the tip crate's final link invocation. rustc adds vars here that the + /// rustc process itself never had (MSVC `LIB`/`INCLUDE`/`PATH`, sysroot tool dirs), so we + /// replay the link with this env rather than the rustc env. + fn link_env_file(&self) -> PathBuf { + self.session_cache_dir().join("link_env.json") + } + /// A response file for MSVC's `link.exe`. Windows command lines have a ~32k character /// limit, so we write linker arguments to this file and pass `@` instead. pub(crate) fn windows_command_file(&self) -> PathBuf { diff --git a/packages/cli/src/cli/link.rs b/packages/cli/src/cli/link.rs index ea7ff78ef3..f75c7ed265 100644 --- a/packages/cli/src/cli/link.rs +++ b/packages/cli/src/cli/link.rs @@ -1,7 +1,12 @@ use crate::Result; use anyhow::{Context, bail}; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, ffi::OsString, path::PathBuf, process::ExitCode}; +use std::{ + borrow::Cow, + ffi::OsString, + path::{Path, PathBuf}, + process::ExitCode, +}; use target_lexicon::Triple; /// `dx` can act as a linker in a few scenarios. Note that we don't *actually* implement the linker logic, @@ -33,6 +38,7 @@ pub struct LinkAction { pub triple: Triple, pub link_args_file: PathBuf, pub link_err_file: PathBuf, + pub link_env_file: PathBuf, } /// The linker flavor to use. This influences the argument style that gets passed to the linker. @@ -52,6 +58,7 @@ impl LinkAction { const DX_LINK_ARG: &str = "DX_LINK"; const DX_ARGS_FILE: &str = "DX_LINK_ARGS_FILE"; const DX_ERR_FILE: &str = "DX_LINK_ERR_FILE"; + const DX_ENV_FILE: &str = "DX_LINK_ENV_FILE"; const DX_LINK_TRIPLE: &str = "DX_LINK_TRIPLE"; const DX_LINK_CUSTOM_LINKER: &str = "DX_LINK_CUSTOM_LINKER"; @@ -73,6 +80,9 @@ impl LinkAction { link_err_file: std::env::var(Self::DX_ERR_FILE) .expect("Linker error file not set") .into(), + link_env_file: std::env::var(Self::DX_ENV_FILE) + .expect("Linker env file not set") + .into(), triple: std::env::var(Self::DX_LINK_TRIPLE) .expect("Linker triple not set") .parse() @@ -95,6 +105,10 @@ impl LinkAction { Self::DX_ERR_FILE.into(), dunce::canonicalize(&self.link_err_file)?.into_os_string(), )); + env_vars.push(( + Self::DX_ENV_FILE.into(), + dunce::canonicalize(&self.link_env_file)?.into_os_string(), + )); env_vars.push((Self::DX_LINK_TRIPLE.into(), self.triple.to_string().into())); if let Some(linker) = &self.linker { env_vars.push(( @@ -144,6 +158,12 @@ impl LinkAction { // todo: we might need to encode these as escaped shell words in case newlines are passed std::fs::write(&self.link_args_file, args.join("\n"))?; + // Also write the env rustc gave us. rustc only hands some vars to its linker child, not to + // the rustc process the wrapper captured: `LIB`/`INCLUDE`/`PATH` from MSVC tool discovery + // on windows, the sysroot tool dirs on `PATH`, `LC_ALL`/`VSLANG`. Without `LIB` the fat/thin + // link replay can't resolve `kernel32.lib` and friends outside of a VS developer prompt. + write_link_envs(&self.link_env_file, std::env::vars())?; + // If there's a linker specified, we use that. Otherwise, we write a dummy object file to satisfy // any post-processing steps that rustc does. match self.linker { @@ -242,6 +262,27 @@ impl LinkAction { } } +/// Persist the env of a linker invocation so the driving `dx` process can replay the link with it. +pub(crate) fn write_link_envs( + path: &Path, + envs: impl IntoIterator, +) -> Result<()> { + let envs: Vec<(String, String)> = envs.into_iter().collect(); + std::fs::write(path, serde_json::to_string(&envs)?)?; + Ok(()) +} + +/// Read back the env written by [`write_link_envs`]. An empty or missing file means the link step +/// never ran through our interception, so there is nothing to replay with. +pub(crate) fn read_link_envs(path: &Path) -> Vec<(String, String)> { + match std::fs::read_to_string(path) { + Ok(contents) if !contents.trim().is_empty() => { + serde_json::from_str(&contents).unwrap_or_default() + } + _ => vec![], + } +} + pub fn get_actual_linker_args_excluding_program_name(args: Vec) -> Vec { args.into_iter() .skip(1) // the first arg is program name diff --git a/packages/cli/src/rustcwrapper.rs b/packages/cli/src/rustcwrapper.rs index 5abd08f9de..ff79794807 100644 --- a/packages/cli/src/rustcwrapper.rs +++ b/packages/cli/src/rustcwrapper.rs @@ -11,6 +11,9 @@ use std::{ #[derive(Clone, Debug, PartialEq)] pub struct WorkspaceRustcArgs { pub link_args: Vec, + /// The env rustc gave the tip crate's linker invocation. Empty if the link step was not + /// intercepted, in which case replays fall back to the tip crate's rustc env. + pub link_envs: Vec<(String, String)>, pub rustc_args: HashMap, } @@ -18,6 +21,7 @@ impl WorkspaceRustcArgs { pub fn new(link_args: Vec) -> Self { Self { link_args, + link_envs: Default::default(), rustc_args: Default::default(), } }