diff --git a/Cargo.lock b/Cargo.lock index 2e7cb7871..0da630cd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index f1af72c3c..93bf49408 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index f17a46bdd..c06ffb1ab 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -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"] } diff --git a/packages/cli/src/build/builder.rs b/packages/cli/src/build/builder.rs index 519dafc14..0c080d456 100644 --- a/packages/cli/src/build/builder.rs +++ b/packages/cli/src/build/builder.rs @@ -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()); diff --git a/packages/cli/src/build/patch.rs b/packages/cli/src/build/patch.rs index de2f76810..86a71ea7b 100644 --- a/packages/cli/src/build/patch.rs +++ b/packages/cli/src/build/patch.rs @@ -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, } 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 { 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", + } +} diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index b92e9ffe7..9ab322049 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -316,8 +316,9 @@ //! - xbuild: 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, pub(crate) extra_rustc_args: Vec, pub(crate) no_default_features: bool, - pub(crate) custom_target_dir: Option, + 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::>(); + + 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> { - 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 { @@ -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::>(); - // 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::>(); - // 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::() .as_bytes(), - ); + ) + .to_string() + .chars() + .take(8) + .collect::(); // 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::>()) + .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::>(); - 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,` 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 } diff --git a/packages/cli/src/cli/link.rs b/packages/cli/src/cli/link.rs index bbf5cd1f6..7ffbdd33e 100644 --- a/packages/cli/src/cli/link.rs +++ b/packages/cli/src/cli/link.rs @@ -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 { diff --git a/packages/cli/src/devcfg.rs b/packages/cli/src/devcfg.rs index c248ca908..eb5cc4c38 100644 --- a/packages/cli/src/devcfg.rs +++ b/packages/cli/src/devcfg.rs @@ -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 diff --git a/packages/cli/src/rustcwrapper.rs b/packages/cli/src/rustcwrapper.rs index bd269d684..86c5e8b76 100644 --- a/packages/cli/src/rustcwrapper.rs +++ b/packages/cli/src/rustcwrapper.rs @@ -25,6 +25,7 @@ pub fn is_wrapping_rustc() -> bool { pub struct RustcArgs { pub args: Vec, pub envs: Vec<(String, String)>, + pub link_args: Vec, } /// 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::>(), envs: vars().collect::<_>(), + link_args: Default::default(), }; std::fs::create_dir_all(var_file.parent().expect("Failed to get parent dir")) diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index 5c7db25f6..8d32696ce 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -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], ); diff --git a/packages/cli/src/serve/proxy.rs b/packages/cli/src/serve/proxy.rs index 063584dca..028174fac 100644 --- a/packages/cli/src/serve/proxy.rs +++ b/packages/cli/src/serve/proxy.rs @@ -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 { diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index 4598517c7..2f36f79cd 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -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(); diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 027f63f04..54fa1e9c5 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -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 + Send + Sync>>, +// platform_config: Vec>, +// ) { +// 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::().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 + Send + Sync>>, - platform_config: Vec>, -) { - 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::().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 + Send + Sync>>, + platform_config: Vec>, +) -> ! { + 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::().ok()) + .unwrap_or_default(); + + launch_virtual_dom(virtual_dom, platform_config) +} diff --git a/packages/devtools/src/lib.rs b/packages/devtools/src/lib.rs index 077ed6c58..e79c7be8a 100644 --- a/packages/devtools/src/lib.rs +++ b/packages/devtools/src/lib.rs @@ -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() ); diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 4bc60e4b6..cb9b8c08d 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -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")] diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 07c920018..c6ad881bd 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -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 + Send + Sync>>, + platform_config: Vec>, + ) { + 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 + Send + Sync>>, platform_config: Vec>, ) { - 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 is technically safe +/// to move between threads if you can prove that no other thread is using the Rc 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 + Send + Sync>>, + platform_config: Vec>, +} + +unsafe impl Send for BoundLaunchObjects {} +unsafe impl Sync for BoundLaunchObjects {} + +static APP_OBJECTS: Mutex> = 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 + Send + Sync>>, +// platform_config: Vec>, +// ) { +// dioxus_desktop::launch::launch_cfg(root, contexts, platform_config); +// } diff --git a/packages/server/src/config.rs b/packages/server/src/config.rs index f1698bf64..4a6fdfc37 100644 --- a/packages/server/src/config.rs +++ b/packages/server/src/config.rs @@ -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#" - // - // - // - // - //
- // - // - // "# - // .to_string() - // }) + load_index_path(index_path).unwrap_or_else(|_| { + r#" + + + + +
+ + + "# + .to_string() + }) } }; diff --git a/packages/subsecond/subsecond-tests/cross-tls-crate-dylib/Cargo.toml b/packages/subsecond/subsecond-tests/cross-tls-crate-dylib/Cargo.toml new file mode 100644 index 000000000..dc53b47c4 --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-crate-dylib/Cargo.toml @@ -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] diff --git a/packages/subsecond/subsecond-tests/cross-tls-crate-dylib/src/lib.rs b/packages/subsecond/subsecond-tests/cross-tls-crate-dylib/src/lib.rs new file mode 100644 index 000000000..29d2aec1f --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-crate-dylib/src/lib.rs @@ -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, +} + +thread_local! { + pub static BAZ: RefCell> = const { RefCell::new(None) }; +} + +pub fn get_baz() -> &'static LocalKey>> { + 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 +} diff --git a/packages/subsecond/subsecond-tests/cross-tls-crate/Cargo.toml b/packages/subsecond/subsecond-tests/cross-tls-crate/Cargo.toml new file mode 100644 index 000000000..c6e6e80e1 --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-crate/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "cross-tls-crate" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] diff --git a/packages/subsecond/subsecond-tests/cross-tls-crate/src/lib.rs b/packages/subsecond/subsecond-tests/cross-tls-crate/src/lib.rs new file mode 100644 index 000000000..3c4d099b9 --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-crate/src/lib.rs @@ -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, +} + +thread_local! { + pub static BAR: RefCell> = const { RefCell::new(None) }; +} + +pub fn get_bar() -> &'static LocalKey>> { + 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 +} diff --git a/packages/subsecond/subsecond-tests/cross-tls-test/Cargo.toml b/packages/subsecond/subsecond-tests/cross-tls-test/Cargo.toml new file mode 100644 index 000000000..6e1df6cf2 --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-test/Cargo.toml @@ -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" } diff --git a/packages/subsecond/subsecond-tests/cross-tls-test/README.md b/packages/subsecond/subsecond-tests/cross-tls-test/README.md new file mode 100644 index 000000000..400e65b00 --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-test/README.md @@ -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 +``` diff --git a/packages/subsecond/subsecond-tests/cross-tls-test/src/main.rs b/packages/subsecond/subsecond-tests/cross-tls-test/src/main.rs new file mode 100644 index 000000000..e3fe649a2 --- /dev/null +++ b/packages/subsecond/subsecond-tests/cross-tls-test/src/main.rs @@ -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 = 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); + }); + }); + } +} diff --git a/packages/subsecond/subsecond/src/lib.rs b/packages/subsecond/subsecond/src/lib.rs index 1415eb1ff..5f311e982 100644 --- a/packages/subsecond/subsecond/src/lib.rs +++ b/packages/subsecond/subsecond/src/lib.rs @@ -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//lib/