Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 84 additions & 14 deletions packages/cli/src/build/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!
//
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1534,3 +1524,83 @@ fn dep_info_path_for_rustc_args(args: &[String]) -> Option<PathBuf> {
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);
}
}
10 changes: 10 additions & 0 deletions packages/cli/src/build/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. }) {
Expand Down Expand Up @@ -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)?;
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 `@<path>` instead.
pub(crate) fn windows_command_file(&self) -> PathBuf {
Expand Down
43 changes: 42 additions & 1 deletion packages/cli/src/cli/link.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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";

Expand All @@ -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()
Expand All @@ -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((
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Item = (String, String)>,
) -> 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<String>) -> Vec<String> {
args.into_iter()
.skip(1) // the first arg is program name
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/rustcwrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ use std::{
#[derive(Clone, Debug, PartialEq)]
pub struct WorkspaceRustcArgs {
pub link_args: Vec<String>,
/// 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<String, RustcArgs>,
}

impl WorkspaceRustcArgs {
pub fn new(link_args: Vec<String>) -> Self {
Self {
link_args,
link_envs: Default::default(),
rustc_args: Default::default(),
}
}
Expand Down