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:
1 parent
9b0d380765
commit
33159f366a
12 files changed
+1229
-348
No files matched your search
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user