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
@@ -29,6 +29,7 @@ subsecond-types = { workspace = true }
|
||||
dioxus-cli-telemetry = { workspace = true }
|
||||
dioxus-component-manifest = { workspace = true }
|
||||
|
||||
bytes = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "cargo"] }
|
||||
convert_case = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::{
|
||||
serve::WebServer, verbosity_or_default, BuildArtifacts, BuildRequest, BuildStage,
|
||||
BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result, RustcArgs, StructuredOutput,
|
||||
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
||||
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
||||
RustcArgs, StructuredOutput,
|
||||
};
|
||||
use anyhow::{bail, Context, Error};
|
||||
use dioxus_cli_opt::process_file_to;
|
||||
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
||||
use itertools::Itertools;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
@@ -103,6 +105,13 @@ pub(crate) struct AppBuilder {
|
||||
|
||||
/// The debugger for the app - must be enabled with the `d` key
|
||||
pub(crate) pid: Option<u32>,
|
||||
|
||||
/// Cumulative set of workspace crates modified since the last fat build.
|
||||
/// Each patch includes objects from ALL crates in this set.
|
||||
pub modified_crates: HashSet<String>,
|
||||
|
||||
/// Cache of the latest `.rcgu.o` files for each modified workspace crate.
|
||||
pub object_cache: ObjectCache,
|
||||
}
|
||||
|
||||
impl AppBuilder {
|
||||
@@ -159,6 +168,8 @@ impl AppBuilder {
|
||||
artifacts: None,
|
||||
patch_cache: None,
|
||||
pid: None,
|
||||
modified_crates: HashSet::new(),
|
||||
object_cache: ObjectCache::new(&request.session_cache_dir()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -319,7 +330,12 @@ impl AppBuilder {
|
||||
update
|
||||
}
|
||||
|
||||
pub(crate) fn patch_rebuild(&mut self, changed_files: Vec<PathBuf>, build_id: BuildId) {
|
||||
pub(crate) fn patch_rebuild(
|
||||
&mut self,
|
||||
changed_files: Vec<PathBuf>,
|
||||
changed_crates: Vec<String>,
|
||||
build_id: BuildId,
|
||||
) {
|
||||
// We need the rustc args from the original build to pass to the new build
|
||||
let Some(artifacts) = self.artifacts.as_ref().cloned() else {
|
||||
tracing::warn!(
|
||||
@@ -353,6 +369,38 @@ impl AppBuilder {
|
||||
.context("Failed to get patch cache")
|
||||
.unwrap();
|
||||
|
||||
// Pre-compute the cumulative modified_crates set. Every patch includes objects from
|
||||
// ALL crates modified since the fat build. We compute the full cascade closure here
|
||||
// (while we have &mut self) so it doesn't need to be round-tripped through BuildArtifacts.
|
||||
//
|
||||
// Note: compile_workspace_deps() independently computes which crates to compile for THIS
|
||||
// patch (starting from changed_crates + cascade). That serves a different purpose — it only
|
||||
// compiles what changed now, not everything ever modified. Both use workspace_dependents_of
|
||||
// for the BFS, so they stay in sync automatically.
|
||||
let tip_crate_name = self.build.main_target.replace('-', "_");
|
||||
self.modified_crates.insert(tip_crate_name.clone());
|
||||
|
||||
// Add changed crates and their transitive workspace dependents (cascade).
|
||||
let mut to_visit: Vec<String> = changed_crates.clone();
|
||||
let mut visited = HashSet::new();
|
||||
while let Some(c) = to_visit.pop() {
|
||||
if !visited.insert(c.clone()) {
|
||||
continue;
|
||||
}
|
||||
self.modified_crates.insert(c.clone());
|
||||
for dep in self.build.workspace_dependents_of(&c) {
|
||||
if dep != tip_crate_name && !visited.contains(&dep) {
|
||||
to_visit.push(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Patch rebuild: changed_crates={:?}, modified_crates={:?}",
|
||||
changed_crates,
|
||||
self.modified_crates,
|
||||
);
|
||||
|
||||
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
|
||||
self.abort_all(BuildStage::Restarting);
|
||||
self.build_task = tokio::spawn({
|
||||
@@ -362,9 +410,12 @@ impl AppBuilder {
|
||||
tx: self.tx.clone(),
|
||||
mode: BuildMode::Thin {
|
||||
changed_files,
|
||||
rustc_args: artifacts.direct_rustc,
|
||||
changed_crates,
|
||||
modified_crates: self.modified_crates.clone(),
|
||||
workspace_rustc_args: artifacts.workspace_rustc_args,
|
||||
aslr_reference,
|
||||
cache,
|
||||
object_cache: self.object_cache.clone(),
|
||||
},
|
||||
};
|
||||
async move { request.build(&ctx).await }
|
||||
@@ -378,6 +429,10 @@ impl AppBuilder {
|
||||
self.abort_all(BuildStage::Restarting);
|
||||
self.artifacts.take();
|
||||
self.patch_cache.take();
|
||||
|
||||
// A full rebuild resets all accumulated hotpatch state — the fat binary is a clean baseline.
|
||||
self.modified_crates.clear();
|
||||
self.object_cache = ObjectCache::new(&self.build.session_cache_dir());
|
||||
self.build_task = tokio::spawn({
|
||||
let request = self.build.clone();
|
||||
let ctx = BuildContext {
|
||||
@@ -545,12 +600,21 @@ impl AppBuilder {
|
||||
));
|
||||
}
|
||||
|
||||
// If there's any CARGO vars in the rustc_wrapper files, push those too
|
||||
if let Ok(res) = std::fs::read_to_string(self.build.rustc_wrapper_args_file()) {
|
||||
if let Ok(res) = serde_json::from_str::<RustcArgs>(&res) {
|
||||
for (key, value) in res.envs {
|
||||
if key.starts_with("CARGO_") {
|
||||
envs.push((key, value));
|
||||
// If there's any CARGO vars in the rustc_wrapper files, push those too.
|
||||
// Read from any per-crate args file in the directory (they all share the same CARGO_ envs).
|
||||
if let Ok(entries) = std::fs::read_dir(self.build.rustc_wrapper_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) {
|
||||
for (key, value) in args.envs {
|
||||
if key.starts_with("CARGO_") {
|
||||
envs.push((key, value));
|
||||
}
|
||||
}
|
||||
break; // Only need one file for CARGO_ env vars
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -740,17 +804,21 @@ impl AppBuilder {
|
||||
tracing::info!(
|
||||
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
|
||||
changed_file
|
||||
.display()
|
||||
.to_string()
|
||||
.trim_start_matches(&self.build.crate_dir().display().to_string()),
|
||||
.strip_prefix(self.build.workspace_dir())
|
||||
.unwrap_or(changed_file)
|
||||
.display(),
|
||||
SystemTime::now()
|
||||
.duration_since(res.time_start)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
);
|
||||
|
||||
// Commit this patch
|
||||
self.patches.push(jump_table.clone());
|
||||
|
||||
// Sync the updated object cache back from the build artifacts.
|
||||
self.object_cache = res.object_cache.clone();
|
||||
|
||||
Ok(jump_table)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//! Object file cache for workspace hotpatching.
|
||||
//!
|
||||
//! Maintains the latest `.rcgu.o` files for each crate in the cumulative `modified_crates` set.
|
||||
//! Used for accumulated relinking: combining objects from all modified crates into a single
|
||||
//! patch dylib.
|
||||
//!
|
||||
//! Objects are stored on disk under `session_cache_dir/object_cache/{crate_name}/` so they
|
||||
//! persist across patches without holding file contents in memory. The session cache dir
|
||||
//! lives in `/tmp/` and is cleaned up by the OS.
|
||||
//!
|
||||
//! - **Dep crates:** objects are extracted from their rlib in `target/deps/`.
|
||||
//! - **Tip crate:** objects are copied from linker arg paths since incremental compilation
|
||||
//! overwrites them in place.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Cache of compiled object files on disk, keyed by crate name.
|
||||
///
|
||||
/// After each compilation, the cache is updated for the compiled crate by extracting
|
||||
/// objects to `dir/{crate_name}/`. On relink, paths from all crates in `modified_crates`
|
||||
/// are passed directly to the linker — no intermediate copy needed.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ObjectCache {
|
||||
/// Root directory: `session_cache_dir/object_cache/`
|
||||
dir: PathBuf,
|
||||
|
||||
/// crate_name -> object file paths on disk
|
||||
objects: HashMap<String, Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl ObjectCache {
|
||||
pub fn new(session_cache_dir: &Path) -> Self {
|
||||
let dir = session_cache_dir.join("object_cache");
|
||||
Self {
|
||||
dir,
|
||||
objects: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `.rcgu.o` files from an rlib archive and write them to
|
||||
/// `dir/{crate_name}/`. Replaces any previously cached objects for this crate.
|
||||
pub fn cache_from_rlib(&mut self, crate_name: &str, rlib_path: &Path) -> anyhow::Result<()> {
|
||||
let crate_dir = self.dir.join(crate_name);
|
||||
// Clear previous objects for this crate
|
||||
if crate_dir.exists() {
|
||||
std::fs::remove_dir_all(&crate_dir)?;
|
||||
}
|
||||
std::fs::create_dir_all(&crate_dir)?;
|
||||
|
||||
let rlib_contents = std::fs::read(rlib_path)?;
|
||||
let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents));
|
||||
let mut paths = Vec::new();
|
||||
|
||||
while let Some(Ok(mut entry)) = reader.next_entry() {
|
||||
let name = std::str::from_utf8(entry.header().identifier())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
// Skip rmeta and empty entries
|
||||
if name.ends_with(".rmeta") || entry.header().size() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only keep object files
|
||||
if !name.ends_with(".o") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut data = Vec::with_capacity(entry.header().size() as usize);
|
||||
entry.read_to_end(&mut data)?;
|
||||
|
||||
let obj_path = crate_dir.join(&name);
|
||||
std::fs::write(&obj_path, &data)?;
|
||||
paths.push(obj_path);
|
||||
}
|
||||
|
||||
self.objects.insert(crate_name.to_string(), paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cache tip crate objects by copying them from their filesystem paths.
|
||||
///
|
||||
/// Tip crate `.rcgu.o` files get overwritten on recompilation, so we copy
|
||||
/// them into our cache directory for stable access.
|
||||
pub fn cache_from_paths(
|
||||
&mut self,
|
||||
crate_name: &str,
|
||||
object_paths: &[impl AsRef<Path>],
|
||||
) -> anyhow::Result<()> {
|
||||
let crate_dir = self.dir.join(crate_name);
|
||||
if crate_dir.exists() {
|
||||
std::fs::remove_dir_all(&crate_dir)?;
|
||||
}
|
||||
std::fs::create_dir_all(&crate_dir)?;
|
||||
|
||||
let mut paths = Vec::with_capacity(object_paths.len());
|
||||
|
||||
for path in object_paths {
|
||||
let path = path.as_ref();
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default();
|
||||
let dest = crate_dir.join(name);
|
||||
std::fs::copy(path, &dest)?;
|
||||
paths.push(dest);
|
||||
}
|
||||
|
||||
self.objects.insert(crate_name.to_string(), paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get cached object file paths for a crate.
|
||||
pub fn get(&self, crate_name: &str) -> Option<&Vec<PathBuf>> {
|
||||
self.objects.get(crate_name)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
mod assets;
|
||||
mod builder;
|
||||
mod cache;
|
||||
mod context;
|
||||
mod manifest;
|
||||
mod patch;
|
||||
@@ -19,6 +20,7 @@ mod tools;
|
||||
|
||||
pub(crate) use assets::*;
|
||||
pub(crate) use builder::*;
|
||||
pub(crate) use cache::*;
|
||||
pub(crate) use context::*;
|
||||
pub(crate) use manifest::*;
|
||||
pub(crate) use patch::*;
|
||||
|
||||
+102
-22
@@ -4,7 +4,7 @@ use object::{
|
||||
macho::{self},
|
||||
read::File,
|
||||
write::{MachOBuildVersion, SectionId, StandardSection, Symbol, SymbolId, SymbolSection},
|
||||
Endianness, Object, ObjectSymbol, SymbolFlags, SymbolKind, SymbolScope,
|
||||
Endianness, Object, ObjectSection, ObjectSymbol, SymbolFlags, SymbolKind, SymbolScope,
|
||||
};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use std::{
|
||||
@@ -76,6 +76,16 @@ pub struct HotpatchModuleCache {
|
||||
|
||||
// ... native stuff
|
||||
pub symbol_table: HashMap<String, CachedSymbol>,
|
||||
|
||||
/// Contents of the .tdata section from the original binary (TLS initialization image).
|
||||
/// Used to provide correct init data for TLS symbol stubs instead of garbage addresses.
|
||||
pub tls_init_data: Vec<u8>,
|
||||
|
||||
/// Map from `$tlv$init` symbol name to (offset_in_tdata, computed_size).
|
||||
/// On macOS, Mach-O nlist doesn't carry symbol sizes, so we compute them from
|
||||
/// adjacent symbol addresses in the `__thread_data` section. This lets us provide
|
||||
/// correctly-sized TLS init data in stubs instead of defaulting to pointer_width.
|
||||
pub tls_init_sizes: HashMap<String, (u64, u64)>,
|
||||
}
|
||||
|
||||
pub struct CachedSymbol {
|
||||
@@ -277,10 +287,55 @@ impl HotpatchModuleCache {
|
||||
))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// Extract TLS initialization data and section metadata.
|
||||
// This is used to correctly initialize TLS symbols in the stub
|
||||
// instead of writing bogus absolute addresses into .tdata.
|
||||
let tls_section = obj
|
||||
.sections()
|
||||
.find(|s| matches!(s.name(), Ok(".tdata" | "__thread_data")));
|
||||
|
||||
let tls_init_data = tls_section
|
||||
.as_ref()
|
||||
.and_then(|s| s.data().ok())
|
||||
.unwrap_or(&[])
|
||||
.to_vec();
|
||||
|
||||
// Build TLS init size map for macOS. Mach-O nlist doesn't carry symbol
|
||||
// sizes, so we compute them from adjacent symbols in __thread_data.
|
||||
// LLVM/rustc names init data symbols as `FOO$tlv$init` in __thread_data.
|
||||
let tls_data_addr = tls_section.as_ref().map(|s| s.address()).unwrap_or(0);
|
||||
let tls_data_size = tls_section.as_ref().map(|s| s.size()).unwrap_or(0);
|
||||
let tls_section_index = tls_section.as_ref().map(|s| s.index());
|
||||
|
||||
let mut tls_init_syms: Vec<(u64, String)> = Vec::new();
|
||||
for sym in obj.symbols() {
|
||||
if let (Some(section_idx), Ok(sname)) = (sym.section_index(), sym.name()) {
|
||||
if Some(section_idx) == tls_section_index {
|
||||
let offset = sym.address().saturating_sub(tls_data_addr);
|
||||
tls_init_syms.push((offset, sname.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
tls_init_syms.sort_by_key(|(addr, _)| *addr);
|
||||
tls_init_syms.dedup_by_key(|(addr, _)| *addr);
|
||||
|
||||
let mut tls_init_sizes: HashMap<String, (u64, u64)> = HashMap::new();
|
||||
for (i, (offset, sname)) in tls_init_syms.iter().enumerate() {
|
||||
let size = if i + 1 < tls_init_syms.len() {
|
||||
tls_init_syms[i + 1].0 - offset
|
||||
} else {
|
||||
tls_data_size.saturating_sub(*offset)
|
||||
};
|
||||
tls_init_sizes.insert(sname.clone(), (*offset, size));
|
||||
}
|
||||
|
||||
HotpatchModuleCache {
|
||||
symbol_table,
|
||||
path: original.to_path_buf(),
|
||||
old_bytes,
|
||||
tls_init_data,
|
||||
tls_init_sizes,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1106,36 +1161,61 @@ pub fn create_undefined_symbol_stub(
|
||||
PointerWidth::U64 => 8,
|
||||
};
|
||||
|
||||
let size = if sym.size == 0 {
|
||||
pointer_width
|
||||
} else {
|
||||
sym.size
|
||||
};
|
||||
// Resolve the TLS init data offset and size.
|
||||
//
|
||||
// On ELF: sym.address IS the TLS offset and sym.size is the data size.
|
||||
// On Mach-O: sym.address points to __thread_vars (TLV descriptor), NOT
|
||||
// __thread_data. Mach-O nlist has no size field (always 0). We look up
|
||||
// the corresponding $tlv$init symbol (LLVM convention) to get the real
|
||||
// offset and size within __thread_data.
|
||||
//
|
||||
// Note: each patch gets its own TLS copy (not shared with the main exe).
|
||||
// TLS variables reset to their initial value on patch.
|
||||
// Use the full name (with Mach-O `_` prefix) since tls_init_sizes
|
||||
// keys come from the same symbol table and include the prefix.
|
||||
let init_key = format!("{}$tlv$init", name);
|
||||
let (tls_offset, size) =
|
||||
if let Some(&(offset, size)) = cache.tls_init_sizes.get(&init_key) {
|
||||
// macOS: found the $tlv$init symbol with correct offset and size
|
||||
(offset, size)
|
||||
} else if sym.size > 0 {
|
||||
// ELF: sym.address is the TLS offset, sym.size is the data size
|
||||
(sym.address, sym.size)
|
||||
} else if !cache.tls_init_sizes.is_empty() {
|
||||
// macOS fallback: $tlv$init not found but map isn't empty (binary
|
||||
// might be partially stripped). Use entire tdata as upper bound.
|
||||
(0, cache.tls_init_data.len() as u64)
|
||||
} else {
|
||||
// Last resort (ELF with size=0): use pointer width
|
||||
(sym.address, pointer_width)
|
||||
};
|
||||
|
||||
let align = size.min(pointer_width).next_power_of_two();
|
||||
let mut init = vec![0u8; size as usize];
|
||||
|
||||
// write the contents of the symbol to the init vec
|
||||
init.iter_mut()
|
||||
.zip(match triple.endianness() {
|
||||
Ok(target_lexicon::Endianness::Little) => abs_addr.to_le_bytes(),
|
||||
Ok(target_lexicon::Endianness::Big) => abs_addr.to_be_bytes(),
|
||||
_ => return Err(PatchError::UnsupportedPlatform(triple.to_string())),
|
||||
})
|
||||
.for_each(|(b, v)| *b = v);
|
||||
let start = tls_offset as usize;
|
||||
let end = start + size as usize;
|
||||
let init = if end <= cache.tls_init_data.len() {
|
||||
cache.tls_init_data[start..end].to_vec()
|
||||
} else {
|
||||
// Beyond .tdata bounds (.tbss) or Mach-O fallback: zero-init
|
||||
vec![0u8; size as usize]
|
||||
};
|
||||
|
||||
let offset = obj.append_section_data(tls_section, &init, align);
|
||||
|
||||
obj.add_symbol(Symbol {
|
||||
// Use add_symbol_data() so the object crate's Mach-O writer auto-creates
|
||||
// __thread_vars TLV descriptors (via macho_add_thread_var). Without this,
|
||||
// the symbol stays in __thread_data and the runtime misinterprets raw init
|
||||
// bytes as a TLV descriptor — first 8 bytes become the thunk pointer.
|
||||
let sym_id = obj.add_symbol(Symbol {
|
||||
name: name.as_bytes()[name_offset..].to_vec(),
|
||||
value: offset, // offset inside .tdata
|
||||
size,
|
||||
value: 0,
|
||||
size: 0,
|
||||
scope: SymbolScope::Linkage,
|
||||
kind: SymbolKind::Tls,
|
||||
weak: false,
|
||||
section: SymbolSection::Section(tls_section),
|
||||
flags: SymbolFlags::None, // ignore for these stubs
|
||||
section: SymbolSection::Undefined,
|
||||
flags: SymbolFlags::None,
|
||||
});
|
||||
obj.add_symbol_data(sym_id, tls_section, &init, align);
|
||||
}
|
||||
|
||||
// We just assume all non-text symbols are data (globals, statics, etc)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -209,12 +209,24 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
||||
|
||||
impl BuildArtifacts {
|
||||
pub(crate) fn into_structured_output(self) -> StructuredBuildArtifacts {
|
||||
// Extract the tip crate's args for the structured output.
|
||||
// The tip crate is identified by replacing hyphens with underscores in the target name,
|
||||
// but since we don't have the BuildRequest here, we look for the entry with link_args
|
||||
// (only the tip crate has link_args attached) or fall back to any entry.
|
||||
let tip_args = self
|
||||
.workspace_rustc_args
|
||||
.values()
|
||||
.find(|a| !a.link_args.is_empty())
|
||||
.or_else(|| self.workspace_rustc_args.values().next())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
StructuredBuildArtifacts {
|
||||
path: self.root_dir,
|
||||
exe: self.exe,
|
||||
rustc_args: self.direct_rustc.args,
|
||||
rustc_envs: self.direct_rustc.envs,
|
||||
link_args: self.direct_rustc.link_args,
|
||||
rustc_args: tip_args.args,
|
||||
rustc_envs: tip_args.envs,
|
||||
link_args: tip_args.link_args,
|
||||
assets: self.assets.unique_assets().cloned().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::{
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, io::Read};
|
||||
|
||||
const HELP_HEADING: &str = "Hotpatching a binary";
|
||||
|
||||
@@ -70,15 +70,25 @@ impl HotpatchTip {
|
||||
// consider a shared-mem approach or a binary serializer? something like arrow / parquet / bincode?
|
||||
let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?);
|
||||
|
||||
let mode = BuildMode::Thin {
|
||||
rustc_args: crate::RustcArgs {
|
||||
let tip_crate_name = request.main_target.replace('-', "_");
|
||||
let mut workspace_rustc_args = HashMap::new();
|
||||
workspace_rustc_args.insert(
|
||||
format!("{tip_crate_name}.bin"),
|
||||
crate::RustcArgs {
|
||||
args: rustc_args,
|
||||
envs: rustc_envs,
|
||||
link_args,
|
||||
},
|
||||
);
|
||||
|
||||
let mode = BuildMode::Thin {
|
||||
workspace_rustc_args,
|
||||
changed_files: vec![],
|
||||
changed_crates: vec![],
|
||||
modified_crates: std::collections::HashSet::new(),
|
||||
aslr_reference: self.aslr_reference,
|
||||
cache: cache.clone(),
|
||||
object_cache: crate::ObjectCache::new(&request.session_cache_dir()),
|
||||
};
|
||||
|
||||
let artifacts = AppBuilder::started(request, mode, build_id)?
|
||||
|
||||
@@ -5,9 +5,10 @@ use std::{
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
/// The environment variable indicating where the args file is located.
|
||||
/// The environment variable indicating where the args directory is located.
|
||||
///
|
||||
/// When `dx-rustc` runs, it writes its arguments to this file.
|
||||
/// When `dx-rustc` runs, it writes each workspace crate's arguments to a
|
||||
/// separate file in this directory: `{dir}/{crate_name}.json`.
|
||||
pub const DX_RUSTC_WRAPPER_ENV_VAR: &str = "DX_RUSTC";
|
||||
|
||||
/// Is `dx` being used as a rustc wrapper?
|
||||
@@ -64,7 +65,7 @@ fn has_linking_args() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Run rustc directly, but output the result to a file.
|
||||
/// Run rustc directly, but output the result to a per-crate file in the args directory.
|
||||
///
|
||||
/// <https://doc.rust-lang.org/cargo/reference/config.html#buildrustc>
|
||||
pub fn run_rustc() -> ExitCode {
|
||||
@@ -75,11 +76,11 @@ pub fn run_rustc() -> ExitCode {
|
||||
.run_link();
|
||||
}
|
||||
|
||||
let var_file: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR)
|
||||
let args_dir: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR)
|
||||
.expect("DX_RUSTC env var must be set")
|
||||
.into();
|
||||
|
||||
// Cargo invokes a wrapper like: `wrapper-name rustc [args...]`
|
||||
// Cargo invokes a workspace wrapper like: `wrapper-name rustc [args...]`
|
||||
// We skip our own executable name (`wrapper-name`) to get the args passed to us.
|
||||
let captured_args = args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
@@ -89,32 +90,51 @@ pub fn run_rustc() -> ExitCode {
|
||||
link_args: Default::default(),
|
||||
};
|
||||
|
||||
// Another terrible hack to avoid caching non-sensical args when
|
||||
// a build is completely fresh (rustc is invoked with --crate-name ___)
|
||||
if rustc_args
|
||||
// Extract the crate name from the args to use as the filename.
|
||||
// Skip non-sensical args when a build is completely fresh (rustc is invoked with --crate-name ___)
|
||||
let crate_name = rustc_args
|
||||
.args
|
||||
.iter()
|
||||
.skip_while(|arg| *arg != "--crate-name")
|
||||
.nth(1)
|
||||
.is_some_and(|name| name != "___")
|
||||
{
|
||||
let parent_dir = var_file
|
||||
.parent()
|
||||
.expect("Args file path has no parent directory");
|
||||
std::fs::create_dir_all(parent_dir)
|
||||
.expect("Failed to create parent directory for args file");
|
||||
.nth(1);
|
||||
|
||||
let serialized_args =
|
||||
serde_json::to_string(&rustc_args).expect("Failed to serialize rustc args");
|
||||
if let Some(crate_name) = crate_name {
|
||||
if crate_name != "___" {
|
||||
std::fs::create_dir_all(&args_dir)
|
||||
.expect("Failed to create args directory for rustc wrapper");
|
||||
|
||||
std::fs::write(&var_file, serialized_args).expect("Failed to write rustc args to file");
|
||||
let crate_type = rustc_args
|
||||
.args
|
||||
.iter()
|
||||
.skip_while(|arg| *arg != "--crate-type")
|
||||
.nth(1)
|
||||
.map(|s| s.as_str());
|
||||
|
||||
let serialized_args =
|
||||
serde_json::to_string(&rustc_args).expect("Failed to serialize rustc args");
|
||||
|
||||
// Write args with an explicit target suffix: {crate_name}.lib.json or
|
||||
// {crate_name}.bin.json. This avoids the ambiguity of a bare {crate_name}.json
|
||||
// and ensures lib+bin crates don't overwrite each other.
|
||||
let suffix = match crate_type {
|
||||
Some("lib" | "rlib") => "lib",
|
||||
Some("bin") => "bin",
|
||||
_ => "bin", // proc-macro, cdylib, etc. — treat as bin
|
||||
};
|
||||
|
||||
std::fs::write(
|
||||
args_dir.join(format!("{crate_name}.{suffix}.json")),
|
||||
&serialized_args,
|
||||
)
|
||||
.expect("Failed to write rustc args to file");
|
||||
}
|
||||
}
|
||||
|
||||
// Run the actual rustc command.
|
||||
// We want all stdout/stderr to be inherited, so the user sees the compiler output.
|
||||
let mut cmd = std::process::Command::new("rustc");
|
||||
|
||||
// The first argument in `captured_args` is "rustc", which we need to skip
|
||||
// The first argument in `captured_args` is the rustc path, which we need to skip
|
||||
// when passing arguments to the `rustc` command we are spawning.
|
||||
cmd.args(captured_args.iter().skip(1));
|
||||
cmd.envs(rustc_args.envs);
|
||||
|
||||
@@ -22,9 +22,9 @@ use notify::{
|
||||
Config, EventKind, RecursiveMode, Watcher as NotifyWatcher,
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
net::{IpAddr, TcpListener},
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -511,9 +511,13 @@ impl AppServer {
|
||||
// A full rebuild is required when the user modifies static initializers which we haven't wired up yet.
|
||||
if needs_full_rebuild && self.automatic_rebuilds {
|
||||
if self.use_hotpatch_engine {
|
||||
self.client.patch_rebuild(files.to_vec(), BuildId::PRIMARY);
|
||||
let changed_crates = self.order_changed_crates(files);
|
||||
|
||||
self.client
|
||||
.patch_rebuild(files.to_vec(), changed_crates.clone(), BuildId::PRIMARY);
|
||||
|
||||
if let Some(server) = self.server.as_mut() {
|
||||
server.patch_rebuild(files.to_vec(), BuildId::SECONDARY);
|
||||
server.patch_rebuild(files.to_vec(), changed_crates, BuildId::SECONDARY);
|
||||
}
|
||||
self.clear_hot_reload_changes();
|
||||
self.clear_cached_rsx();
|
||||
@@ -1163,6 +1167,152 @@ impl AppServer {
|
||||
krates
|
||||
}
|
||||
|
||||
/// Compute the ordered compilation chain from a changed workspace crate to the tip crate.
|
||||
///
|
||||
/// Returns crate names (underscore-normalized) in compilation order: the changed crate first,
|
||||
/// then each intermediate workspace crate that depends on it, ending with the tip crate.
|
||||
///
|
||||
/// Uses BFS from the tip crate through its workspace dependencies to find the path.
|
||||
/// If the changed crate IS the tip crate, returns just `[tip]`.
|
||||
pub(crate) fn workspace_dep_chain(&self, changed_crate: &str) -> Vec<String> {
|
||||
let tip_name = self.client.build.main_target.replace('-', "_");
|
||||
|
||||
// If the changed crate is the tip, no chain needed
|
||||
if changed_crate == tip_name {
|
||||
return vec![tip_name];
|
||||
}
|
||||
|
||||
// Build a map of workspace crate names to their krates NodeIds
|
||||
let mut name_to_node: HashMap<String, NodeId> = HashMap::new();
|
||||
for member in self.workspace.krates.workspace_members() {
|
||||
if let krates::Node::Krate { id, krate, .. } = member {
|
||||
let normalized = krate.name.replace('-', "_");
|
||||
name_to_node.insert(normalized, self.workspace.krates.nid_for_kid(id).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// BFS/DFS from tip through workspace deps to find path to changed crate.
|
||||
// We walk the dependency edges (tip → its deps → their deps → ...) looking for changed_crate.
|
||||
let Some(&tip_node) = name_to_node.get(&tip_name) else {
|
||||
return vec![changed_crate.to_string()];
|
||||
};
|
||||
|
||||
// parent[node] = the workspace crate that depends on it (closer to tip)
|
||||
let mut parent: HashMap<NodeId, Option<NodeId>> = HashMap::new();
|
||||
parent.insert(tip_node, None);
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(tip_node);
|
||||
|
||||
let mut target_node = None;
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
for (dep, _edge) in self.workspace.krates.get_deps(current) {
|
||||
let (dep_name, dep_nid) = match dep {
|
||||
krates::Node::Krate { id, krate, .. } => {
|
||||
let normalized = krate.name.replace('-', "_");
|
||||
let nid = self.workspace.krates.nid_for_kid(id).unwrap();
|
||||
(normalized, nid)
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Only traverse workspace members
|
||||
if !name_to_node.contains_key(&dep_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if parent.contains_key(&dep_nid) {
|
||||
continue; // already visited
|
||||
}
|
||||
|
||||
parent.insert(dep_nid, Some(current));
|
||||
|
||||
if dep_name == changed_crate {
|
||||
target_node = Some(dep_nid);
|
||||
break;
|
||||
}
|
||||
|
||||
queue.push_back(dep_nid);
|
||||
}
|
||||
|
||||
if target_node.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the path from changed_crate → ... → tip
|
||||
let Some(target) = target_node else {
|
||||
// Changed crate not found in workspace dep graph — just compile it alone
|
||||
return vec![changed_crate.to_string()];
|
||||
};
|
||||
|
||||
let mut chain = vec![];
|
||||
let mut node = target;
|
||||
loop {
|
||||
// Find the crate name for this node
|
||||
let krate = &self.workspace.krates[node];
|
||||
chain.push(krate.name.replace('-', "_"));
|
||||
|
||||
match parent.get(&node) {
|
||||
Some(Some(parent_node)) => node = *parent_node,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
chain
|
||||
}
|
||||
|
||||
/// Order a set of changed workspace crates so that deeper dependencies compile first.
|
||||
///
|
||||
/// Uses `workspace_dep_chain` to determine the depth of each crate in the dependency graph,
|
||||
/// then sorts so that leaves (deepest deps) compile before crates closer to the tip.
|
||||
fn order_changed_crates(&self, files: &[PathBuf]) -> Vec<String> {
|
||||
// Determine which workspace crates changed based on the file paths.
|
||||
// Order them so deeper deps compile first (leaves before dependents).
|
||||
let changed_set: HashSet<String> = files
|
||||
.iter()
|
||||
.filter_map(|f| self.file_to_workspace_crate(f))
|
||||
.collect();
|
||||
|
||||
let mut crates_with_depth: Vec<_> = changed_set
|
||||
.iter()
|
||||
.map(|c| (c.clone(), self.workspace_dep_chain(c).len()))
|
||||
.collect();
|
||||
|
||||
// Longer chain = deeper in dep tree = should compile first
|
||||
crates_with_depth.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
crates_with_depth.into_iter().map(|(c, _)| c).collect()
|
||||
}
|
||||
|
||||
/// Map a changed file path to the workspace crate it belongs to.
|
||||
///
|
||||
/// Returns the crate name in rustc convention (hyphens → underscores), matching the
|
||||
/// `--crate-name` arg used by rustc and the keys in `workspace_rustc_args`.
|
||||
///
|
||||
/// Finds the workspace member whose crate directory is the longest prefix of the file path.
|
||||
fn file_to_workspace_crate(&self, file: &Path) -> Option<String> {
|
||||
let mut best_match: Option<(String, usize)> = None;
|
||||
|
||||
for member in self.workspace.krates.workspace_members() {
|
||||
if let krates::Node::Krate { krate, .. } = member {
|
||||
let Some(crate_dir) = krate.manifest_path.parent() else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(relative) = file.strip_prefix(crate_dir.as_std_path()) {
|
||||
let depth = relative.components().count();
|
||||
let is_better = best_match
|
||||
.as_ref()
|
||||
.map_or(true, |(_, best_depth)| depth < *best_depth);
|
||||
if is_better {
|
||||
best_match = Some((krate.name.replace('-', "_"), depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_match.map(|(name, _)| name)
|
||||
}
|
||||
|
||||
/// Check if this is a fullstack build. This means that there is an additional build with the `server` platform.
|
||||
pub(crate) fn is_fullstack(&self) -> bool {
|
||||
self.fullstack
|
||||
|
||||
Reference in new issue
Block a user