feat: copy frameworks to Frameworks folder, detect linkers, custom target dirs (#4174)

* feat: copy frameworks to Frameworks folder

* add dylib linking to patching

* remove logging

* rename __aslr_offset

* fix android/linux

* support linker flavor

* support custom target dirs

* custom cargo target dir support

* whoops ios

* dlsym doesn't exist on windows

* look for the right main symbol per platform

* distinction between mains

* export main on windows

* enbable rlib caching

* add dx hash

* adjust linking warnings

* update tui with hotreload state

* implement symbol flags

* add flags

* only run ranlib on darwin

* enable lld

* paths

* add harness test for tls

* wip: revert launch mobile

* wip

* fix fullstack mobile

* fix a few bugs
- long paths?
- reverting back to original

* pass-thru fuse-ld args

* fix nan in render, fix empty archives

* consider existing dep files if archive exists

* fmt

* drop msrv
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-24 22:41:30 +01:00
1 parent 751f1920c3
commit 0982c1ff8b
25 files changed
+1044 -315

No files matched your search

Generated
+30
View File
@@ -2022,6 +2022,18 @@ dependencies = [
"serde",
]
[[package]]
name = "cargo-config2"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dc3749a36e0423c991f1e7a3e4ab0c36a1f489658313db4b187d401d79cc461"
dependencies = [
"serde",
"serde_derive",
"toml_edit 0.22.26",
"windows-sys 0.59.0",
]
[[package]]
name = "cargo-generate"
version = "0.23.3"
@@ -2943,6 +2955,14 @@ dependencies = [
"itertools 0.10.5",
]
[[package]]
name = "cross-tls-crate"
version = "0.1.0"
[[package]]
name = "cross-tls-crate-dylib"
version = "0.1.0"
[[package]]
name = "crossbeam"
version = "0.8.4"
@@ -3662,6 +3682,7 @@ dependencies = [
"backtrace",
"brotli 7.0.0",
"built",
"cargo-config2",
"cargo-generate",
"cargo_metadata",
"cargo_toml",
@@ -13468,6 +13489,15 @@ dependencies = [
"web-sys",
]
[[package]]
name = "subsecond-tls-harness"
version = "0.1.0"
dependencies = [
"cross-tls-crate",
"cross-tls-crate-dylib",
"dioxus-devtools",
]
[[package]]
name = "subsecond-types"
version = "0.7.0-alpha.0"
+3
View File
@@ -94,6 +94,9 @@ members = [
# subsecond
"packages/subsecond/subsecond",
"packages/subsecond/subsecond-types",
"packages/subsecond/subsecond-tests/cross-tls-crate",
"packages/subsecond/subsecond-tests/cross-tls-crate-dylib",
"packages/subsecond/subsecond-tests/cross-tls-test",
# Full project examples
"example-projects/fullstack-hackernews",
+1
View File
@@ -138,6 +138,7 @@ wasm-bindgen-externref-xform = "0.2.100"
pdb = "0.8.0"
self_update = { version = "0.42.0", features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate"] }
self-replace = "1.5.0"
cargo-config2 = { workspace = true }
[build-dependencies]
built = { version = "0.7.5", features = ["git2"] }
+2 -2
View File
@@ -208,12 +208,12 @@ impl AppBuilder {
self.bundling_progress = 0.0;
}
BuildStage::Starting { crate_count, .. } => {
self.expected_crates = *crate_count;
self.expected_crates = *crate_count.max(&1);
}
BuildStage::InstallingTooling => {}
BuildStage::Compiling { current, total, .. } => {
self.compiled_crates = *current;
self.expected_crates = *total;
self.expected_crates = *total.max(&1);
if self.compile_start.is_none() {
self.compile_start = Some(Instant::now());
+70 -40
View File
@@ -3,8 +3,8 @@ use itertools::Itertools;
use object::{
macho::{self},
read::File,
write::{MachOBuildVersion, StandardSection, Symbol, SymbolSection},
Endianness, Object, ObjectSymbol, SymbolKind, SymbolScope,
write::{MachOBuildVersion, SectionId, StandardSection, Symbol, SymbolId, SymbolSection},
Endianness, Object, ObjectSymbol, SymbolFlags, SymbolKind, SymbolScope,
};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::{
@@ -84,6 +84,7 @@ pub struct CachedSymbol {
pub is_undefined: bool,
pub is_weak: bool,
pub size: u64,
pub flags: SymbolFlags<SectionId, SymbolId>,
}
impl PartialEq for HotpatchModuleCache {
@@ -139,6 +140,7 @@ impl HotpatchModuleCache {
is_undefined,
is_weak: false,
size: 0,
flags: SymbolFlags::None,
},
);
}
@@ -158,6 +160,7 @@ impl HotpatchModuleCache {
is_undefined,
is_weak: false,
size: 0,
flags: SymbolFlags::None,
},
);
}
@@ -252,6 +255,15 @@ impl HotpatchModuleCache {
let symbol_table = obj
.symbols()
.filter_map(|s| {
let flags = match s.flags() {
SymbolFlags::None => SymbolFlags::None,
SymbolFlags::Elf { st_info, st_other } => {
SymbolFlags::Elf { st_info, st_other }
}
SymbolFlags::MachO { n_desc } => SymbolFlags::MachO { n_desc },
_ => SymbolFlags::None,
};
Some((
s.name().ok()?.to_string(),
CachedSymbol {
@@ -260,6 +272,7 @@ impl HotpatchModuleCache {
is_weak: s.is_weak(),
kind: s.kind(),
size: s.size(),
flags,
},
))
})
@@ -326,9 +339,9 @@ fn create_windows_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Resul
.context("failed to find 'main' symbol in patch")?;
let aslr_reference = old_name_to_addr
.get("__aslr_reference")
.get("main")
.map(|s| s.address)
.context("failed to find '_aslr_reference' symbol in original module")?;
.context("failed to find '_main' symbol in original module")?;
Ok(JumpTable {
lib: patch.to_path_buf(),
@@ -371,31 +384,15 @@ fn create_native_jump_table(
}
}
let new_base_address = match triple.operating_system {
// The symbol in the symtab is called "_main" but in the dysymtab it is called "main"
OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
*new_name_to_addr
.get("_main")
.context("failed to find '_main' symbol in patch")?
}
// No distincation between the two on these platforms
OperatingSystem::Freebsd
| OperatingSystem::Openbsd
| OperatingSystem::Linux
| OperatingSystem::Windows => *new_name_to_addr
.get("main")
.context("failed to find 'main' symbol in patch")?,
// On wasm, it doesn't matter what the address is since the binary is PIC
_ => 0,
};
let sentinel = main_sentinel(triple);
let new_base_address = new_name_to_addr
.get(sentinel)
.cloned()
.context("failed to find 'main' symbol in base - are deubg symbols enabled?")?;
let aslr_reference = old_name_to_addr
.get("___aslr_reference")
.or_else(|| old_name_to_addr.get("__aslr_reference"))
.get(sentinel)
.map(|s| s.address)
.context("failed to find '___aslr_reference' symbol in original module")?;
.context("failed to find 'main' symbol in original module - are debug symbols enabled?")?;
Ok(JumpTable {
lib: patch.to_path_buf(),
@@ -831,22 +828,31 @@ pub fn create_undefined_symbol_stub(
_ => {}
}
let symbol_table = &cache.symbol_table;
// Get the offset from the main module and adjust the addresses by the slide;
let aslr_ref_address = cache
.symbol_table
.get(main_sentinel(triple))
.context("failed to find '_main' symbol in patch")?
.address;
if aslr_reference < aslr_ref_address {
return Err(PatchError::InvalidModule(
format!(
"ASLR reference is less than the main module's address - is there a `main`?. {:x} < {:x}", aslr_reference, aslr_ref_address )
));
}
// Get the offset from the main module and adjust the addresses by the slide
let aslr_ref_address = symbol_table
.get("___aslr_reference")
.or_else(|| symbol_table.get("__aslr_reference"))
.map(|s| s.address)
.context("Failed to find ___aslr_reference symbol")?;
let aslr_offset = aslr_reference - aslr_ref_address;
// we need to assemble a PLT/GOT so direct calls to the patch symbols work
// for each symbol we either write the address directly (as a symbol) or create a PLT/GOT entry
let text_section = obj.section_id(StandardSection::Text);
for name in undefined_symbols {
let Some(sym) = symbol_table.get(name.as_str().trim_start_matches("__imp_")) else {
tracing::error!("Symbol not found: {}", name);
let Some(sym) = cache
.symbol_table
.get(name.as_str().trim_start_matches("__imp_"))
else {
tracing::debug!("Symbol not found: {}", name);
continue;
};
@@ -899,7 +905,7 @@ pub fn create_undefined_symbol_stub(
kind: SymbolKind::Data, // Always Data for IAT entries
weak: false,
section: SymbolSection::Section(data_section),
flags: object::SymbolFlags::None,
flags: SymbolFlags::None,
});
}
@@ -1028,7 +1034,7 @@ pub fn create_undefined_symbol_stub(
kind: SymbolKind::Text,
weak: false,
section: SymbolSection::Section(text_section),
flags: object::SymbolFlags::None,
flags: SymbolFlags::None, // ignore for these stubs
});
}
@@ -1097,7 +1103,7 @@ pub fn create_undefined_symbol_stub(
kind: SymbolKind::Tls,
weak: false,
section: SymbolSection::Section(tls_section),
flags: object::SymbolFlags::None,
flags: SymbolFlags::None, // ignore for these stubs
});
}
@@ -1108,6 +1114,15 @@ pub fn create_undefined_symbol_stub(
SymbolKind::Unknown => SymbolKind::Data,
k => k,
};
// plain linux *wants* these flags, but android doesn't.
// unsure what's going on here, but this is special cased for now.
// I think the more advanced linkers don't want these flags, but the default linux linker (ld) does.
let flags = match triple.environment {
target_lexicon::Environment::Android => SymbolFlags::None,
_ => sym.flags,
};
obj.add_symbol(Symbol {
name: name.as_bytes()[name_offset..].to_vec(),
value: abs_addr,
@@ -1116,7 +1131,7 @@ pub fn create_undefined_symbol_stub(
kind,
weak: sym.is_weak,
section: SymbolSection::Absolute,
flags: object::SymbolFlags::None,
flags,
});
}
}
@@ -1439,3 +1454,18 @@ fn parse_module_with_ids(bindgened: &[u8]) -> Result<ParsedModule> {
symbols,
})
}
/// Get the main sentinel symbol for the given target triple
///
/// We need to special case darwin since `main` is the entrypoint but `_main` is the actual symbol.
/// The entrypoint ends up outside the text section, seemingly, and breaks our aslr detection.
fn main_sentinel(triple: &Triple) -> &'static str {
match triple.operating_system {
// The symbol in the symtab is called "_main" but in the dysymtab it is called "main"
OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
"_main"
}
_ => "main",
}
}
+319 -115
View File
@@ -316,8 +316,9 @@
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
use crate::{
AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, Platform, Result, RustcArgs,
TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, LinkerFlavor, Platform, Result,
RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace,
DX_RUSTC_WRAPPER_ENV_VAR,
};
use anyhow::Context;
use cargo_metadata::diagnostic::Diagnostic;
@@ -377,7 +378,7 @@ pub(crate) struct BuildRequest {
pub(crate) extra_cargo_args: Vec<String>,
pub(crate) extra_rustc_args: Vec<String>,
pub(crate) no_default_features: bool,
pub(crate) custom_target_dir: Option<PathBuf>,
pub(crate) target_dir: PathBuf,
pub(crate) skip_assets: bool,
pub(crate) wasm_split: bool,
pub(crate) debug_symbols: bool,
@@ -643,12 +644,26 @@ impl BuildRequest {
},
};
let custom_linker = if platform == Platform::Android {
// Somethings we override are also present in the user's config.
// If we can't get them by introspecting cargo, then we need to get them from the config
let cargo_config = cargo_config2::Config::load().unwrap();
let mut custom_linker = if platform == Platform::Android {
Some(workspace.android_tools()?.android_cc(&triple))
} else {
None
};
if let Ok(Some(linker)) = cargo_config.linker(triple.to_string()) {
custom_linker = Some(linker);
}
let target_dir = std::env::var("CARGO_TARGET_DIR")
.ok()
.map(PathBuf::from)
.or_else(|| cargo_config.build.target_dir.clone())
.unwrap_or_else(|| workspace.workspace_root().join("target"));
// Set up some tempfiles so we can do some IPC between us and the linker/rustc wrapper (which is occasionally us!)
let link_args_file = Arc::new(
NamedTempFile::with_suffix(".txt")
@@ -677,11 +692,16 @@ impl BuildRequest {
• link_args_file: {},
• link_err_file: {},
• rustc_wrapper_args_file: {},
• session_cache_dir: {}"#,
• session_cache_dir: {}
• linker: {:?}
• target_dir: {:?}
"#,
link_args_file.path().display(),
link_err_file.path().display(),
rustc_wrapper_args_file.path().display(),
session_cache_dir.path().display(),
custom_linker,
target_dir,
);
Ok(Self {
@@ -696,7 +716,7 @@ impl BuildRequest {
workspace,
config,
enabled_platforms,
custom_target_dir: None,
target_dir,
custom_linker,
link_args_file,
link_err_file,
@@ -739,6 +759,9 @@ impl BuildRequest {
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
.await
.context("Failed to write main executable")?;
self.write_frameworks(ctx, &artifacts.direct_rustc)
.await
.context("Failed to write frameworks")?;
self.write_assets(ctx, &artifacts.assets)
.await
.context("Failed to write assets")?;
@@ -874,8 +897,6 @@ impl BuildRequest {
}
}
let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
// 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.path()) {
@@ -887,13 +908,34 @@ impl BuildRequest {
// 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.path()) {
if !linker_warnings.is_empty() {
tracing::warn!("Linker warnings: {}", linker_warnings);
if output_location.is_none() {
tracing::error!("Linker warnings: {}", linker_warnings);
} else {
tracing::debug!("Linker warnings: {}", linker_warnings);
}
}
}
// Collect the linker args from the and update the rustc args
direct_rustc.link_args = std::fs::read_to_string(self.link_args_file.path())
.context("Failed to read link args from file")?
.lines()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
// Fat builds need to be linked with the fat linker. Would also like to link here for thin builds
if matches!(ctx.mode, BuildMode::Fat) {
let link_start = SystemTime::now();
self.run_fat_link(ctx, &exe, &direct_rustc).await?;
tracing::debug!(
"Fat linking completed in {}us",
SystemTime::now()
.duration_since(link_start)
.unwrap()
.as_micros()
);
}
let assets = self.collect_assets(&exe, ctx)?;
@@ -901,7 +943,11 @@ impl BuildRequest {
let mode = ctx.mode.clone();
let platform = self.platform;
tracing::debug!("Build completed successfully: {:?}", exe);
tracing::debug!(
"Build completed successfully in {}us: {:?}",
time_end.duration_since(time_start).unwrap().as_micros(),
exe
);
Ok(BuildArtifacts {
time_end,
@@ -991,12 +1037,6 @@ impl BuildRequest {
| Platform::Ios
| Platform::Liveview
| Platform::Server => {
// We wipe away the dir completely, which is not great behavior :/
// Don't wipe server since web will need this folder too.
if self.platform != Platform::Server {
_ = std::fs::remove_dir_all(self.exe_dir());
}
std::fs::create_dir_all(self.exe_dir())?;
std::fs::copy(exe, self.main_exe())?;
}
@@ -1005,6 +1045,52 @@ impl BuildRequest {
Ok(())
}
async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
let framework_dir = self.frameworks_folder();
_ = std::fs::create_dir_all(&framework_dir);
for arg in &direct_rustc.link_args {
// todo - how do we handle windows dlls? we don't want to bundle the system dlls
// for now, we don't do anything with dlls, and only use .dylibs and .so files
if arg.ends_with(".dylib") | arg.ends_with(".so") {
let from = PathBuf::from(arg);
let to = framework_dir.join(from.file_name().unwrap());
_ = std::fs::remove_file(&to);
tracing::debug!("Copying framework from {from:?} to {to:?}");
// in dev and on normal oses, we want to symlink the file
// otherwise, just copy it (since in release you want to distribute the framework)
if cfg!(any(windows, unix)) && !self.release {
#[cfg(windows)]
std::os::windows::fs::symlink_file(from, to).with_context(|| {
"Failed to symlink framework into bundle: {from:?} -> {to:?}"
})?;
#[cfg(unix)]
std::os::unix::fs::symlink(from, to).with_context(|| {
"Failed to symlink framework into bundle: {from:?} -> {to:?}"
})?;
} else {
std::fs::copy(from, to)?;
}
}
}
Ok(())
}
fn frameworks_folder(&self) -> PathBuf {
match self.triple.operating_system {
OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => {
self.root_dir().join("Contents").join("Frameworks")
}
OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"),
OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(),
_ => self.root_dir(),
}
}
/// Copy the assets out of the manifest and into the target location
///
/// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
@@ -1200,6 +1286,7 @@ impl BuildRequest {
// -nodefaultlibs
// -Wl,-all_load
// ```
let mut dylibs = vec![];
let mut object_files = args
.iter()
.filter(|arg| arg.ends_with(".rcgu.o"))
@@ -1233,6 +1320,15 @@ impl BuildRequest {
let patch_file = self.main_exe().with_file_name("stub.o");
std::fs::write(&patch_file, stub_bytes)?;
object_files.push(patch_file);
// 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 {
if arg.ends_with(".dylib") || arg.ends_with(".so") {
let path = PathBuf::from(arg);
dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
}
}
}
// And now we can run the linker with our new args
@@ -1251,6 +1347,7 @@ impl BuildRequest {
// does it since it uses llvm-objcopy into the `target/debug/` folder.
let res = Command::new(linker)
.args(object_files.iter())
.args(dylibs.iter())
.args(self.thin_link_args(&args)?)
.args(out_arg)
.env_clear()
@@ -1298,12 +1395,9 @@ impl BuildRequest {
/// This is basically just stripping away the rlibs and other libraries that will be satisfied
/// by our stub step.
fn thin_link_args(&self, original_args: &[&str]) -> Result<Vec<String>> {
use target_lexicon::OperatingSystem;
let triple = self.triple.clone();
let mut out_args = vec![];
match triple.operating_system {
match self.linker_flavor() {
// wasm32-unknown-unknown -> use wasm-ld (gnu-lld)
//
// We need to import a few things - namely the memory and ifunc table.
@@ -1323,7 +1417,7 @@ impl BuildRequest {
// I think we can make relocation-model=pic work for non-wasm platforms, enabling
// fully relocatable modules with no host coordination in lieu of sending out
// the aslr slide at runtime.
OperatingSystem::Unknown if self.platform == Platform::Web => {
LinkerFlavor::WasmLld => {
out_args.extend([
"--fatal-warnings".to_string(),
"--verbose".to_string(),
@@ -1344,7 +1438,7 @@ impl BuildRequest {
//
// Most importantly, we want to pass `-dylib` to both CC and the linker to indicate that
// we want to generate the shared library instead of an executable.
OperatingSystem::IOS(_) | OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) => {
LinkerFlavor::Darwin => {
out_args.extend(["-Wl,-dylib".to_string()]);
// Preserve the original args. We only preserve:
@@ -1367,7 +1461,7 @@ impl BuildRequest {
// android/linux need to be compatible with lld
//
// android currently drags along its own libraries and other zany flags
OperatingSystem::Linux => {
LinkerFlavor::Gnu => {
out_args.extend([
"-shared".to_string(),
"-Wl,--eh-frame-hdr".to_string(),
@@ -1391,13 +1485,14 @@ impl BuildRequest {
if arg.starts_with("-l")
|| arg.starts_with("-m")
|| arg.starts_with("-Wl,--target=")
|| arg.starts_with("-Wl,-fuse-ld")
{
out_args.push(arg.to_string());
}
}
}
OperatingSystem::Windows => {
LinkerFlavor::Msvc => {
out_args.extend([
"shlwapi.lib".to_string(),
"kernel32.lib".to_string(),
@@ -1415,7 +1510,9 @@ impl BuildRequest {
]);
}
_ => return Err(anyhow::anyhow!("Unsupported platform for thin linking").into()),
LinkerFlavor::Unsupported => {
return Err(anyhow::anyhow!("Unsupported platform for thin linking").into())
}
}
let extract_value = |arg: &str| -> Option<String> {
@@ -1460,15 +1557,12 @@ impl BuildRequest {
.unwrap_or(0),
));
let extension = match self.triple.operating_system {
OperatingSystem::Darwin(_) => "dylib",
OperatingSystem::MacOSX(_) => "dylib",
OperatingSystem::IOS(_) => "dylib",
OperatingSystem::Windows => "dll",
OperatingSystem::Linux => "so",
OperatingSystem::Wasi => "wasm",
OperatingSystem::Unknown if self.platform == Platform::Web => "wasm",
_ => "",
let extension = match self.linker_flavor() {
LinkerFlavor::Darwin => "dylib",
LinkerFlavor::Gnu => "so",
LinkerFlavor::WasmLld => "wasm",
LinkerFlavor::Msvc => "dll",
LinkerFlavor::Unsupported => "",
};
path.with_extension(extension)
@@ -1512,38 +1606,65 @@ impl BuildRequest {
) -> Result<()> {
ctx.status_starting_link();
let raw_args = std::fs::read_to_string(self.link_args_file.path())
.context("Failed to read link args from file")?;
let args = raw_args.lines().collect::<Vec<_>>();
// Filter out the rlib files from the arguments
let rlibs = args
let rlibs = rustc_args
.link_args
.iter()
.filter(|arg| arg.ends_with(".rlib"))
.map(PathBuf::from)
.collect::<Vec<_>>();
// Acquire a hash from the rlib names
// Acquire a hash from the rlib names, sizes, modified times, and dx's git commit hash
// This ensures that any changes in dx or the rlibs will cause a new hash to be generated
// The hash relies on both dx and rustc hashes, so it should be thoroughly unique. Keep it
// short to avoid long file names.
let hash_id = Uuid::new_v5(
&Uuid::NAMESPACE_OID,
rlibs
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy())
.map(|p| {
format!(
"{}-{}-{}-{}",
p.file_name().unwrap().to_string_lossy(),
p.metadata().map(|m| m.len()).unwrap_or_default(),
p.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|f| f.duration_since(UNIX_EPOCH).map(|f| f.as_secs()).ok())
.unwrap_or_default(),
crate::dx_build_info::GIT_COMMIT_HASH.unwrap_or_default()
)
})
.collect::<String>()
.as_bytes(),
);
)
.to_string()
.chars()
.take(8)
.collect::<String>();
// Check if we already have a cached object file
let out_ar_path = exe.with_file_name(format!("libfatdependencies-{hash_id}.a"));
let out_ar_path = exe.with_file_name(format!("libdeps-{hash_id}.a",));
let out_rlibs_list = exe.with_file_name(format!("rlibs-{hash_id}.txt"));
let mut archive_has_contents = out_ar_path.exists();
let mut compiler_rlibs = vec![];
// Use the rlibs list if it exists
let mut compiler_rlibs = std::fs::read_to_string(&out_rlibs_list)
.ok()
.map(|s| s.lines().map(PathBuf::from).collect::<Vec<_>>())
.unwrap_or_default();
// Create it by dumping all the rlibs into it
// This will include the std rlibs too, which can severely bloat the size of the archive
//
// The nature of this process involves making extremely fat archives, so we should try and
// speed up the future linking process by caching the archive.
if !crate::devcfg::should_cache_dep_lib(&out_ar_path) {
//
// Since we're using the git hash for the CLI entropy, debug builds should always regenerate
// the archive since their hash might not change, but the logic might.
if !archive_has_contents || cfg!(debug_assertions) {
compiler_rlibs.clear();
let mut bytes = vec![];
let mut out_ar = ar::Builder::new(&mut bytes);
for rlib in &rlibs {
@@ -1573,7 +1694,11 @@ impl BuildRequest {
}
// rlibs might contain dlls/sos/lib files which we don't want to include
if name.ends_with(".dll") || name.ends_with(".so") || name.ends_with(".lib") {
if name.ends_with(".dll")
|| name.ends_with(".so")
|| name.ends_with(".lib")
|| name.ends_with(".dylib")
{
compiler_rlibs.push(rlib.to_owned());
continue;
}
@@ -1582,6 +1707,7 @@ impl BuildRequest {
tracing::debug!("Unknown object file in rlib: {:?}", name);
}
archive_has_contents = true;
out_ar
.append(&object_file.header().clone(), object_file)
.context("Failed to add object file to archive")?;
@@ -1595,8 +1721,10 @@ impl BuildRequest {
// Run the ranlib command to index the archive. This slows down this process a bit,
// but is necessary for some linkers to work properly.
// We ignore its error in case it doesn't recognize the architecture
if let Some(ranlib) = self.select_ranlib() {
_ = Command::new(ranlib).arg(&out_ar_path).output().await;
if self.linker_flavor() == LinkerFlavor::Darwin {
if let Some(ranlib) = self.select_ranlib() {
_ = Command::new(ranlib).arg(&out_ar_path).output().await;
}
}
}
@@ -1606,62 +1734,71 @@ impl BuildRequest {
// And then remove the rest of the rlibs
//
// We also need to insert the -force_load flag to force the linker to load the archive
let mut args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
if let Some(first_rlib) = args.iter().position(|arg| arg.ends_with(".rlib")) {
match self.triple.operating_system {
OperatingSystem::Unknown if self.platform == Platform::Web => {
// We need to use the --whole-archive flag for wasm
args[first_rlib] = "--whole-archive".to_string();
args.insert(first_rlib + 1, out_ar_path.display().to_string());
args.insert(first_rlib + 2, "--no-whole-archive".to_string());
args.retain(|arg| !arg.ends_with(".rlib"));
// add back the compiler rlibs
for rlib in compiler_rlibs.iter().rev() {
args.insert(first_rlib + 3, rlib.display().to_string());
let mut args = rustc_args.link_args.clone();
if let Some(last_object) = args.iter().rposition(|arg| arg.ends_with(".o")) {
if archive_has_contents {
match self.linker_flavor() {
LinkerFlavor::WasmLld => {
args.insert(last_object, "--whole-archive".to_string());
args.insert(last_object + 1, out_ar_path.display().to_string());
args.insert(last_object + 2, "--no-whole-archive".to_string());
args.retain(|arg| !arg.ends_with(".rlib"));
for rlib in compiler_rlibs.iter().rev() {
args.insert(last_object + 3, rlib.display().to_string());
}
}
}
// Subtle difference - on linux and android we go through clang and thus pass `-Wl,` prefix
OperatingSystem::Linux => {
args[first_rlib] = "-Wl,--whole-archive".to_string();
args.insert(first_rlib + 1, out_ar_path.display().to_string());
args.insert(first_rlib + 2, "-Wl,--no-whole-archive".to_string());
args.retain(|arg| !arg.ends_with(".rlib"));
// add back the compiler rlibs
for rlib in compiler_rlibs.iter().rev() {
args.insert(first_rlib + 3, rlib.display().to_string());
LinkerFlavor::Gnu => {
args.insert(last_object, "-Wl,--whole-archive".to_string());
args.insert(last_object + 1, out_ar_path.display().to_string());
args.insert(last_object + 2, "-Wl,--no-whole-archive".to_string());
args.retain(|arg| !arg.ends_with(".rlib"));
for rlib in compiler_rlibs.iter().rev() {
args.insert(last_object + 3, rlib.display().to_string());
}
}
}
OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
args[first_rlib] = "-Wl,-force_load".to_string();
args.insert(first_rlib + 1, out_ar_path.display().to_string());
args.retain(|arg| !arg.ends_with(".rlib"));
// add back the compiler rlibs
for rlib in compiler_rlibs.iter().rev() {
args.insert(first_rlib + 2, rlib.display().to_string());
LinkerFlavor::Darwin => {
args.insert(last_object, "-Wl,-force_load".to_string());
args.insert(last_object + 1, out_ar_path.display().to_string());
args.retain(|arg| !arg.ends_with(".rlib"));
for rlib in compiler_rlibs.iter().rev() {
args.insert(last_object + 2, rlib.display().to_string());
}
}
args.insert(first_rlib + 3, "-Wl,-all_load".to_string());
}
OperatingSystem::Windows => {
args[first_rlib] = format!("/WHOLEARCHIVE:{}", out_ar_path.display());
args.retain(|arg| !arg.ends_with(".rlib"));
// add back the compiler rlibs
for rlib in compiler_rlibs.iter().rev() {
args.insert(first_rlib + 1, rlib.display().to_string());
LinkerFlavor::Msvc => {
args.insert(
last_object,
format!("/WHOLEARCHIVE:{}", out_ar_path.display()),
);
args.retain(|arg| !arg.ends_with(".rlib"));
for rlib in compiler_rlibs.iter().rev() {
args.insert(last_object + 1, rlib.display().to_string());
}
}
LinkerFlavor::Unsupported => {
tracing::error!("Unsupported platform for fat linking");
}
};
}
}
args.insert(first_rlib, "/HIGHENTROPYVA:NO".to_string());
}
// Add custom args to the linkers
match self.linker_flavor() {
LinkerFlavor::Gnu => {
// Export `main` so subsecond can use it for a reference point
args.push("-Wl,--export-dynamic-symbol,main".to_string());
}
LinkerFlavor::Darwin => {
// `-all_load` is an extra step to ensure that all symbols are loaded (different than force_load)
args.push("-Wl,-all_load".to_string());
}
LinkerFlavor::Msvc => {
// Prevent alsr from overflowing 32 bits
args.push("/HIGHENTROPYVA:NO".to_string());
_ => {}
};
// Export `main` so subsecond can use it for a reference point
args.push("/EXPORT:main".to_string());
}
LinkerFlavor::WasmLld | LinkerFlavor::Unsupported => {}
}
// We also need to remove the `-o` flag since we want the linker output to end up in the
@@ -1690,6 +1827,7 @@ impl BuildRequest {
let linker = self.select_linker()?;
tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args);
tracing::trace!("Fat linking with env: {:#?}", rustc_args.envs);
// Run the linker directly!
let out_arg = match self.triple.operating_system {
@@ -1724,9 +1862,64 @@ impl BuildRequest {
_ = std::fs::remove_file(f);
}
// Cache the rlibs list
_ = std::fs::write(
&out_rlibs_list,
compiler_rlibs
.into_iter()
.map(|s| s.display().to_string())
.join("\n"),
);
Ok(())
}
/// Automatically detect the linker flavor based on the target triple and any custom linkers.
///
/// This tries to replicate what rustc does when selecting the linker flavor based on the linker
/// and triple.
fn linker_flavor(&self) -> LinkerFlavor {
if let Some(custom) = self.custom_linker.as_ref() {
let name = custom.file_name().unwrap().to_ascii_lowercase();
match name.to_str() {
Some("lld-link") => return LinkerFlavor::Msvc,
Some("lld-link.exe") => return LinkerFlavor::Msvc,
Some("wasm-ld") => return LinkerFlavor::WasmLld,
Some("ld64.lld") => return LinkerFlavor::Darwin,
Some("ld.lld") => return LinkerFlavor::Gnu,
Some("ld.gold") => return LinkerFlavor::Gnu,
Some("mold") => return LinkerFlavor::Gnu,
Some("sold") => return LinkerFlavor::Gnu,
Some("wild") => return LinkerFlavor::Gnu,
_ => {}
}
}
match self.triple.environment {
target_lexicon::Environment::Gnu
| target_lexicon::Environment::Gnuabi64
| target_lexicon::Environment::Gnueabi
| target_lexicon::Environment::Gnueabihf
| target_lexicon::Environment::GnuLlvm => LinkerFlavor::Gnu,
target_lexicon::Environment::Musl => LinkerFlavor::Gnu,
target_lexicon::Environment::Android => LinkerFlavor::Gnu,
target_lexicon::Environment::Msvc => LinkerFlavor::Msvc,
target_lexicon::Environment::Macabi => LinkerFlavor::Darwin,
_ => match self.triple.operating_system {
OperatingSystem::Darwin(_) => LinkerFlavor::Darwin,
OperatingSystem::IOS(_) => LinkerFlavor::Darwin,
OperatingSystem::MacOSX(_) => LinkerFlavor::Darwin,
OperatingSystem::Linux => LinkerFlavor::Gnu,
OperatingSystem::Windows => LinkerFlavor::Msvc,
_ => match self.triple.architecture {
target_lexicon::Architecture::Wasm32 => LinkerFlavor::WasmLld,
target_lexicon::Architecture::Wasm64 => LinkerFlavor::WasmLld,
_ => LinkerFlavor::Unsupported,
},
},
}
}
/// Select the linker to use for this platform.
///
/// We prefer to use the rust-lld linker when we can since it's usually there.
@@ -1750,25 +1943,25 @@ impl BuildRequest {
return Ok(PathBuf::from(linker));
}
let cc = match self.triple.operating_system {
OperatingSystem::Unknown if self.platform == Platform::Web => self.workspace.wasm_ld(),
if let Some(linker) = self.custom_linker.clone() {
return Ok(linker);
}
// The android clang linker is *special* and has some android-specific flags that we need
//
// Note that this is *clang*, not `lld`.
OperatingSystem::Linux if self.platform == Platform::Android => {
self.workspace.android_tools()?.android_cc(&self.triple)
}
let cc = match self.linker_flavor() {
LinkerFlavor::WasmLld => self.workspace.wasm_ld(),
// On macOS, we use the system linker since it's usually there.
// We could also use `lld` here, but it might not be installed by default.
//
// Note that this is *clang*, not `lld`.
OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => self.workspace.cc(),
LinkerFlavor::Darwin => self.workspace.cc(),
// On Linux, we use the system linker since it's usually there.
LinkerFlavor::Gnu => self.workspace.cc(),
// On windows, instead of trying to find the system linker, we just go with the lld.link
// that rustup provides. It's faster and more stable then reyling on link.exe in path.
OperatingSystem::Windows => self.workspace.lld_link(),
LinkerFlavor::Msvc => self.workspace.lld_link(),
// The rest of the platforms use `cc` as the linker which should be available in your path,
// provided you have build-tools setup. On mac/linux this is the default, but on Windows
@@ -1782,7 +1975,7 @@ impl BuildRequest {
// Note that "cc" is *not* a linker. It's a compiler! The arguments we pass need to be in
// the form of `-Wl,<args>` for them to make it to the linker. This matches how rust does it
// which is confusing.
_ => self.workspace.cc(),
LinkerFlavor::Unsupported => self.workspace.cc(),
};
Ok(cc)
@@ -1827,7 +2020,7 @@ impl BuildRequest {
cmd.envs(rustc_args.envs.iter().cloned());
tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
// tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
Ok(cmd)
}
@@ -1935,6 +2128,21 @@ impl BuildRequest {
));
}
// Handle frameworks/dylibs by setting the rpath
// This is dependent on the bundle structure - in this case, appimage and appbundle for mac/linux
// todo: we need to figure out what to do for windows
match self.triple.operating_system {
OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path/../Frameworks".to_string());
cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path".to_string());
}
OperatingSystem::Linux => {
cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN/../lib".to_string());
cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN".to_string());
}
_ => {}
}
// Our fancy hot-patching engine needs a lot of customization to work properly.
//
// These args are mostly intended to be passed when *fat* linking but are generally fine to
@@ -2051,10 +2259,6 @@ impl BuildRequest {
}));
}
if let Some(target_dir) = self.custom_target_dir.as_ref() {
env_vars.push(("CARGO_TARGET_DIR", target_dir.display().to_string()));
}
// If this is a release build, bake the base path and title into the binary with env vars.
// todo: should we even be doing this? might be better being a build.rs or something else.
if self.release {
@@ -2510,7 +2714,7 @@ impl BuildRequest {
/// is "distributed" after building an application (configurable in the
/// `Dioxus.toml`).
fn internal_out_dir(&self) -> PathBuf {
let dir = self.workspace_dir().join("target").join("dx");
let dir = self.target_dir.join("dx");
std::fs::create_dir_all(&dir).unwrap();
dir
}
+1 -1
View File
@@ -44,8 +44,8 @@ pub enum LinkerFlavor {
Gnu,
Darwin,
WasmLld,
Unix,
Msvc,
Unsupported, // a catch-all for unsupported linkers, usually the stripped-down unix ones
}
impl LinkAction {
-10
View File
@@ -1,15 +1,5 @@
//! Configuration of the CLI at runtime to enable certain experimental features.
use std::path::Path;
/// Should we cache the dependency library?
///
/// When the `DIOXUS_CACHE_DEP_LIB` environment variable is set, we will cache the dependency library
/// built from the target's dependencies.
pub(crate) fn should_cache_dep_lib(lib: &Path) -> bool {
std::env::var("DIOXUS_CACHE_DEP_LIB").is_ok() && lib.exists()
}
/// Should we force the entropy to be used on the main exe?
///
/// This is used to verify that binaries are copied with different names such that they don't collide
+2
View File
@@ -25,6 +25,7 @@ pub fn is_wrapping_rustc() -> bool {
pub struct RustcArgs {
pub args: Vec<String>,
pub envs: Vec<(String, String)>,
pub link_args: Vec<String>,
}
/// Run rustc directly, but output the result to a file.
@@ -50,6 +51,7 @@ pub async fn run_rustc() {
let rustc_args = RustcArgs {
args: args().skip(1).collect::<Vec<_>>(),
envs: vars().collect::<_>(),
link_args: Default::default(),
};
std::fs::create_dir_all(var_file.parent().expect("Failed to get parent dir"))
+7 -1
View File
@@ -721,7 +721,13 @@ impl Output {
frame.render_widget(
Paragraph::new(Line::from(vec![
"Hotreload: ".gray(),
"rsx and assets".yellow(),
if !state.runner.automatic_rebuilds {
"disabled".dark_gray()
} else if state.runner.use_hotpatch_engine {
"hot-patching".yellow()
} else {
"rsx and assets".yellow()
},
])),
meta_list[3],
);
+4 -3
View File
@@ -138,9 +138,10 @@ pub(crate) fn proxy_to(
match res {
Ok(res) => {
// log assets at a different log level
if uri.path().starts_with("/assets")
|| uri.path().starts_with("/_dioxus")
|| uri.path().starts_with("/public")
if uri.path().starts_with("/assets/")
|| uri.path().starts_with("/_dioxus/")
|| uri.path().starts_with("/public/")
|| uri.path().starts_with("/wasm/")
{
tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
} else {
+3 -3
View File
@@ -341,15 +341,15 @@ impl AppServer {
continue;
};
// Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version
cached_file.most_recent = Some(new_contents);
// This assumes the two files are structured similarly. If they're not, we can't diff them
let Some(changed_rsx) = dioxus_rsx_hotreload::diff_rsx(&new_file, &old_file) else {
needs_full_rebuild = true;
break;
};
// Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version
cached_file.most_recent = Some(new_contents);
for ChangedRsx { old, new } in changed_rsx {
let old_start = old.span().start();
+261 -107
View File
@@ -1,3 +1,228 @@
// use crate::Config;
// use crate::{
// app::App,
// ipc::{IpcMethod, UserWindowEvent},
// };
// use dioxus_core::*;
// use dioxus_document::eval;
// use std::any::Any;
// use tao::event::{Event, StartCause, WindowEvent};
// /// Launches the WebView and runs the event loop
// pub fn launch(root: fn() -> Element) {
// launch_cfg(root, vec![], vec![]);
// }
// /// Launches the WebView and runs the event loop, with configuration and root props.
// pub fn launch_cfg(
// root: fn() -> Element,
// contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
// platform_config: Vec<Box<dyn Any>>,
// ) {
// let mut virtual_dom = VirtualDom::new(root);
// for context in contexts {
// virtual_dom.insert_any_root_context(context());
// }
// let platform_config = *platform_config
// .into_iter()
// .find_map(|cfg| cfg.downcast::<Config>().ok())
// .unwrap_or_default();
// launch_virtual_dom(virtual_dom, platform_config)
// }
// /// Launches the WebView and runs the event loop, with configuration and root props.
// pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
// #[cfg(feature = "tokio_runtime")]
// {
// tokio::runtime::Builder::new_multi_thread()
// .enable_all()
// .build()
// .unwrap()
// .block_on(tokio::task::unconstrained(async move {
// launch_virtual_dom_blocking(virtual_dom, desktop_config)
// }));
// unreachable!("The desktop launch function will never exit")
// }
// #[cfg(not(feature = "tokio_runtime"))]
// {
// launch_virtual_dom_blocking(virtual_dom, desktop_config);
// }
// }
// /// Launch the WebView and run the event loop, with configuration and root props.
// ///
// /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
// /// and is equivalent to calling launch_with_props with the tokio feature disabled.
// pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config: Config) -> ! {
// let mut custom_event_handler = desktop_config.custom_event_handler.take();
// let (event_loop, mut app) = App::new(desktop_config, virtual_dom);
// event_loop.run(move |window_event, event_loop, control_flow| {
// // Set the control flow and check if any events need to be handled in the app itself
// app.tick(&window_event);
// if let Some(ref mut f) = custom_event_handler {
// f(&window_event, event_loop)
// }
// match window_event {
// Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(),
// Event::LoopDestroyed => app.handle_loop_destroyed(),
// Event::WindowEvent {
// event, window_id, ..
// } => match event {
// WindowEvent::CloseRequested => app.handle_close_requested(window_id),
// WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
// WindowEvent::Resized(new_size) => app.resize_window(window_id, new_size),
// _ => {}
// },
// Event::UserEvent(event) => match event {
// UserWindowEvent::Poll(id) => app.poll_vdom(id),
// UserWindowEvent::NewWindow => app.handle_new_window(),
// UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id),
// UserWindowEvent::Shutdown => app.control_flow = tao::event_loop::ControlFlow::Exit,
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
// UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt),
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
// UserWindowEvent::MudaMenuEvent(evnt) => app.handle_menu_event(evnt),
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
// UserWindowEvent::TrayMenuEvent(evnt) => app.handle_tray_menu_event(evnt),
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
// UserWindowEvent::TrayIconEvent(evnt) => app.handle_tray_icon_event(evnt),
// #[cfg(all(feature = "devtools", debug_assertions))]
// UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
// // Windows-only drag-n-drop fix events. We need to call the interpreter drag-n-drop code.
// UserWindowEvent::WindowsDragDrop(id) => {
// if let Some(webview) = app.webviews.get(&id) {
// webview.dom.in_runtime(|| {
// ScopeId::ROOT.in_runtime(|| {
// eval("window.interpreter.handleWindowsDragDrop();");
// });
// });
// }
// }
// UserWindowEvent::WindowsDragLeave(id) => {
// if let Some(webview) = app.webviews.get(&id) {
// webview.dom.in_runtime(|| {
// ScopeId::ROOT.in_runtime(|| {
// eval("window.interpreter.handleWindowsDragLeave();");
// });
// });
// }
// }
// UserWindowEvent::WindowsDragOver(id, x_pos, y_pos) => {
// if let Some(webview) = app.webviews.get(&id) {
// webview.dom.in_runtime(|| {
// ScopeId::ROOT.in_runtime(|| {
// let e = eval(
// r#"
// const xPos = await dioxus.recv();
// const yPos = await dioxus.recv();
// window.interpreter.handleWindowsDragOver(xPos, yPos)
// "#,
// );
// _ = e.send(x_pos);
// _ = e.send(y_pos);
// });
// });
// }
// }
// UserWindowEvent::Ipc { id, msg } => match msg.method() {
// IpcMethod::Initialize => app.handle_initialize_msg(id),
// IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
// IpcMethod::UserEvent => {}
// IpcMethod::Query => app.handle_query_msg(msg, id),
// IpcMethod::BrowserOpen => app.handle_browser_open(msg),
// IpcMethod::Other(_) => {}
// },
// },
// _ => {}
// }
// *control_flow = app.control_flow;
// })
// }
// /// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
// /// We hardcode these to have a single trampoline for host Java code to call into.
// ///
// /// This saves us from having to plumb the top-level package name all the way down into
// /// this file. This is better for modularity (ie just call dioxus' main to run the app) as
// /// well as cache thrashing since this crate doesn't rely on external env vars.
// ///
// /// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
// /// change this, you'll need to change the CLI as well.
// #[cfg(target_os = "android")]
// #[no_mangle]
// #[inline(never)]
// pub extern "C" fn start_app() {
// wry::android_binding!(dev_dioxus, main, wry);
// tao::android_binding!(
// dev_dioxus,
// main,
// WryActivity,
// wry::android_setup,
// dioxus_main_root_fn,
// tao
// );
// }
// #[cfg(target_os = "android")]
// #[doc(hidden)]
// pub fn dioxus_main_root_fn() {
// // we're going to find the `main` symbol using dlsym directly and call it
// unsafe {
// let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
// if main_fn_ptr.is_null() {
// main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
// }
// if main_fn_ptr.is_null() {
// panic!("Failed to find main symbol");
// }
// // Set the env vars that rust code might expect, passed off to us by the android app
// // Doing this before main emulates the behavior of a regular executable
// if cfg!(target_os = "android") && cfg!(debug_assertions) {
// load_env_file_from_session_cache();
// }
// let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
// main_fn();
// };
// }
// /// Load the env file from the session cache if we're in debug mode and on android
// ///
// /// This is a slightly hacky way of being able to use std::env::var code in android apps without
// /// going through their custom java-based system.
// #[cfg(target_os = "android")]
// fn load_env_file_from_session_cache() {
// let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
// if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
// for line in env_file.lines() {
// if let Some((key, value)) = line.trim().split_once('=') {
// std::env::set_var(key, value);
// }
// }
// }
// }
use crate::Config;
use crate::{
app::App,
@@ -8,52 +233,6 @@ use dioxus_document::eval;
use std::any::Any;
use tao::event::{Event, StartCause, WindowEvent};
/// Launches the WebView and runs the event loop
pub fn launch(root: fn() -> Element) {
launch_cfg(root, vec![], vec![]);
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch_cfg(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
let mut virtual_dom = VirtualDom::new(root);
for context in contexts {
virtual_dom.insert_any_root_context(context());
}
let platform_config = *platform_config
.into_iter()
.find_map(|cfg| cfg.downcast::<Config>().ok())
.unwrap_or_default();
launch_virtual_dom(virtual_dom, platform_config)
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
#[cfg(feature = "tokio_runtime")]
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(tokio::task::unconstrained(async move {
launch_virtual_dom_blocking(virtual_dom, desktop_config)
}));
unreachable!("The desktop launch function will never exit")
}
#[cfg(not(feature = "tokio_runtime"))]
{
launch_virtual_dom_blocking(virtual_dom, desktop_config);
}
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
@@ -157,68 +336,43 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config:
})
}
/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
/// We hardcode these to have a single trampoline for host Java code to call into.
///
/// This saves us from having to plumb the top-level package name all the way down into
/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
/// well as cache thrashing since this crate doesn't rely on external env vars.
///
/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
/// change this, you'll need to change the CLI as well.
#[cfg(target_os = "android")]
#[no_mangle]
#[inline(never)]
pub extern "C" fn start_app() {
wry::android_binding!(dev_dioxus, main, wry);
tao::android_binding!(
dev_dioxus,
main,
WryActivity,
wry::android_setup,
dioxus_main_root_fn,
tao
);
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
#[cfg(feature = "tokio_runtime")]
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(tokio::task::unconstrained(async move {
launch_virtual_dom_blocking(virtual_dom, desktop_config)
}));
#[cfg(target_os = "android")]
#[doc(hidden)]
pub fn dioxus_main_root_fn() {
// we're going to find the `main` symbol using dlsym directly and call it
unsafe {
let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
unreachable!("The desktop launch function will never exit")
}
if main_fn_ptr.is_null() {
main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
}
if main_fn_ptr.is_null() {
panic!("Failed to find main symbol");
}
// Set the env vars that rust code might expect, passed off to us by the android app
// Doing this before main emulates the behavior of a regular executable
if cfg!(target_os = "android") && cfg!(debug_assertions) {
load_env_file_from_session_cache();
}
let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
main_fn();
};
}
/// Load the env file from the session cache if we're in debug mode and on android
///
/// This is a slightly hacky way of being able to use std::env::var code in android apps without
/// going through their custom java-based system.
#[cfg(target_os = "android")]
fn load_env_file_from_session_cache() {
let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
for line in env_file.lines() {
if let Some((key, value)) = line.trim().split_once('=') {
std::env::set_var(key, value);
}
}
#[cfg(not(feature = "tokio_runtime"))]
{
launch_virtual_dom_blocking(virtual_dom, desktop_config);
}
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) -> ! {
let mut virtual_dom = VirtualDom::new(root);
for context in contexts {
virtual_dom.insert_any_root_context(context());
}
let platform_config = *platform_config
.into_iter()
.find_map(|cfg| cfg.downcast::<Config>().ok())
.unwrap_or_default();
launch_virtual_dom(virtual_dom, platform_config)
}
+1 -1
View File
@@ -80,7 +80,7 @@ pub fn connect_at(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Sen
std::thread::spawn(move || {
let uri = format!(
"{endpoint}?aslr_reference={}&build_id={}",
subsecond::__aslr_reference(),
subsecond::aslr_reference(),
dioxus_cli_config::build_id()
);
+2 -2
View File
@@ -332,12 +332,12 @@ impl LaunchBuilder {
#[cfg(feature = "mobile")]
if matches!(platform, KnownPlatform::Mobile) {
return dioxus_desktop::launch::launch_cfg(app, contexts, configs);
return dioxus_mobile::launch_bindings::launch(app, contexts, configs);
}
#[cfg(feature = "desktop")]
if matches!(platform, KnownPlatform::Desktop) {
return dioxus_desktop::launch::launch_cfg(app, contexts, configs);
return dioxus_desktop::launch::launch(app, contexts, configs);
}
#[cfg(feature = "server")]
+160 -1
View File
@@ -5,6 +5,23 @@
pub use dioxus_desktop::*;
use dioxus_lib::prelude::*;
use std::any::Any;
use std::sync::Mutex;
pub mod launch_bindings {
use super::*;
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
super::launch_cfg(root, contexts, platform_config);
}
pub fn launch_virtual_dom(_virtual_dom: VirtualDom, _desktop_config: Config) -> ! {
todo!()
}
}
/// Launch via the binding API
pub fn launch(root: fn() -> Element) {
@@ -16,5 +33,147 @@ pub fn launch_cfg(
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
dioxus_desktop::launch::launch_cfg(root, contexts, platform_config);
#[cfg(target_os = "android")]
{
*APP_OBJECTS.lock().unwrap() = Some(BoundLaunchObjects {
root,
contexts,
platform_config,
});
}
#[cfg(not(target_os = "android"))]
{
dioxus_desktop::launch::launch(root, contexts, platform_config);
}
}
/// We need to store the root function and contexts in a static so that when the tao bindings call
/// "start_app", that the original function arguments are still around.
///
/// If you look closely, you'll notice that we impl Send for this struct. This would normally be
/// unsound. However, we know that the thread that created these objects ("main()" - see JNI_OnLoad)
/// is finished once `start_app` is called. This is similar to how an Rc<T> is technically safe
/// to move between threads if you can prove that no other thread is using the Rc<T> at the same time.
/// Crates like https://crates.io/crates/sendable exist that build on this idea but with runtimk,
/// validation that the current thread is the one that created the object.
///
/// Since `main()` completes, the only reader of this data will be `start_app`, so it's okay to
/// impl this as Send/Sync.
///
/// Todo(jon): the visibility of functions in this module is too public. Make sure to hide them before
/// releasing 0.7.
struct BoundLaunchObjects {
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
}
unsafe impl Send for BoundLaunchObjects {}
unsafe impl Sync for BoundLaunchObjects {}
static APP_OBJECTS: Mutex<Option<BoundLaunchObjects>> = Mutex::new(None);
#[doc(hidden)]
pub fn root() {
let app = APP_OBJECTS
.lock()
.expect("APP_FN_PTR lock failed")
.take()
.expect("Android to have set the app trampoline");
let BoundLaunchObjects {
root,
contexts,
platform_config,
} = app;
dioxus_desktop::launch::launch(root, contexts, platform_config);
}
/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
/// We hardcode these to have a single trampoline for host Java code to call into.
///
/// This saves us from having to plumb the top-level package name all the way down into
/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
/// well as cache thrashing since this crate doesn't rely on external env vars.
///
/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
/// change this, you'll need to change the CLI as well.
#[cfg(target_os = "android")]
#[no_mangle]
#[inline(never)]
pub extern "C" fn start_app() {
tao::android_binding!(dev_dioxus, main, WryActivity, wry::android_setup, root, tao);
wry::android_binding!(dev_dioxus, main, wry);
}
/// Call our `main` function to initialize the rust runtime and set the launch binding trampoline
#[cfg(target_os = "android")]
#[no_mangle]
#[inline(never)]
pub extern "C" fn JNI_OnLoad(
_vm: *mut libc::c_void,
_reserved: *mut libc::c_void,
) -> jni::sys::jint {
// we're going to find the `main` symbol using dlsym directly and call it
unsafe {
let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
if main_fn_ptr.is_null() {
main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
}
if main_fn_ptr.is_null() {
panic!("Failed to find main symbol");
}
// Set the env vars that rust code might expect, passed off to us by the android app
// Doing this before main emulates the behavior of a regular executable
if cfg!(target_os = "android") && cfg!(debug_assertions) {
load_env_file_from_session_cache();
}
let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
main_fn();
};
jni::sys::JNI_VERSION_1_6
}
/// Load the env file from the session cache if we're in debug mode and on android
///
/// This is a slightly hacky way of being able to use std::env::var code in android apps without
/// going through their custom java-based system.
#[cfg(target_os = "android")]
fn load_env_file_from_session_cache() {
let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
for line in env_file.lines() {
if let Some((key, value)) = line.trim().split_once('=') {
std::env::set_var(key, value);
}
}
}
}
// #![doc = include_str!("../README.md")]
// #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
// #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
// pub use dioxus_desktop::*;
// use dioxus_lib::prelude::*;
// use std::any::Any;
// /// Launch via the binding API
// pub fn launch(root: fn() -> Element) {
// launch_cfg(root, vec![], vec![]);
// }
// pub fn launch_cfg(
// root: fn() -> Element,
// contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
// platform_config: Vec<Box<dyn Any>>,
// ) {
// dioxus_desktop::launch::launch_cfg(root, contexts, platform_config);
// }
+12 -13
View File
@@ -354,19 +354,18 @@ impl ServeConfigBuilder {
let index_path = self
.index_path
.unwrap_or_else(|| public_path.join("index.html"));
load_index_path(index_path)?
// load_index_path(index_path).unwrap_or_else(|_| {
// r#"
// <!DOCTYPE html>
// <html>
// <head> </head>
// <body>
// <div id="main"></div>
// </body>
// </html>
// "#
// .to_string()
// })
load_index_path(index_path).unwrap_or_else(|_| {
r#"
<!DOCTYPE html>
<html>
<head> </head>
<body>
<div id="main"></div>
</body>
</html>
"#
.to_string()
})
}
};
@@ -0,0 +1,11 @@
[package]
name = "cross-tls-crate-dylib"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
# when testing, use a dylib
# crate-type = ["dylib"]
[dependencies]
@@ -0,0 +1,25 @@
pub use std::cell::Cell;
use std::{cell::RefCell, thread::LocalKey};
#[derive(Debug)]
pub struct StoredItem {
pub name: String,
pub value: f32,
pub items: Vec<String>,
}
thread_local! {
pub static BAZ: RefCell<Option<StoredItem>> = const { RefCell::new(None) };
}
pub fn get_baz() -> &'static LocalKey<RefCell<Option<StoredItem>>> {
if BAZ.with(|f| f.borrow().is_none()) {
BAZ.set(Some(StoredItem {
name: "BAR".to_string(),
value: 0.0,
items: vec!["item1".to_string(), "item2".to_string()],
}));
}
&BAZ
}
@@ -0,0 +1,7 @@
[package]
name = "cross-tls-crate"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
@@ -0,0 +1,25 @@
pub use std::cell::Cell;
use std::{cell::RefCell, thread::LocalKey};
#[derive(Debug)]
pub struct StoredItem {
pub name: String,
pub value: f32,
pub items: Vec<String>,
}
thread_local! {
pub static BAR: RefCell<Option<StoredItem>> = const { RefCell::new(None) };
}
pub fn get_bar() -> &'static LocalKey<RefCell<Option<StoredItem>>> {
if BAR.with(|f| f.borrow().is_none()) {
BAR.set(Some(StoredItem {
name: "BAR".to_string(),
value: 0.0,
items: vec!["item1".to_string(), "item2".to_string()],
}));
}
&BAR
}
@@ -0,0 +1,10 @@
[package]
name = "subsecond-tls-harness"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
dioxus-devtools = { workspace = true }
cross-tls-crate = { path = "../cross-tls-crate" }
cross-tls-crate-dylib = { path = "../cross-tls-crate-dylib" }
@@ -0,0 +1,13 @@
harness for testing cross-crate TLS imports.
TLS is the hardest to get right with binary patchinig since we need to rewrite initializers.
this crate relies on two crates - one as a dylib and the other as an rlib
the dylib should make it into our bundle and the rlib should just be linked.
both shouldn't cause access errors or segfaults during hotpatch.
```sh
cargo run --package dioxus-cli -- serve --hotpatch
```
@@ -0,0 +1,33 @@
use std::{cell::Cell, thread, time::Duration};
use cross_tls_crate::get_bar;
use cross_tls_crate_dylib::get_baz;
fn main() {
dioxus_devtools::connect_subsecond();
loop {
dioxus_devtools::subsecond::call(|| {
use cross_tls_crate::BAR;
use cross_tls_crate_dylib::BAZ;
thread_local! {
pub static FOO: Cell<f32> = const { Cell::new(2.0) };
}
println!("Hello 123s123123s: {}", FOO.get());
get_bar().with(|f| println!("Bar: {:?}", f.borrow()));
thread::sleep(Duration::from_secs(1));
FOO.set(2.0);
get_bar().with(|f| f.borrow_mut().as_mut().unwrap().value = 3.0);
get_baz().with(|f| f.borrow_mut().as_mut().unwrap().value = 4.0);
BAR.with_borrow(|f| {
println!("Bar: {:?}", f);
});
BAZ.with_borrow(|f| {
println!("Baz: {:?}", f);
});
});
}
}
+42 -16
View File
@@ -495,10 +495,9 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
}
}));
// Use the `__aslr_reference` symbol as a sentinel for the current executable. This is basically a
// cross-platform version of `__mh_execute_header` on macOS that sets a reference point for the
// jump table.
let old_offset = __aslr_reference() - table.aslr_reference as usize;
// Use the `main` symbol as a sentinel for the current executable. This is basically a
// cross-platform version of `__mh_execute_header` on macOS that we can use to base the executable.
let old_offset = aslr_reference() - table.aslr_reference as usize;
// Use the `main` symbol as a sentinel for the loaded library. Might want to move away
// from this at some point, or make it configurable
@@ -672,22 +671,49 @@ pub enum PatchError {
AndroidMemfd(String),
}
/// This function returns its own address, providing a stable reference point for hot-patch engine
/// to hook onto. If you were to write an object file for this function, it would amount to:
///
/// ```asm
/// __aslr_reference:
/// mov rax, qword ptr [rip + __aslr_reference@GOTPCREL] // notice the @GOTPCREL relocation
/// ret
/// ```
/// This function returns the address of the main function in the current executable. This is used as
/// an anchor to reference the current executable's base address.
///
/// The point here being that we have a stable address both at runtime and compile time, making it
/// possible to calculate the ASLR offset from within the process to correct the jump table.
///
/// It should only be called from the main executable *first* and not from a shared library since it
/// self-initializes.
#[doc(hidden)]
#[inline(never)]
#[no_mangle]
pub extern "C" fn __aslr_reference() -> usize {
__aslr_reference as *const () as usize
pub fn aslr_reference() -> usize {
#[cfg(target_family = "wasm")]
return 0;
#[cfg(not(target_family = "wasm"))]
unsafe {
use std::ffi::c_void;
// The first call to this function should occur in the
static mut MAIN_PTR: *mut c_void = std::ptr::null_mut();
if MAIN_PTR.is_null() {
#[cfg(unix)]
{
MAIN_PTR = libc::dlsym(libc::RTLD_DEFAULT, c"main".as_ptr() as _);
}
#[cfg(windows)]
{
extern "system" {
fn GetModuleHandleA(lpModuleName: *const i8) -> *mut std::ffi::c_void;
fn GetProcAddress(
hModule: *mut std::ffi::c_void,
lpProcName: *const i8,
) -> *mut std::ffi::c_void;
}
MAIN_PTR =
GetProcAddress(GetModuleHandleA(std::ptr::null()), c"main".as_ptr() as _) as _;
}
}
MAIN_PTR as usize
}
}
/// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/