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
+1216
-335
No files matched your search
Generated
+224
-224
File diff suppressed because it is too large.
Load diff
@@ -414,6 +414,13 @@ incremental = true
|
|||||||
[profile.dev.package.walrus]
|
[profile.dev.package.walrus]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
# ensure we have adversarial setup for tls
|
||||||
|
[profile.dev.package.cross-tls-crate]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[profile.dev.package.cross-tls-crate-dylib]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
[profile.release-max-opt]
|
[profile.release-max-opt]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ subsecond-types = { workspace = true }
|
|||||||
dioxus-cli-telemetry = { workspace = true }
|
dioxus-cli-telemetry = { workspace = true }
|
||||||
dioxus-component-manifest = { workspace = true }
|
dioxus-component-manifest = { workspace = true }
|
||||||
|
|
||||||
|
bytes = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive", "cargo"] }
|
clap = { workspace = true, features = ["derive", "cargo"] }
|
||||||
convert_case = { workspace = true }
|
convert_case = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
serve::WebServer, verbosity_or_default, BuildArtifacts, BuildRequest, BuildStage,
|
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
||||||
BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result, RustcArgs, StructuredOutput,
|
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
||||||
|
RustcArgs, StructuredOutput,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context, Error};
|
use anyhow::{bail, Context, Error};
|
||||||
use dioxus_cli_opt::process_file_to;
|
use dioxus_cli_opt::process_file_to;
|
||||||
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
env,
|
env,
|
||||||
time::{Duration, Instant, SystemTime},
|
time::{Duration, Instant, SystemTime},
|
||||||
};
|
};
|
||||||
@@ -103,6 +105,13 @@ pub(crate) struct AppBuilder {
|
|||||||
|
|
||||||
/// The debugger for the app - must be enabled with the `d` key
|
/// The debugger for the app - must be enabled with the `d` key
|
||||||
pub(crate) pid: Option<u32>,
|
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 {
|
impl AppBuilder {
|
||||||
@@ -159,6 +168,8 @@ impl AppBuilder {
|
|||||||
artifacts: None,
|
artifacts: None,
|
||||||
patch_cache: None,
|
patch_cache: None,
|
||||||
pid: None,
|
pid: None,
|
||||||
|
modified_crates: HashSet::new(),
|
||||||
|
object_cache: ObjectCache::new(&request.session_cache_dir()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +330,12 @@ impl AppBuilder {
|
|||||||
update
|
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
|
// We need the rustc args from the original build to pass to the new build
|
||||||
let Some(artifacts) = self.artifacts.as_ref().cloned() else {
|
let Some(artifacts) = self.artifacts.as_ref().cloned() else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -353,6 +369,38 @@ impl AppBuilder {
|
|||||||
.context("Failed to get patch cache")
|
.context("Failed to get patch cache")
|
||||||
.unwrap();
|
.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
|
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
|
||||||
self.abort_all(BuildStage::Restarting);
|
self.abort_all(BuildStage::Restarting);
|
||||||
self.build_task = tokio::spawn({
|
self.build_task = tokio::spawn({
|
||||||
@@ -362,9 +410,12 @@ impl AppBuilder {
|
|||||||
tx: self.tx.clone(),
|
tx: self.tx.clone(),
|
||||||
mode: BuildMode::Thin {
|
mode: BuildMode::Thin {
|
||||||
changed_files,
|
changed_files,
|
||||||
rustc_args: artifacts.direct_rustc,
|
changed_crates,
|
||||||
|
modified_crates: self.modified_crates.clone(),
|
||||||
|
workspace_rustc_args: artifacts.workspace_rustc_args,
|
||||||
aslr_reference,
|
aslr_reference,
|
||||||
cache,
|
cache,
|
||||||
|
object_cache: self.object_cache.clone(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
async move { request.build(&ctx).await }
|
async move { request.build(&ctx).await }
|
||||||
@@ -378,6 +429,10 @@ impl AppBuilder {
|
|||||||
self.abort_all(BuildStage::Restarting);
|
self.abort_all(BuildStage::Restarting);
|
||||||
self.artifacts.take();
|
self.artifacts.take();
|
||||||
self.patch_cache.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({
|
self.build_task = tokio::spawn({
|
||||||
let request = self.build.clone();
|
let request = self.build.clone();
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
@@ -545,14 +600,23 @@ impl AppBuilder {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's any CARGO vars in the rustc_wrapper files, push those too
|
// 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()) {
|
// Read from any per-crate args file in the directory (they all share the same CARGO_ envs).
|
||||||
if let Ok(res) = serde_json::from_str::<RustcArgs>(&res) {
|
if let Ok(entries) = std::fs::read_dir(self.build.rustc_wrapper_args_dir()) {
|
||||||
for (key, value) in res.envs {
|
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_") {
|
if key.starts_with("CARGO_") {
|
||||||
envs.push((key, value));
|
envs.push((key, value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break; // Only need one file for CARGO_ env vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,17 +804,21 @@ impl AppBuilder {
|
|||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
|
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
|
||||||
changed_file
|
changed_file
|
||||||
.display()
|
.strip_prefix(self.build.workspace_dir())
|
||||||
.to_string()
|
.unwrap_or(changed_file)
|
||||||
.trim_start_matches(&self.build.crate_dir().display().to_string()),
|
.display(),
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(res.time_start)
|
.duration_since(res.time_start)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_millis()
|
.as_millis()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Commit this patch
|
||||||
self.patches.push(jump_table.clone());
|
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)
|
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 assets;
|
||||||
mod builder;
|
mod builder;
|
||||||
|
mod cache;
|
||||||
mod context;
|
mod context;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
mod patch;
|
mod patch;
|
||||||
@@ -19,6 +20,7 @@ mod tools;
|
|||||||
|
|
||||||
pub(crate) use assets::*;
|
pub(crate) use assets::*;
|
||||||
pub(crate) use builder::*;
|
pub(crate) use builder::*;
|
||||||
|
pub(crate) use cache::*;
|
||||||
pub(crate) use context::*;
|
pub(crate) use context::*;
|
||||||
pub(crate) use manifest::*;
|
pub(crate) use manifest::*;
|
||||||
pub(crate) use patch::*;
|
pub(crate) use patch::*;
|
||||||
|
|||||||
+100
-20
@@ -4,7 +4,7 @@ use object::{
|
|||||||
macho::{self},
|
macho::{self},
|
||||||
read::File,
|
read::File,
|
||||||
write::{MachOBuildVersion, SectionId, StandardSection, Symbol, SymbolId, SymbolSection},
|
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 rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -76,6 +76,16 @@ pub struct HotpatchModuleCache {
|
|||||||
|
|
||||||
// ... native stuff
|
// ... native stuff
|
||||||
pub symbol_table: HashMap<String, CachedSymbol>,
|
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 {
|
pub struct CachedSymbol {
|
||||||
@@ -277,10 +287,55 @@ impl HotpatchModuleCache {
|
|||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.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 {
|
HotpatchModuleCache {
|
||||||
symbol_table,
|
symbol_table,
|
||||||
path: original.to_path_buf(),
|
path: original.to_path_buf(),
|
||||||
old_bytes,
|
old_bytes,
|
||||||
|
tls_init_data,
|
||||||
|
tls_init_sizes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1106,36 +1161,61 @@ pub fn create_undefined_symbol_stub(
|
|||||||
PointerWidth::U64 => 8,
|
PointerWidth::U64 => 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
let size = if sym.size == 0 {
|
// Resolve the TLS init data offset and size.
|
||||||
pointer_width
|
//
|
||||||
|
// 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 {
|
} else {
|
||||||
sym.size
|
// Last resort (ELF with size=0): use pointer width
|
||||||
|
(sym.address, pointer_width)
|
||||||
};
|
};
|
||||||
|
|
||||||
let align = size.min(pointer_width).next_power_of_two();
|
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
|
let start = tls_offset as usize;
|
||||||
init.iter_mut()
|
let end = start + size as usize;
|
||||||
.zip(match triple.endianness() {
|
let init = if end <= cache.tls_init_data.len() {
|
||||||
Ok(target_lexicon::Endianness::Little) => abs_addr.to_le_bytes(),
|
cache.tls_init_data[start..end].to_vec()
|
||||||
Ok(target_lexicon::Endianness::Big) => abs_addr.to_be_bytes(),
|
} else {
|
||||||
_ => return Err(PatchError::UnsupportedPlatform(triple.to_string())),
|
// Beyond .tdata bounds (.tbss) or Mach-O fallback: zero-init
|
||||||
})
|
vec![0u8; size as usize]
|
||||||
.for_each(|(b, v)| *b = v);
|
};
|
||||||
|
|
||||||
let offset = obj.append_section_data(tls_section, &init, align);
|
// 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,
|
||||||
obj.add_symbol(Symbol {
|
// 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(),
|
name: name.as_bytes()[name_offset..].to_vec(),
|
||||||
value: offset, // offset inside .tdata
|
value: 0,
|
||||||
size,
|
size: 0,
|
||||||
scope: SymbolScope::Linkage,
|
scope: SymbolScope::Linkage,
|
||||||
kind: SymbolKind::Tls,
|
kind: SymbolKind::Tls,
|
||||||
weak: false,
|
weak: false,
|
||||||
section: SymbolSection::Section(tls_section),
|
section: SymbolSection::Undefined,
|
||||||
flags: SymbolFlags::None, // ignore for these stubs
|
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)
|
// We just assume all non-text symbols are data (globals, statics, etc)
|
||||||
|
|||||||
@@ -321,8 +321,8 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
|
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
|
||||||
LinkAction, LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc,
|
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
|
||||||
WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use cargo_metadata::diagnostic::Diagnostic;
|
use cargo_metadata::diagnostic::Diagnostic;
|
||||||
@@ -339,7 +339,7 @@ use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{borrow::Cow, ffi::OsString};
|
use std::{borrow::Cow, ffi::OsString};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Stdio,
|
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.
|
/// - 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
|
/// 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.
|
/// generates *much* faster than a regular base or fat build.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum BuildMode {
|
pub enum BuildMode {
|
||||||
/// A normal build generated using `cargo rustc`
|
/// 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
|
/// A "thin" build generated with `rustc` directly and dx as a custom linker
|
||||||
Thin {
|
Thin {
|
||||||
rustc_args: RustcArgs,
|
/// List of changed files causing this rebuild. Mostly used for diagnostics
|
||||||
changed_files: Vec<PathBuf>,
|
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,
|
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>,
|
cache: Arc<HotpatchModuleCache>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -450,7 +468,7 @@ pub enum BuildMode {
|
|||||||
pub struct BuildArtifacts {
|
pub struct BuildArtifacts {
|
||||||
pub(crate) root_dir: PathBuf,
|
pub(crate) root_dir: PathBuf,
|
||||||
pub(crate) exe: 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_start: SystemTime,
|
||||||
pub(crate) time_end: SystemTime,
|
pub(crate) time_end: SystemTime,
|
||||||
pub(crate) assets: AssetManifest,
|
pub(crate) assets: AssetManifest,
|
||||||
@@ -458,6 +476,7 @@ pub struct BuildArtifacts {
|
|||||||
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
|
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
|
||||||
pub(crate) depinfo: RustcDepInfo,
|
pub(crate) depinfo: RustcDepInfo,
|
||||||
pub(crate) build_id: BuildId,
|
pub(crate) build_id: BuildId,
|
||||||
|
pub(crate) object_cache: ObjectCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildRequest {
|
impl BuildRequest {
|
||||||
@@ -1018,7 +1037,7 @@ impl BuildRequest {
|
|||||||
// Create the session cache directory
|
// Create the session cache directory
|
||||||
let cache_dir = self.session_cache_dir();
|
let cache_dir = self.session_cache_dir();
|
||||||
_ = std::fs::create_dir_all(&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_err_file());
|
||||||
_ = std::fs::File::create_new(self.link_args_file());
|
_ = std::fs::File::create_new(self.link_args_file());
|
||||||
_ = std::fs::File::create_new(self.windows_command_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
|
// We need to make sure to not react to this though, so the filemap must cache it
|
||||||
_ = self.bust_fingerprint(ctx);
|
_ = 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?;
|
let mut artifacts = self.cargo_build(ctx).await?;
|
||||||
|
|
||||||
// Write the build artifacts to the bundle on the disk
|
// Write the build artifacts to the bundle on the disk
|
||||||
@@ -1062,10 +1082,10 @@ impl BuildRequest {
|
|||||||
BuildMode::Thin {
|
BuildMode::Thin {
|
||||||
aslr_reference,
|
aslr_reference,
|
||||||
cache,
|
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?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,7 +1096,7 @@ impl BuildRequest {
|
|||||||
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
|
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
|
||||||
.await
|
.await
|
||||||
.context("Failed to write executable")?;
|
.context("Failed to write executable")?;
|
||||||
self.write_frameworks(ctx, &artifacts.direct_rustc)
|
self.write_frameworks(ctx, &artifacts)
|
||||||
.await
|
.await
|
||||||
.context("Failed to write frameworks")?;
|
.context("Failed to write frameworks")?;
|
||||||
self.write_assets(ctx, &artifacts.assets)
|
self.write_assets(ctx, &artifacts.assets)
|
||||||
@@ -1125,7 +1145,6 @@ impl BuildRequest {
|
|||||||
|
|
||||||
Ok(artifacts)
|
Ok(artifacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the cargo build by assembling the build command and executing it.
|
/// 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
|
/// 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> {
|
async fn cargo_build(&self, ctx: &BuildContext) -> Result<BuildArtifacts> {
|
||||||
let time_start = SystemTime::now();
|
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
|
// 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
|
// "Thin" builds only build the final exe, so we only need to build one crate
|
||||||
let crate_count = match ctx.mode {
|
let crate_count = match ctx.mode {
|
||||||
@@ -1246,13 +1269,32 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate the rustc args from the wrapper, if they exist and can be parsed.
|
// Load per-crate rustc args from the wrapper directory.
|
||||||
let mut direct_rustc = RustcArgs::default();
|
// Each workspace crate compiled through the wrapper has its own JSON file:
|
||||||
if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file()) {
|
// "{crate_name}.lib.json" (key: "{crate_name}.lib") for lib targets and
|
||||||
if let Ok(res) = serde_json::from_str(&res) {
|
// "{crate_name}.bin.json" (key: "{crate_name}.bin") for bin targets.
|
||||||
direct_rustc = res;
|
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 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 let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file()) {
|
||||||
@@ -1265,12 +1307,16 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect the linker args from the and update the rustc args
|
// Collect the linker args and attach them to the tip crate's bin entry
|
||||||
direct_rustc.link_args = std::fs::read_to_string(self.link_args_file())
|
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")?
|
.context("Failed to read link args from file")?
|
||||||
.lines()
|
.lines()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
|
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) {
|
if matches!(ctx.mode, BuildMode::Fat) {
|
||||||
ctx.status_starting_link();
|
ctx.status_starting_link();
|
||||||
let link_start = SystemTime::now();
|
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!(
|
tracing::debug!(
|
||||||
"Fat linking completed in {}us",
|
"Fat linking completed in {}us",
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
@@ -1302,7 +1355,7 @@ impl BuildRequest {
|
|||||||
Ok(BuildArtifacts {
|
Ok(BuildArtifacts {
|
||||||
time_end,
|
time_end,
|
||||||
exe,
|
exe,
|
||||||
direct_rustc,
|
workspace_rustc_args,
|
||||||
time_start,
|
time_start,
|
||||||
assets,
|
assets,
|
||||||
mode,
|
mode,
|
||||||
@@ -1310,9 +1363,122 @@ impl BuildRequest {
|
|||||||
root_dir: self.root_dir(),
|
root_dir: self.root_dir(),
|
||||||
patch_cache: None,
|
patch_cache: None,
|
||||||
build_id: ctx.build_id,
|
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
|
/// Collect the assets from the final executable and modify the binary in place to point to the right
|
||||||
/// hashed asset location.
|
/// hashed asset location.
|
||||||
async fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
|
async fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
|
||||||
@@ -1421,9 +1587,20 @@ impl BuildRequest {
|
|||||||
Ok(())
|
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();
|
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
|
// 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 = AndroidTools::openssl_lib_dir(&self.triple);
|
||||||
let openssl_dir_disp = openssl_dir.display().to_string();
|
let openssl_dir_disp = openssl_dir.display().to_string();
|
||||||
@@ -1633,17 +1810,66 @@ impl BuildRequest {
|
|||||||
/// manually do any linking.
|
/// manually do any linking.
|
||||||
///
|
///
|
||||||
/// We also run some post processing steps here, like extracting out any new assets.
|
/// 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(
|
async fn write_patch(
|
||||||
&self,
|
&self,
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
aslr_reference: u64,
|
aslr_reference: u64,
|
||||||
artifacts: &mut BuildArtifacts,
|
artifacts: &mut BuildArtifacts,
|
||||||
cache: &Arc<HotpatchModuleCache>,
|
cache: &Arc<HotpatchModuleCache>,
|
||||||
rustc_args: &RustcArgs,
|
modified_crates: &HashSet<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
ctx.status_hotpatching();
|
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.
|
// Extract out the incremental object files.
|
||||||
//
|
//
|
||||||
@@ -1698,12 +1924,21 @@ impl BuildRequest {
|
|||||||
// -Wl,-all_load
|
// -Wl,-all_load
|
||||||
// ```
|
// ```
|
||||||
let mut dylibs = vec![];
|
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()
|
.iter()
|
||||||
.filter(|arg| arg.ends_with(".rcgu.o"))
|
.filter(|arg| arg.ends_with(".rcgu.o"))
|
||||||
.sorted()
|
.sorted()
|
||||||
.map(PathBuf::from)
|
.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
|
// On non-wasm platforms, we generate a special shim object file which converts symbols from
|
||||||
// fat binary into direct addresses from the running process.
|
// fat binary into direct addresses from the running process.
|
||||||
@@ -1734,7 +1969,7 @@ impl BuildRequest {
|
|||||||
|
|
||||||
// Add the dylibs/sos to the linker args
|
// 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.
|
// 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") {
|
if arg.ends_with(".dylib") || arg.ends_with(".so") {
|
||||||
let path = PathBuf::from(arg);
|
let path = PathBuf::from(arg);
|
||||||
dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
|
dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
|
||||||
@@ -1755,7 +1990,7 @@ impl BuildRequest {
|
|||||||
let mut out_args: Vec<OsString> = vec![];
|
let mut out_args: Vec<OsString> = vec![];
|
||||||
out_args.extend(object_files.iter().map(Into::into));
|
out_args.extend(object_files.iter().map(Into::into));
|
||||||
out_args.extend(dylibs.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));
|
out_args.extend(out_arg.iter().map(Into::into));
|
||||||
|
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
@@ -1769,7 +2004,7 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add more search paths for the linker
|
// 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
|
// On linux, we need to set a more complete PATH for the linker to find its libraries
|
||||||
if cfg!(target_os = "linux") {
|
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
|
// 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.
|
// can just remove the one in the deps dir and the problem goes away.
|
||||||
if let Some(idx) = args.iter().position(|arg| *arg == "-o") {
|
if let Some(idx) = args.link_args.iter().position(|arg| *arg == "-o") {
|
||||||
_ = std::fs::remove_file(PathBuf::from(args[idx + 1].as_str()));
|
_ = std::fs::remove_file(PathBuf::from(args.link_args[idx + 1].as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now extract the assets from the fat binary
|
// Now extract the assets from the fat binary
|
||||||
@@ -1821,9 +2056,9 @@ impl BuildRequest {
|
|||||||
self.write_index_html(&artifacts.assets)
|
self.write_index_html(&artifacts.assets)
|
||||||
.context("Failed to write index.html")?;
|
.context("Failed to write index.html")?;
|
||||||
|
|
||||||
// Clean up the temps manually
|
// Clean up temp object files (tip incremental objects + stub.o).
|
||||||
// todo: we might want to keep them around for debugging purposes
|
// Cached dep objects in object_cache/ are NOT deleted — they persist across patches.
|
||||||
for file in object_files {
|
for file in &temp_objects {
|
||||||
_ = std::fs::remove_file(file);
|
_ = 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.
|
// 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?
|
// 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");
|
let mut cmd = Command::new("rustc");
|
||||||
cmd.current_dir(self.workspace_dir());
|
cmd.current_dir(self.workspace_dir());
|
||||||
cmd.env_clear();
|
cmd.env_clear();
|
||||||
@@ -2533,13 +2775,11 @@ impl BuildRequest {
|
|||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Base and Fat builds, we use a regular cargo setup, but we might need to intercept
|
// For Base and Fat builds, we use a regular cargo setup, but we intercept rustc for
|
||||||
// rustc itself in case we're hot-patching and need a reliable rustc environment to
|
// workspace member crates to capture their args/envs for hot-patching.
|
||||||
// continuously recompile the top-level crate with.
|
|
||||||
//
|
//
|
||||||
// In the future, when we support hot-patching *all* workspace crates, we will need to
|
// We use RUSTC_WORKSPACE_WRAPPER which wraps only workspace member crates, letting us
|
||||||
// make use of the RUSTC_WORKSPACE_WRAPPER environment variable instead of RUSTC_WRAPPER
|
// capture per-crate args without interfering with external dependency compilation.
|
||||||
// and then keep track of env and args on a per-crate basis.
|
|
||||||
//
|
//
|
||||||
// We've also had a number of issues with incorrect canonicalization when passing paths
|
// 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.
|
// 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)));
|
.envs(env.iter().map(|(k, v)| (k.as_ref(), v)));
|
||||||
|
|
||||||
if matches!(build_mode, BuildMode::Fat | BuildMode::Base { run: true }) {
|
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(
|
cmd.env(
|
||||||
DX_RUSTC_WRAPPER_ENV_VAR,
|
DX_RUSTC_WRAPPER_ENV_VAR,
|
||||||
dunce::canonicalize(self.rustc_wrapper_args_file())
|
dunce::canonicalize(&args_dir)
|
||||||
.context("Failed to canonicalize rustc wrapper args file")?
|
.context("Failed to canonicalize rustc wrapper args dir")?
|
||||||
.display()
|
.display()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
cmd.env(
|
cmd.env(
|
||||||
"RUSTC_WRAPPER",
|
"RUSTC_WORKSPACE_WRAPPER",
|
||||||
Workspace::path_to_dx()?.display().to_string(),
|
Workspace::path_to_dx()?.display().to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3488,8 +3731,13 @@ impl BuildRequest {
|
|||||||
self.session_cache_dir.join(self.bundle.to_string())
|
self.session_cache_dir.join(self.bundle.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn rustc_wrapper_args_file(&self) -> PathBuf {
|
pub(crate) fn rustc_wrapper_args_dir(&self) -> PathBuf {
|
||||||
self.session_cache_dir().join("rustc_wrapper_args.txt")
|
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 {
|
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.
|
/// 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
|
/// 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.
|
/// 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?)
|
/// 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.
|
/// but that would require relying on cargo internals.
|
||||||
///
|
///
|
||||||
/// This might stop working if/when cargo stabilizes contents-based fingerprinting.
|
/// 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<()> {
|
fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
|
||||||
if matches!(ctx.mode, BuildMode::Fat) {
|
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
|
let fingerprint_dir = self
|
||||||
.target_dir
|
.target_dir
|
||||||
.join(self.triple.to_string())
|
.join(self.triple.to_string())
|
||||||
.join(&self.profile)
|
.join(&self.profile)
|
||||||
.join(".fingerprint");
|
.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
|
// split at the last `-` used to separate the hash from the name
|
||||||
// This causes to more aggressively bust hashes for all combinations of features
|
// This causes to more aggressively bust hashes for all combinations of features
|
||||||
// and fingerprints for this package since we're just ignoring the hash
|
// and fingerprints for this package since we're just ignoring the hash
|
||||||
for entry in std::fs::read_dir(&fingerprint_dir)?.flatten() {
|
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(fname) = entry.file_name().to_str() {
|
||||||
if let Some((name, _)) = fname.rsplit_once('-') {
|
if let Some((name, _)) = fname.rsplit_once('-') {
|
||||||
if name == self.package().name {
|
if busted.contains(name) {
|
||||||
_ = std::fs::remove_dir_all(entry.path());
|
_ = std::fs::remove_dir_all(entry.path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,12 +209,24 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
|||||||
|
|
||||||
impl BuildArtifacts {
|
impl BuildArtifacts {
|
||||||
pub(crate) fn into_structured_output(self) -> StructuredBuildArtifacts {
|
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 {
|
StructuredBuildArtifacts {
|
||||||
path: self.root_dir,
|
path: self.root_dir,
|
||||||
exe: self.exe,
|
exe: self.exe,
|
||||||
rustc_args: self.direct_rustc.args,
|
rustc_args: tip_args.args,
|
||||||
rustc_envs: self.direct_rustc.envs,
|
rustc_envs: tip_args.envs,
|
||||||
link_args: self.direct_rustc.link_args,
|
link_args: tip_args.link_args,
|
||||||
assets: self.assets.unique_assets().cloned().collect(),
|
assets: self.assets.unique_assets().cloned().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use crate::{
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
||||||
use std::io::Read;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::{collections::HashMap, io::Read};
|
||||||
|
|
||||||
const HELP_HEADING: &str = "Hotpatching a binary";
|
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?
|
// consider a shared-mem approach or a binary serializer? something like arrow / parquet / bincode?
|
||||||
let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?);
|
let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?);
|
||||||
|
|
||||||
let mode = BuildMode::Thin {
|
let tip_crate_name = request.main_target.replace('-', "_");
|
||||||
rustc_args: crate::RustcArgs {
|
let mut workspace_rustc_args = HashMap::new();
|
||||||
|
workspace_rustc_args.insert(
|
||||||
|
format!("{tip_crate_name}.bin"),
|
||||||
|
crate::RustcArgs {
|
||||||
args: rustc_args,
|
args: rustc_args,
|
||||||
envs: rustc_envs,
|
envs: rustc_envs,
|
||||||
link_args,
|
link_args,
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mode = BuildMode::Thin {
|
||||||
|
workspace_rustc_args,
|
||||||
changed_files: vec![],
|
changed_files: vec![],
|
||||||
|
changed_crates: vec![],
|
||||||
|
modified_crates: std::collections::HashSet::new(),
|
||||||
aslr_reference: self.aslr_reference,
|
aslr_reference: self.aslr_reference,
|
||||||
cache: cache.clone(),
|
cache: cache.clone(),
|
||||||
|
object_cache: crate::ObjectCache::new(&request.session_cache_dir()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let artifacts = AppBuilder::started(request, mode, build_id)?
|
let artifacts = AppBuilder::started(request, mode, build_id)?
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ use std::{
|
|||||||
process::ExitCode,
|
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";
|
pub const DX_RUSTC_WRAPPER_ENV_VAR: &str = "DX_RUSTC";
|
||||||
|
|
||||||
/// Is `dx` being used as a rustc wrapper?
|
/// Is `dx` being used as a rustc wrapper?
|
||||||
@@ -64,7 +65,7 @@ fn has_linking_args() -> bool {
|
|||||||
false
|
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>
|
/// <https://doc.rust-lang.org/cargo/reference/config.html#buildrustc>
|
||||||
pub fn run_rustc() -> ExitCode {
|
pub fn run_rustc() -> ExitCode {
|
||||||
@@ -75,11 +76,11 @@ pub fn run_rustc() -> ExitCode {
|
|||||||
.run_link();
|
.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")
|
.expect("DX_RUSTC env var must be set")
|
||||||
.into();
|
.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.
|
// We skip our own executable name (`wrapper-name`) to get the args passed to us.
|
||||||
let captured_args = args().skip(1).collect::<Vec<_>>();
|
let captured_args = args().skip(1).collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -89,32 +90,51 @@ pub fn run_rustc() -> ExitCode {
|
|||||||
link_args: Default::default(),
|
link_args: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Another terrible hack to avoid caching non-sensical args when
|
// Extract the crate name from the args to use as the filename.
|
||||||
// a build is completely fresh (rustc is invoked with --crate-name ___)
|
// Skip non-sensical args when a build is completely fresh (rustc is invoked with --crate-name ___)
|
||||||
if rustc_args
|
let crate_name = rustc_args
|
||||||
.args
|
.args
|
||||||
.iter()
|
.iter()
|
||||||
.skip_while(|arg| *arg != "--crate-name")
|
.skip_while(|arg| *arg != "--crate-name")
|
||||||
|
.nth(1);
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
let crate_type = rustc_args
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.skip_while(|arg| *arg != "--crate-type")
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.is_some_and(|name| name != "___")
|
.map(|s| s.as_str());
|
||||||
{
|
|
||||||
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");
|
|
||||||
|
|
||||||
let serialized_args =
|
let serialized_args =
|
||||||
serde_json::to_string(&rustc_args).expect("Failed to serialize rustc args");
|
serde_json::to_string(&rustc_args).expect("Failed to serialize rustc args");
|
||||||
|
|
||||||
std::fs::write(&var_file, serialized_args).expect("Failed to write rustc args to file");
|
// 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.
|
// Run the actual rustc command.
|
||||||
// We want all stdout/stderr to be inherited, so the user sees the compiler output.
|
// We want all stdout/stderr to be inherited, so the user sees the compiler output.
|
||||||
let mut cmd = std::process::Command::new("rustc");
|
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.
|
// when passing arguments to the `rustc` command we are spawning.
|
||||||
cmd.args(captured_args.iter().skip(1));
|
cmd.args(captured_args.iter().skip(1));
|
||||||
cmd.envs(rustc_args.envs);
|
cmd.envs(rustc_args.envs);
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ use notify::{
|
|||||||
Config, EventKind, RecursiveMode, Watcher as NotifyWatcher,
|
Config, EventKind, RecursiveMode, Watcher as NotifyWatcher,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
net::{IpAddr, TcpListener},
|
net::{IpAddr, TcpListener},
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
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.
|
// 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 needs_full_rebuild && self.automatic_rebuilds {
|
||||||
if self.use_hotpatch_engine {
|
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() {
|
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_hot_reload_changes();
|
||||||
self.clear_cached_rsx();
|
self.clear_cached_rsx();
|
||||||
@@ -1163,6 +1167,152 @@ impl AppServer {
|
|||||||
krates
|
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.
|
/// 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 {
|
pub(crate) fn is_fullstack(&self) -> bool {
|
||||||
self.fullstack
|
self.fullstack
|
||||||
|
|||||||
Reference in new issue
Block a user