workspace hot-patching, dynamic tls fixups (#5291)

* Refactor cross-tls-crate-dylib configuration and update test output

- Commented out the crate-type configuration in Cargo.toml for cross-tls-crate-dylib.
- Modified the initial value of the thread-local variable FOO from 2.0 to 1.0 in the main.rs test file.
- Updated the output message in main.rs to be more descriptive.
- Added a new field `tls_fixups` to the JumpTable struct in subsecond-types, which will facilitate future runtime TLS sharing.

* ws hotpatch!

* remove old file

* undo core change

* svh

* Enhance rlib path resolution by incorporating -C extra-filename handling

* remove obj diffing

* small cleanups

* obj cache as files

* simplify

* simplify a bit

* dont bother round tripping cache

* extract to helper

* cleanup `build` function

* dont round-trip modified_crates

* make sure to wipe between full

* bit of a cleanup

* use explicit naming

* compact extra method

* fix temp cleanup

* fix warnign

* cleanup

* fmt, clippy
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2026-02-10 22:37:16 -08:00
1 parent 9b0d380765
commit 33159f366a
12 files changed
+1229 -348

No files matched your search

+471 -59
View File
@@ -321,8 +321,8 @@
use crate::{
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
LinkAction, LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc,
WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
};
use anyhow::{bail, Context};
use cargo_metadata::diagnostic::Diagnostic;
@@ -339,7 +339,7 @@ use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, ffi::OsString};
use std::{
collections::{BTreeMap, HashSet},
collections::{BTreeMap, HashMap, HashSet},
io::Write,
path::{Path, PathBuf},
process::Stdio,
@@ -419,6 +419,7 @@ pub(crate) struct BuildRequest {
/// - Thin: A "thin" build that dynamically links against the dependencies produced by the "fat" build.
/// This is generated by calling rustc *directly* and might be more fragile to construct, but
/// generates *much* faster than a regular base or fat build.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq)]
pub enum BuildMode {
/// A normal build generated using `cargo rustc`
@@ -433,9 +434,26 @@ pub enum BuildMode {
/// A "thin" build generated with `rustc` directly and dx as a custom linker
Thin {
rustc_args: RustcArgs,
/// List of changed files causing this rebuild. Mostly used for diagnostics
changed_files: Vec<PathBuf>,
/// Which workspace crates had source file changes in this edit.
changed_crates: Vec<String>,
/// The ASLR slide of the running program, used to hardcode symbol jumps
aslr_reference: u64,
/// The captured RustcArgs for every crate in the workspace, collected by RUSTC_WORKSPACE_WRAPPER
/// This is used for replaying rustc invocations for workspace hotpatching
workspace_rustc_args: HashMap<String, RustcArgs>,
/// Cumulative set of all workspace crates modified since the fat build.
modified_crates: HashSet<String>,
/// Cache of compiled objects from previous thin builds, used by future re-linking
object_cache: ObjectCache,
/// Cache of initial binary parsing which speeds up stub creation
cache: Arc<HotpatchModuleCache>,
},
}
@@ -450,7 +468,7 @@ pub enum BuildMode {
pub struct BuildArtifacts {
pub(crate) root_dir: PathBuf,
pub(crate) exe: PathBuf,
pub(crate) direct_rustc: RustcArgs,
pub(crate) workspace_rustc_args: HashMap<String, RustcArgs>,
pub(crate) time_start: SystemTime,
pub(crate) time_end: SystemTime,
pub(crate) assets: AssetManifest,
@@ -458,6 +476,7 @@ pub struct BuildArtifacts {
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
pub(crate) depinfo: RustcDepInfo,
pub(crate) build_id: BuildId,
pub(crate) object_cache: ObjectCache,
}
impl BuildRequest {
@@ -1018,7 +1037,7 @@ impl BuildRequest {
// Create the session cache directory
let cache_dir = self.session_cache_dir();
_ = std::fs::create_dir_all(&cache_dir);
_ = std::fs::File::create_new(self.rustc_wrapper_args_file());
_ = std::fs::create_dir_all(self.rustc_wrapper_args_dir());
_ = std::fs::File::create_new(self.link_err_file());
_ = std::fs::File::create_new(self.link_args_file());
_ = std::fs::File::create_new(self.windows_command_file());
@@ -1054,7 +1073,8 @@ impl BuildRequest {
// We need to make sure to not react to this though, so the filemap must cache it
_ = self.bust_fingerprint(ctx);
// Run the cargo build to produce our artifacts
// Run the cargo build to produce our artifacts.
// For thin builds this also pre-compiles workspace dep crates before the tip.
let mut artifacts = self.cargo_build(ctx).await?;
// Write the build artifacts to the bundle on the disk
@@ -1062,10 +1082,10 @@ impl BuildRequest {
BuildMode::Thin {
aslr_reference,
cache,
rustc_args,
modified_crates,
..
} => {
self.write_patch(ctx, *aslr_reference, &mut artifacts, cache, rustc_args)
self.write_patch(ctx, *aslr_reference, &mut artifacts, cache, modified_crates)
.await?;
}
@@ -1076,7 +1096,7 @@ impl BuildRequest {
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
.await
.context("Failed to write executable")?;
self.write_frameworks(ctx, &artifacts.direct_rustc)
self.write_frameworks(ctx, &artifacts)
.await
.context("Failed to write frameworks")?;
self.write_assets(ctx, &artifacts.assets)
@@ -1125,7 +1145,6 @@ impl BuildRequest {
Ok(artifacts)
}
/// Run the cargo build by assembling the build command and executing it.
///
/// This method needs to be very careful with processing output since errors being swallowed will
@@ -1133,6 +1152,10 @@ impl BuildRequest {
async fn cargo_build(&self, ctx: &BuildContext) -> Result<BuildArtifacts> {
let time_start = SystemTime::now();
// For thin builds, compile workspace dep crates before the tip.
// This updates dep rlibs on disk so cargo links the tip against fresh code.
let object_cache = self.compile_workspace_deps(ctx).await?;
// Extract the unit count of the crate graph so build_cargo has more accurate data
// "Thin" builds only build the final exe, so we only need to build one crate
let crate_count = match ctx.mode {
@@ -1246,14 +1269,33 @@ impl BuildRequest {
}
}
// Accumulate the rustc args from the wrapper, if they exist and can be parsed.
let mut direct_rustc = RustcArgs::default();
if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file()) {
if let Ok(res) = serde_json::from_str(&res) {
direct_rustc = res;
// Load per-crate rustc args from the wrapper directory.
// Each workspace crate compiled through the wrapper has its own JSON file:
// "{crate_name}.lib.json" (key: "{crate_name}.lib") for lib targets and
// "{crate_name}.bin.json" (key: "{crate_name}.bin") for bin targets.
let mut workspace_rustc_args = HashMap::new();
let args_dir = self.rustc_wrapper_args_dir();
if let Ok(entries) = std::fs::read_dir(&args_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "json") {
if let Ok(contents) = std::fs::read_to_string(&path) {
if let Ok(args) = serde_json::from_str::<RustcArgs>(&contents) {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
workspace_rustc_args.insert(stem.to_string(), args);
}
}
}
}
}
}
tracing::trace!(
"Loaded workspace rustc args from {}: keys={:?}",
args_dir.display(),
workspace_rustc_args.keys().collect::<Vec<_>>(),
);
// If there's any warnings from the linker, we should print them out
if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file()) {
if !linker_warnings.is_empty() {
@@ -1265,12 +1307,16 @@ impl BuildRequest {
}
}
// Collect the linker args from the and update the rustc args
direct_rustc.link_args = std::fs::read_to_string(self.link_args_file())
.context("Failed to read link args from file")?
.lines()
.map(|s| s.to_string())
.collect::<Vec<_>>();
// Collect the linker args and attach them to the tip crate's bin entry
let tip_crate_name = self.tip_crate_name();
let tip_bin_key = format!("{tip_crate_name}.bin");
if let Some(tip_args) = workspace_rustc_args.get_mut(&tip_bin_key) {
tip_args.link_args = std::fs::read_to_string(self.link_args_file())
.context("Failed to read link args from file")?
.lines()
.map(|s| s.to_string())
.collect();
}
let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
@@ -1278,7 +1324,14 @@ impl BuildRequest {
if matches!(ctx.mode, BuildMode::Fat) {
ctx.status_starting_link();
let link_start = SystemTime::now();
self.run_fat_link(&exe, &direct_rustc).await?;
self.run_fat_link(
&exe,
&workspace_rustc_args
.get(&tip_bin_key)
.cloned()
.unwrap_or_default(),
)
.await?;
tracing::debug!(
"Fat linking completed in {}us",
SystemTime::now()
@@ -1302,7 +1355,7 @@ impl BuildRequest {
Ok(BuildArtifacts {
time_end,
exe,
direct_rustc,
workspace_rustc_args,
time_start,
assets,
mode,
@@ -1310,9 +1363,122 @@ impl BuildRequest {
root_dir: self.root_dir(),
patch_cache: None,
build_id: ctx.build_id,
object_cache,
})
}
/// For thin builds, compile workspace dep crates BEFORE the tip crate.
///
/// This updates dep rlibs on disk so cargo links the tip against fresh code. Handles cascade
/// (recompiling workspace dependents for SVH consistency) and lib+bin tip targets.
///
/// Returns the updated `ObjectCache` — defaulting to empty for non-thin builds.
async fn compile_workspace_deps(&self, ctx: &BuildContext) -> Result<ObjectCache> {
let BuildMode::Thin {
workspace_rustc_args,
changed_crates,
object_cache,
..
} = &ctx.mode
else {
return Ok(ObjectCache::new(&self.session_cache_dir()));
};
let tip_name = self.tip_crate_name();
let mut object_cache = object_cache.clone();
// Compile workspace dep crates with cascade. Start with the explicitly changed dep
// crates (already in leaf-first order from handle_file_change). As we compile each,
// add the crate's workspace dependents so their rlibs have consistent SVH references.
let mut crates_to_compile: Vec<String> = changed_crates
.iter()
.filter(|c| *c != &tip_name)
.cloned()
.collect();
let mut compiled = HashSet::new();
let mut idx = 0;
while idx < crates_to_compile.len() {
let crate_name = crates_to_compile[idx].clone();
idx += 1;
if !compiled.insert(crate_name.clone()) || crate_name == tip_name {
continue;
}
let Some(rustc_args) = workspace_rustc_args.get(&format!("{crate_name}.lib")) else {
tracing::warn!("No captured rustc args for workspace crate {crate_name}, skipping");
continue;
};
tracing::debug!("Compiling workspace dep crate: {crate_name}");
self.compile_dep_crate(&crate_name, rustc_args)
.await
.with_context(|| format!("Failed to compile workspace dep crate '{crate_name}'"))?;
if let Some(rlib_path) = self.find_rlib_for_crate(&crate_name, rustc_args) {
if let Err(e) = object_cache.cache_from_rlib(&crate_name, &rlib_path) {
tracing::warn!("Failed to cache objects from rlib for {crate_name}: {e}");
}
}
for dependent in self.workspace_dependents_of(&crate_name) {
if dependent != tip_name && !compiled.contains(&dependent) {
tracing::debug!(
"Cascade: recompiling {dependent} (depends on recompiled {crate_name})"
);
crates_to_compile.push(dependent);
}
}
}
// If the tip crate has a lib target (src/lib.rs + src/main.rs), compile it
// before the bin target so the bin links against the fresh lib rlib.
let lib_key = format!("{tip_name}.lib");
if let Some(lib_args) = workspace_rustc_args.get(&lib_key) {
let rlib_pre = self.find_rlib_for_crate(&tip_name, lib_args);
let pre_modified = rlib_pre
.as_ref()
.and_then(|p| std::fs::metadata(p).ok())
.and_then(|m| m.modified().ok());
tracing::info!("Compiling tip lib target: {lib_key}");
if let Err(e) = self.compile_dep_crate(&tip_name, lib_args).await {
tracing::warn!("Failed to compile tip lib target: {e}");
} else if let Some(rlib_path) = self.find_rlib_for_crate(&tip_name, lib_args) {
let post_modified = std::fs::metadata(&rlib_path)
.ok()
.and_then(|m| m.modified().ok());
let rlib_changed = match (pre_modified, post_modified) {
(Some(pre), Some(post)) => post > pre,
_ => true,
};
tracing::info!(
"Found lib rlib at: {} (modified={})",
rlib_path.display(),
rlib_changed,
);
match object_cache.cache_from_rlib(&lib_key, &rlib_path) {
Ok(()) => {
let count = object_cache.get(&lib_key).map(|v| v.len()).unwrap_or(0);
tracing::info!("Cached {count} objects from tip lib rlib");
}
Err(e) => tracing::warn!("Failed to cache tip lib objects: {e}"),
}
} else {
tracing::warn!("Could not find rlib for tip lib target {tip_name}");
}
} else {
tracing::debug!(
"No lib target for tip crate (key '{lib_key}' not in workspace_rustc_args, keys={:?})",
workspace_rustc_args.keys().collect::<Vec<_>>()
);
}
Ok(object_cache)
}
/// Collect the assets from the final executable and modify the binary in place to point to the right
/// hashed asset location.
async fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
@@ -1421,9 +1587,20 @@ impl BuildRequest {
Ok(())
}
async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
async fn write_frameworks(
&self,
_ctx: &BuildContext,
artifacts: &BuildArtifacts,
) -> Result<()> {
let framework_dir = self.frameworks_folder();
// We use the rustc for the tip crate `main.rs` because that's where the linking happens
let direct_rustc = artifacts
.workspace_rustc_args
.get(&format!("{}.bin", self.tip_crate_name()))
.cloned()
.unwrap_or_default();
// We have some prebuilt stuff that needs to be copied into the framework dir
let openssl_dir = AndroidTools::openssl_lib_dir(&self.triple);
let openssl_dir_disp = openssl_dir.display().to_string();
@@ -1633,17 +1810,66 @@ impl BuildRequest {
/// manually do any linking.
///
/// We also run some post processing steps here, like extracting out any new assets.
///
/// `extra_objects` contains additional object file paths from compiled workspace dep crates
/// that should be included in the patch dylib. These are combined with the tip crate's
/// `.rcgu.o` files extracted from linker args, creating a self-contained patch.
async fn write_patch(
&self,
ctx: &BuildContext,
aslr_reference: u64,
artifacts: &mut BuildArtifacts,
cache: &Arc<HotpatchModuleCache>,
rustc_args: &RustcArgs,
modified_crates: &HashSet<String>,
) -> Result<()> {
ctx.status_hotpatching();
let args = artifacts.direct_rustc.link_args.clone();
let tip_name = self.tip_crate_name();
// Cache tip crate objects from the FRESH linker args (from the just-completed
// thin build, not the stale ones from ctx.mode's fat build).
let tip_bin_key = format!("{tip_name}.bin");
let args = artifacts
.workspace_rustc_args
.get(&tip_bin_key)
.cloned()
.with_context(|| {
format!(
"Missing rustc args for tip bin target '{tip_bin_key}' \
(available keys: {:?})",
artifacts.workspace_rustc_args.keys().collect::<Vec<_>>()
)
})?;
// Collect objs from tip and re-cache them in the obj cache map
let tip_object_paths: Vec<PathBuf> = args
.link_args
.iter()
.filter(|arg| arg.ends_with(".rcgu.o"))
.map(PathBuf::from)
.collect();
if !tip_object_paths.is_empty() {
artifacts
.object_cache
.cache_from_paths(&tip_name, &tip_object_paths)
.context("Failed to cache objs during patch")?;
}
// Collect cached object paths from all modified dep crates.
// Objects are already on disk in the object cache directory.
// These must NOT be deleted after linking — they persist across patches.
let mut cached_objects: Vec<PathBuf> = Vec::new();
for dep_name in modified_crates.iter().filter(|c| *c != &tip_name) {
if let Some(paths) = artifacts.object_cache.get(dep_name) {
cached_objects.extend(paths.iter().cloned());
}
}
// If the tip has a lib target (lib+bin crate), include its cached objects too.
let lib_key = format!("{tip_name}.lib");
if let Some(paths) = artifacts.object_cache.get(&lib_key) {
cached_objects.extend(paths.iter().cloned());
}
// Extract out the incremental object files.
//
@@ -1698,12 +1924,21 @@ impl BuildRequest {
// -Wl,-all_load
// ```
let mut dylibs = vec![];
let mut object_files = args
// Tip objects from link_args are temps — safe to delete after linking.
let temp_objects: Vec<PathBuf> = args
.link_args
.iter()
.filter(|arg| arg.ends_with(".rcgu.o"))
.sorted()
.map(PathBuf::from)
.collect::<Vec<_>>();
.collect();
// Merge both sets for the linker.
let mut object_files: Vec<PathBuf> =
Vec::with_capacity(cached_objects.len() + temp_objects.len());
object_files.append(&mut cached_objects);
object_files.extend(temp_objects.iter().cloned());
// On non-wasm platforms, we generate a special shim object file which converts symbols from
// fat binary into direct addresses from the running process.
@@ -1734,7 +1969,7 @@ impl BuildRequest {
// Add the dylibs/sos to the linker args
// Make sure to use the one in the bundle, not the ones in the target dir or system.
for arg in &rustc_args.link_args {
for arg in &args.link_args {
if arg.ends_with(".dylib") || arg.ends_with(".so") {
let path = PathBuf::from(arg);
dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
@@ -1755,7 +1990,7 @@ impl BuildRequest {
let mut out_args: Vec<OsString> = vec![];
out_args.extend(object_files.iter().map(Into::into));
out_args.extend(dylibs.iter().map(Into::into));
out_args.extend(self.thin_link_args(&args)?.iter().map(Into::into));
out_args.extend(self.thin_link_args(&args.link_args)?.iter().map(Into::into));
out_args.extend(out_arg.iter().map(Into::into));
if cfg!(windows) {
@@ -1769,7 +2004,7 @@ impl BuildRequest {
}
// Add more search paths for the linker
let mut command_envs = rustc_args.envs.clone();
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") {
@@ -1808,8 +2043,8 @@ impl BuildRequest {
//
// Fortunately, this binary exists in two places - the deps dir and the target out dir. We
// can just remove the one in the deps dir and the problem goes away.
if let Some(idx) = args.iter().position(|arg| *arg == "-o") {
_ = std::fs::remove_file(PathBuf::from(args[idx + 1].as_str()));
if let Some(idx) = args.link_args.iter().position(|arg| *arg == "-o") {
_ = std::fs::remove_file(PathBuf::from(args.link_args[idx + 1].as_str()));
}
// Now extract the assets from the fat binary
@@ -1821,9 +2056,9 @@ impl BuildRequest {
self.write_index_html(&artifacts.assets)
.context("Failed to write index.html")?;
// Clean up the temps manually
// todo: we might want to keep them around for debugging purposes
for file in object_files {
// Clean up temp object files (tip incremental objects + stub.o).
// Cached dep objects in object_cache/ are NOT deleted — they persist across patches.
for file in &temp_objects {
_ = std::fs::remove_file(file);
}
@@ -2509,7 +2744,14 @@ impl BuildRequest {
// ends up doing some recursive nonsense and dx is trying to link instead of compiling.
//
// todo: maybe rustc needs to be found on the FS instead of using the one in the path?
BuildMode::Thin { rustc_args, .. } => {
BuildMode::Thin {
workspace_rustc_args,
..
} => {
let rustc_args = workspace_rustc_args
.get(&format!("{}.bin", self.tip_crate_name()))
.context("Missing rustc args for tip crate")?;
let mut cmd = Command::new("rustc");
cmd.current_dir(self.workspace_dir());
cmd.env_clear();
@@ -2533,13 +2775,11 @@ impl BuildRequest {
Ok(cmd)
}
// For Base and Fat builds, we use a regular cargo setup, but we might need to intercept
// rustc itself in case we're hot-patching and need a reliable rustc environment to
// continuously recompile the top-level crate with.
// For Base and Fat builds, we use a regular cargo setup, but we intercept rustc for
// workspace member crates to capture their args/envs for hot-patching.
//
// In the future, when we support hot-patching *all* workspace crates, we will need to
// make use of the RUSTC_WORKSPACE_WRAPPER environment variable instead of RUSTC_WRAPPER
// and then keep track of env and args on a per-crate basis.
// We use RUSTC_WORKSPACE_WRAPPER which wraps only workspace member crates, letting us
// capture per-crate args without interfering with external dependency compilation.
//
// We've also had a number of issues with incorrect canonicalization when passing paths
// through envs on windows, hence the frequent use of dunce::canonicalize.
@@ -2566,15 +2806,18 @@ impl BuildRequest {
.envs(env.iter().map(|(k, v)| (k.as_ref(), v)));
if matches!(build_mode, BuildMode::Fat | BuildMode::Base { run: true }) {
let args_dir = self.rustc_wrapper_args_dir();
std::fs::create_dir_all(&args_dir)
.context("Failed to create rustc wrapper args directory")?;
cmd.env(
DX_RUSTC_WRAPPER_ENV_VAR,
dunce::canonicalize(self.rustc_wrapper_args_file())
.context("Failed to canonicalize rustc wrapper args file")?
dunce::canonicalize(&args_dir)
.context("Failed to canonicalize rustc wrapper args dir")?
.display()
.to_string(),
);
cmd.env(
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
Workspace::path_to_dx()?.display().to_string(),
);
}
@@ -3488,8 +3731,13 @@ impl BuildRequest {
self.session_cache_dir.join(self.bundle.to_string())
}
pub(crate) fn rustc_wrapper_args_file(&self) -> PathBuf {
self.session_cache_dir().join("rustc_wrapper_args.txt")
pub(crate) fn rustc_wrapper_args_dir(&self) -> PathBuf {
self.session_cache_dir().join("rustc_wrapper_args")
}
/// The crate name that rustc uses for the tip crate (hyphens replaced with underscores).
fn tip_crate_name(&self) -> String {
self.main_target.replace('-', "_")
}
fn link_err_file(&self) -> PathBuf {
@@ -4772,38 +5020,202 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
/// Blow away the fingerprint for this package, forcing rustc to recompile it.
///
/// This prevents rustc from using the cached version of the binary, which can cause issues
/// Find workspace crates that directly depend on the given crate.
///
/// Returns underscore-normalized crate names of workspace members that have `crate_name`
/// as a dependency. Used for cascade detection — when a dep's public symbols change,
/// its dependents need recompilation too.
pub(crate) fn workspace_dependents_of(&self, crate_name: &str) -> Vec<String> {
let krates = &self.workspace.krates;
// Find the NodeId for the target crate
let target_nid = krates.workspace_members().find_map(|member| {
if let krates::Node::Krate { id, krate, .. } = member {
if krate.name.replace('-', "_") == crate_name {
return krates.nid_for_kid(id);
}
}
None
});
let Some(target_nid) = target_nid else {
return Vec::new();
};
// Use krates' direct_dependents to find reverse deps, filter to workspace members
let workspace_names: HashSet<String> = krates
.workspace_members()
.filter_map(|m| {
if let krates::Node::Krate { krate, .. } = m {
Some(krate.name.replace('-', "_"))
} else {
None
}
})
.collect();
krates
.direct_dependents(target_nid)
.into_iter()
.filter_map(|dep| {
let name = dep.krate.name.replace('-', "_");
if workspace_names.contains(&name) {
Some(name)
} else {
None
}
})
.collect()
}
/// Compile a workspace dependency crate directly with `rustc` using its captured args.
///
/// This produces an updated rlib at the same path cargo originally wrote to.
/// Used during thin builds to recompile changed workspace deps before the tip crate.
async fn compile_dep_crate(&self, crate_name: &str, rustc_args: &RustcArgs) -> Result<()> {
let mut cmd = Command::new("rustc");
cmd.current_dir(self.workspace_dir());
cmd.env_clear();
// Skip args[0] which is the rustc binary path captured by the wrapper
cmd.args(rustc_args.args[1..].iter());
// Restore the captured environment, filtering out wrapper env vars and
// stale cargo jobserver vars to prevent recursive invocation and warnings.
let filtered_env_keys = [
"RUSTC_WORKSPACE_WRAPPER",
"RUSTC_WRAPPER",
DX_RUSTC_WRAPPER_ENV_VAR,
"CARGO_MAKEFLAGS",
"MAKEFLAGS",
];
cmd.envs(
rustc_args
.envs
.iter()
.filter(|(k, _)| !filtered_env_keys.contains(&k.as_str()))
.cloned(),
);
let output = cmd.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to compile workspace dep crate '{crate_name}':\n{stderr}");
}
Ok(())
}
/// Find the rlib path for a workspace crate from its captured rustc args.
///
/// Extracts `--out-dir` and `-C extra-filename` from the args to construct the exact
/// rlib filename. This is important because multiple rlibs for the same crate can coexist
/// in the deps directory (e.g., from different dx builds that produce different `-C metadata`),
/// and globbing would return an arbitrary one.
fn find_rlib_for_crate(&self, crate_name: &str, rustc_args: &RustcArgs) -> Option<PathBuf> {
// Extract --out-dir from the captured args
let out_dir = rustc_args
.args
.iter()
.zip(rustc_args.args.iter().skip(1))
.find(|(flag, _)| *flag == "--out-dir")
.map(|(_, dir)| PathBuf::from(dir))?;
// Extract -C extra-filename from captured args.
// Cargo passes this to rustc to disambiguate output filenames via metadata hash.
// Handle all forms: `-Cextra-filename=X`, `-C extra-filename=X`, and `-C` `extra-filename=X`.
let extra_filename = rustc_args.args.iter().enumerate().find_map(|(i, arg)| {
arg.strip_prefix("-Cextra-filename=")
.map(|s| s.to_string())
.or_else(|| {
// Handle `-C` followed by `extra-filename=X` as separate args
if arg == "-C" {
rustc_args.args.get(i + 1).and_then(|next| {
next.strip_prefix("extra-filename=").map(|s| s.to_string())
})
} else {
None
}
})
});
// If we have an exact extra-filename, construct the precise rlib path.
if let Some(extra) = &extra_filename {
let exact = out_dir.join(format!("lib{crate_name}{extra}.rlib"));
if exact.exists() {
return Some(exact);
}
}
// Fallback: glob for lib<crate_name>-<hash>.rlib in the output directory.
// This handles cases where -C extra-filename isn't in the captured args.
// Prefer the most recently modified rlib to avoid picking up stale artifacts.
let prefix = format!("lib{crate_name}-");
let entries = std::fs::read_dir(&out_dir).ok()?;
let mut best: Option<(PathBuf, std::time::SystemTime)> = None;
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with(&prefix) && name.ends_with(".rlib") {
let mtime = entry.metadata().ok()?.modified().ok()?;
if best.as_ref().map_or(true, |(_, t)| mtime > *t) {
best = Some((entry.path(), mtime));
}
}
}
}
if let Some((path, _)) = best {
return Some(path);
}
None
}
/// with our hotpatching setup since it uses linker interception.
///
/// This is sadly a hack. I think there might be other ways of busting the fingerprint (rustc wrapper?)
/// but that would require relying on cargo internals.
///
/// This might stop working if/when cargo stabilizes contents-based fingerprinting.
///
/// `dx` compiles everything with `--target` which ends up with a structure like:
/// `target/<triple>/<profile>/.fingerprint/<package_name>-<hash>`
///
/// Normally you can't rely on this structure (ie with `cargo build`) but the explicit
/// target arg guarantees this will work.
fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
if matches!(ctx.mode, BuildMode::Fat) {
// `dx` compiles everything with `--target` which ends up with a structure like:
// target/<triple>/<profile>/.fingerprint/<package_name>-<hash>
//
// normally you can't rely on this structure (ie with `cargo build`) but the explicit
// target arg guarantees this will work.
let fingerprint_dir = self
.target_dir
.join(self.triple.to_string())
.join(&self.profile)
.join(".fingerprint");
// Bust fingerprints for ALL workspace member crates during Fat builds.
// This ensures cargo recompiles them through RUSTC_WORKSPACE_WRAPPER
// so we capture their rustc args for later thin builds.
let mut busted = HashSet::new();
for member in self.workspace.krates.workspace_members() {
if let krates::Node::Krate { krate, .. } = member {
busted.insert(krate.name.as_str());
}
}
// split at the last `-` used to separate the hash from the name
// This causes to more aggressively bust hashes for all combinations of features
// and fingerprints for this package since we're just ignoring the hash
for entry in std::fs::read_dir(&fingerprint_dir)?.flatten() {
if let Some(fname) = entry.file_name().to_str() {
if let Some((name, _)) = fname.rsplit_once('-') {
if name == self.package().name {
_ = std::fs::remove_dir_all(entry.path());
if let Ok(entries) = std::fs::read_dir(&fingerprint_dir) {
for entry in entries.flatten() {
if let Some(fname) = entry.file_name().to_str() {
if let Some((name, _)) = fname.rsplit_once('-') {
if busted.contains(name) {
_ = std::fs::remove_dir_all(entry.path());
}
}
}
}
}
}
Ok(())
}