diff --git a/changelog.d/5694-state-value-native.md b/changelog.d/5694-state-value-native.md new file mode 100644 index 0000000000..1c51e176ab --- /dev/null +++ b/changelog.d/5694-state-value-native.md @@ -0,0 +1,3 @@ +Fixed native `State.value` reads returning `undefined`, which made TextField and +SecureField callbacks appear to receive empty values even though their native +change handlers delivered the correct string. diff --git a/changelog.d/5731-embed-project-root.md b/changelog.d/5731-embed-project-root.md new file mode 100644 index 0000000000..7759aa5e87 --- /dev/null +++ b/changelog.d/5731-embed-project-root.md @@ -0,0 +1 @@ +Fixed `--embed` and configured embed patterns resolving relative to an entry file's directory instead of the package/project root. diff --git a/changelog.d/5742-android-x86-64.md b/changelog.d/5742-android-x86-64.md new file mode 100644 index 0000000000..11faa90525 --- /dev/null +++ b/changelog.d/5742-android-x86-64.md @@ -0,0 +1,5 @@ +Fixed `perry compile --target android-x86_64` to use the x86_64 Android +runtime, NDK linker/JNI target, UI library, native-addon architecture, metadata, +lockfile, output-path, and post-link paths consistently instead of falling back +to host or arm64 behavior. Android cross-compilation also now uses libc's +platform-native `ioctl` request type when building PTY support. diff --git a/changelog.d/5884-windows-bloomview-dpi.md b/changelog.d/5884-windows-bloomview-dpi.md new file mode 100644 index 0000000000..bc34e0fc7c --- /dev/null +++ b/changelog.d/5884-windows-bloomview-dpi.md @@ -0,0 +1,4 @@ +Fixed Windows DPI awareness being initialized after the hidden widget parking +window was created. Top-level windows and BloomView surfaces now agree on +physical sizing above 100% display scaling, and BloomView dimensions consistently +use logical 96-DPI pixels. diff --git a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs index b2030377d1..9dcfaaf46e 100644 --- a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs +++ b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs @@ -37,6 +37,11 @@ pub(crate) fn is_native_dispatch_member(module: &str, class: &str, prop: &str) - // Data getters resolved by FFI. "blob" => is_blob_getter_name(prop), "fetch" => is_fetch_response_getter_name(prop), + // `State` is an opaque perry/ui handle, so `.value` must invoke the + // platform getter instead of falling through to an own-property read + // on the NaN-boxed handle. Other perry/ui members are methods and reach + // dispatch through the call-expression path. + "perry/ui" => class == "State" && prop == "value", // Web Streams: only the getter list reaches dispatch (methods are // PropertyGet bound-method reads). "readable_stream" @@ -588,4 +593,11 @@ mod tests { "custom" )); } + + #[test] + fn perry_ui_state_dispatches_value_getter_only() { + assert!(is_native_dispatch_member("perry/ui", "State", "value")); + assert!(!is_native_dispatch_member("perry/ui", "State", "custom")); + assert!(!is_native_dispatch_member("perry/ui", "Canvas", "value")); + } } diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index 1c91f89147..53ba6f62ac 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -508,6 +508,53 @@ fn test_plain_class_to_class_heritage_keeps_static_extends_name() { ); } +/// #5694: `State` is a native `perry/ui` handle, so reading `.value` must +/// remain a zero-argument native getter call after local-native rewriting. +#[test] +fn test_perry_ui_state_value_uses_native_getter() { + use crate::ir::{clear_current_module_source, Expr, Stmt}; + use crate::js_transform::fix_local_native_instances; + + let source = r#" + import { State } from "perry/ui"; + + function main() { + const text = State(""); + return text.value; + } + "#; + let module = + perry_parser::parse_typescript(source, "state_value.ts").expect("source should parse"); + let mut hir = + super::lower_module(&module, "test", "state_value.ts").expect("source should lower"); + clear_current_module_source(); + fix_local_native_instances(&mut hir); + + let main = hir + .functions + .iter() + .find(|function| function.name == "main") + .expect("main function"); + let value = main.body.iter().find_map(|stmt| match stmt { + Stmt::Return(Some(expr)) => Some(expr), + _ => None, + }); + + assert!( + matches!( + value, + Some(Expr::NativeMethodCall { + module, + class_name: None, + object: Some(_), + method, + args, + }) if module == "perry/ui" && method == "value" && args.is_empty() + ), + "State.value must lower through perry_ui_state_get, got: {value:#?}" + ); +} + /// #6679: a NAMED class EXPRESSION's `.name` is its own explicit name /// (`Named` in `const B = class Named {}`), not the outer binding name. Per /// spec a named class expression is not an anonymous function definition, so diff --git a/crates/perry-runtime/src/pty/native.rs b/crates/perry-runtime/src/pty/native.rs index 2bfd7797ed..34ac7f4bba 100644 --- a/crates/perry-runtime/src/pty/native.rs +++ b/crates/perry-runtime/src/pty/native.rs @@ -199,7 +199,10 @@ pub(crate) fn spawn_in_pty(req: &PtySpawnRequest) -> io::Result { // Child. Async-signal-safe calls ONLY from here to execve. unsafe { libc::setsid(); - libc::ioctl(slave, libc::TIOCSCTTY as libc::c_ulong, 0); + // Infer libc's platform-specific `Ioctl` type. Android x86_64 + // uses `c_int` while BSD/macOS uses `c_ulong`; forcing either + // concrete type makes the other platform fail to compile. + libc::ioctl(slave, libc::TIOCSCTTY as _, 0); libc::dup2(slave, 0); libc::dup2(slave, 1); libc::dup2(slave, 2); @@ -255,7 +258,7 @@ pub(crate) fn wait_child(pid: i32) -> (Option, Option) { /// foreground process group. pub(crate) fn resize_pty(master: RawFd, cols: u16, rows: u16) -> bool { let ws = winsize(cols, rows); - unsafe { libc::ioctl(master, libc::TIOCSWINSZ as libc::c_ulong, &ws) == 0 } + unsafe { libc::ioctl(master, libc::TIOCSWINSZ as _, &ws) == 0 } } /// `kill(2)` — deliver `signo` to `pid`. @@ -267,7 +270,7 @@ pub(crate) fn signal_pid(pid: i32, signo: i32) -> bool { #[cfg(test)] pub(crate) fn read_winsize(fd: RawFd) -> Option<(u16, u16)> { let mut ws = winsize(0, 0); - let rc = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ as libc::c_ulong, &mut ws) }; + let rc = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ as _, &mut ws) }; if rc == 0 { Some((ws.ws_col, ws.ws_row)) } else { diff --git a/crates/perry-ui-windows/src/app.rs b/crates/perry-ui-windows/src/app.rs index 4afd974046..1bfde46c33 100644 --- a/crates/perry-ui-windows/src/app.rs +++ b/crates/perry-ui-windows/src/app.rs @@ -50,6 +50,12 @@ const TEST_EXIT_TIMER_ID: usize = 9997; /// Global DPI scale factor (1.0 at 96 DPI, 1.5 at 144 DPI, 2.0 at 192 DPI). static DPI_SCALE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +/// Process DPI awareness must be selected before the first HWND is created. +/// Widget constructors run while `App({ body: ... })` arguments are evaluated, +/// so the hidden parking HWND can predate `app_create` (#5884). +#[cfg(target_os = "windows")] +static DPI_INIT: std::sync::Once = std::sync::Once::new(); + /// Get the system DPI for the primary monitor. /// /// Routes through `crate::dpi_compat::get_system_dpi_compat`, which uses @@ -66,8 +72,15 @@ fn set_dpi_scale(scale: f64) { DPI_SCALE.store(scale.to_bits(), std::sync::atomic::Ordering::Relaxed); } -/// Get the DPI scale factor. Returns 1.0 if not set. +fn dpi_scale_from_dpi(dpi: u32) -> f64 { + dpi.max(96) as f64 / 96.0 +} + +/// Get the DPI scale factor, initializing process awareness on first use. pub fn get_dpi_scale() -> f64 { + #[cfg(target_os = "windows")] + ensure_dpi_initialized(); + let bits = DPI_SCALE.load(std::sync::atomic::Ordering::Relaxed); if bits == 0 { 1.0 @@ -76,6 +89,26 @@ pub fn get_dpi_scale() -> f64 { } } +/// Convert a logical 96-DPI dimension to physical pixels using the active +/// process/monitor scale. +pub(crate) fn scale_logical_px(px: f64) -> i32 { + scale_logical_px_by(px, get_dpi_scale()) +} + +fn scale_logical_px_by(px: f64, scale: f64) -> i32 { + (px * scale.max(1.0)).round() as i32 +} + +/// Select process DPI awareness and cache the initial scale before any window +/// (including the hidden widget parking window) is created. +#[cfg(target_os = "windows")] +pub(crate) fn ensure_dpi_initialized() { + DPI_INIT.call_once(|| { + crate::dpi_compat::set_process_dpi_awareness_compat(); + set_dpi_scale(dpi_scale_from_dpi(get_system_dpi())); + }); +} + thread_local! { static TIMER_TICK_NEEDED: std::cell::Cell = std::cell::Cell::new(false); } @@ -210,22 +243,15 @@ fn to_wide(s: &str) -> Vec { /// Create an app window. Returns app handle (1-based). pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 { let title = str_from_header(title_ptr); - let w = if width > 0.0 { width as i32 } else { 800 }; - let h = if height > 0.0 { height as i32 } else { 600 }; + let w = if width > 0.0 { width } else { 800.0 }; + let h = if height > 0.0 { height } else { 600.0 }; #[cfg(target_os = "windows")] { unsafe { - // Enable DPI awareness — Win10 1607+ best, Win8.1 ok, Win7 silent - // fallback to system DPI. See dpi_compat.rs (issue #303). - crate::dpi_compat::set_process_dpi_awareness_compat(); - - // Scale window size by system DPI (96 = 100%, 144 = 150%, 192 = 200%) - let dpi = get_system_dpi(); - let scale = dpi as f64 / 96.0; - let w = (w as f64 * scale) as i32; - let h = (h as f64 * scale) as i32; - set_dpi_scale(scale); + ensure_dpi_initialized(); + let w = scale_logical_px(w); + let h = scale_logical_px(h); // Initialize common controls let icc = INITCOMMONCONTROLSEX { @@ -1420,6 +1446,28 @@ unsafe extern "system" fn wnd_proc( } } +#[cfg(test)] +mod dpi_tests { + use super::{dpi_scale_from_dpi, scale_logical_px_by}; + + #[test] + fn converts_logical_window_sizes_at_common_scales() { + assert_eq!(dpi_scale_from_dpi(96), 1.0); + assert_eq!(dpi_scale_from_dpi(144), 1.5); + assert_eq!(dpi_scale_from_dpi(192), 2.0); + assert_eq!(scale_logical_px_by(800.0, 1.5), 1200); + assert_eq!(scale_logical_px_by(600.0, 1.5), 900); + assert_eq!(scale_logical_px_by(100.9, 1.5), 151); + } + + #[test] + fn invalid_or_sub_96_dpi_values_do_not_shrink_ui() { + assert_eq!(dpi_scale_from_dpi(0), 1.0); + assert_eq!(dpi_scale_from_dpi(72), 1.0); + assert_eq!(scale_logical_px_by(100.0, 0.0), 100); + } +} + /// Register a system-wide global hotkey. Uses Win32 `RegisterHotKey` API. /// `key_ptr` is a StringHeader pointer to the key (e.g., "s"). /// `modifiers` is a bitfield: 1=Cmd(->Ctrl), 2=Shift, 4=Option(->Alt), 8=Control(->Ctrl). diff --git a/crates/perry-ui-windows/src/widgets/bloomview.rs b/crates/perry-ui-windows/src/widgets/bloomview.rs index aaef182e41..e9bb79753a 100644 --- a/crates/perry-ui-windows/src/widgets/bloomview.rs +++ b/crates/perry-ui-windows/src/widgets/bloomview.rs @@ -86,6 +86,12 @@ pub fn create(width: f64, height: f64) -> i64 { #[cfg(target_os = "windows")] { ensure_class_registered(); + let parking_hwnd = super::get_parking_hwnd(); + // Constructor dimensions are logical (96-DPI) pixels, matching App and + // widgetSetFixedWidth/Height. Creating the parking window first also + // selects process DPI awareness before either HWND exists. + let width_px = crate::app::scale_logical_px(width); + let height_px = crate::app::scale_logical_px(height); let class_name = to_wide("PerryBloomView"); let window_text = to_wide(""); unsafe { @@ -102,9 +108,9 @@ pub fn create(width: f64, height: f64) -> i64 { WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, 0, 0, - width as i32, - height as i32, - Some(super::get_parking_hwnd()), + width_px, + height_px, + Some(parking_hwnd), None, Some(HINSTANCE::from(hinstance)), None, @@ -116,8 +122,8 @@ pub fn create(width: f64, height: f64) -> i64 { let handle = register_widget_with_layout(hwnd, WidgetKind::Image, 0.0, (0.0, 0.0, 0.0, 0.0)); // Reserve the requested size so the view is visible in a layout. - set_fixed_width(handle, width as i32); - set_fixed_height(handle, height as i32); + set_fixed_width(handle, width_px); + set_fixed_height(handle, height_px); handle } } diff --git a/crates/perry-ui-windows/src/widgets/mod.rs b/crates/perry-ui-windows/src/widgets/mod.rs index ea8e643141..b8cf7e068f 100644 --- a/crates/perry-ui-windows/src/widgets/mod.rs +++ b/crates/perry-ui-windows/src/widgets/mod.rs @@ -218,6 +218,10 @@ pub fn get_parking_hwnd() -> HWND { fn to_wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } + // `App({ body: Widget() })` constructs the body before app_create runs. + // Select DPI awareness before this first hidden HWND locks the process + // into DPI-unaware virtualization (#5884). + crate::app::ensure_dpi_initialized(); PARKING_HWND.with(|cell| { let mut opt = cell.borrow_mut(); if let Some(hwnd) = *opt { diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index a00c03ab8f..54c2a2e612 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -9,6 +9,7 @@ use crate::OutputFormat; // `compile/` directory. The `compile.rs` orchestrator stays as the // public API surface; helpers move to focused modules so unrelated // changes don't churn this file. +pub(crate) mod android_target; mod app_metadata; mod apple_codesign; mod apple_info_plist; @@ -50,6 +51,7 @@ mod strip_dedup; mod targets; pub mod well_known; pub(crate) mod widget_build; +use android_target::{android_target, is_android_target}; use app_metadata::rust_target_triple; // apple_info_plist helpers used through bundle_ios (no direct uses in // compile.rs anymore now that the iOS bundle code moved out). diff --git a/crates/perry/src/commands/compile/android_target.rs b/crates/perry/src/commands/compile/android_target.rs new file mode 100644 index 0000000000..8b00c216b1 --- /dev/null +++ b/crates/perry/src/commands/compile/android_target.rs @@ -0,0 +1,63 @@ +//! Canonical Android target properties used by the compile pipeline. +//! +//! Keep architecture-sensitive values together: adding a new Android target +//! must not let codegen, Cargo, clang, native manifests, and APK ABI placement +//! silently disagree. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct AndroidTarget { + pub rust_triple: &'static str, + pub clang_target: &'static str, + pub manifest_arch: &'static str, +} + +const ARM64: AndroidTarget = AndroidTarget { + rust_triple: "aarch64-linux-android", + clang_target: "aarch64-linux-android24", + manifest_arch: "arm64", +}; + +const X86_64: AndroidTarget = AndroidTarget { + rust_triple: "x86_64-linux-android", + clang_target: "x86_64-linux-android24", + manifest_arch: "x64", +}; + +pub(crate) fn android_target(target: Option<&str>) -> Option { + match target { + // Wear OS currently uses the same arm64 NDK build as Android devices. + Some("android") | Some("wearos") => Some(ARM64), + Some("android-x86_64") => Some(X86_64), + _ => None, + } +} + +pub(crate) fn is_android_target(target: Option<&str>) -> bool { + android_target(target).is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_every_android_architecture_consistently() { + let arm64 = android_target(Some("android")).unwrap(); + assert_eq!(arm64.rust_triple, "aarch64-linux-android"); + assert_eq!(arm64.clang_target, "aarch64-linux-android24"); + assert_eq!(arm64.manifest_arch, "arm64"); + + let x86_64 = android_target(Some("android-x86_64")).unwrap(); + assert_eq!(x86_64.rust_triple, "x86_64-linux-android"); + assert_eq!(x86_64.clang_target, "x86_64-linux-android24"); + assert_eq!(x86_64.manifest_arch, "x64"); + + assert_eq!(android_target(Some("wearos")), Some(arm64)); + } + + #[test] + fn does_not_treat_codegen_only_widget_target_as_native_android() { + assert!(!is_android_target(Some("android-widget"))); + assert!(!is_android_target(None)); + } +} diff --git a/crates/perry/src/commands/compile/app_metadata.rs b/crates/perry/src/commands/compile/app_metadata.rs index 62619f14a9..e31f5da4c9 100644 --- a/crates/perry/src/commands/compile/app_metadata.rs +++ b/crates/perry/src/commands/compile/app_metadata.rs @@ -9,15 +9,17 @@ use std::fs; use std::path::Path; +use super::{android_target, is_android_target}; + pub(super) fn target_bundle_section(target: Option<&str>) -> Option<&'static str> { + if is_android_target(target) { + return Some("android"); + } match target { Some("ios") | Some("ios-simulator") => Some("ios"), Some("visionos") | Some("visionos-simulator") => Some("visionos"), Some("watchos") | Some("watchos-simulator") => Some("watchos"), Some("tvos") | Some("tvos-simulator") => Some("tvos"), - Some("android") => Some("android"), - // Wear OS reuses the [android] perry.toml section (bundle_id, etc.). - Some("wearos") => Some("android"), Some("macos") => Some("macos"), // WinUI shares the [windows] perry.toml section (#4680). Some("windows") | Some("windows-winui") => Some("windows"), @@ -154,6 +156,9 @@ pub(super) fn read_app_metadata( /// Get the Rust target triple for a given perry target string pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { + if let Some(android) = android_target(target) { + return Some(android.rust_triple); + } match target { Some("ios-simulator") | Some("ios-widget-simulator") => Some("aarch64-apple-ios-sim"), Some("ios") | Some("ios-widget") => Some("aarch64-apple-ios"), @@ -172,9 +177,6 @@ pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { Some("tvos") => Some("aarch64-apple-tvos"), Some("harmonyos") => Some("aarch64-unknown-linux-ohos"), Some("harmonyos-simulator") => Some("x86_64-unknown-linux-ohos"), - Some("android") => Some("aarch64-linux-android"), - // Wear OS is Android-on-a-watch: same arm64 Android .so + toolchain. - Some("wearos") => Some("aarch64-linux-android"), Some("linux") | Some("linux-x86_64") => Some("x86_64-unknown-linux-gnu"), Some("linux-arm64") | Some("linux-aarch64") => Some("aarch64-unknown-linux-gnu"), // Fully-static musl targets (#4826). The perry-runtime / perry-stdlib @@ -189,12 +191,24 @@ pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { #[cfg(test)] mod app_metadata_tests { - use super::read_app_metadata; + use super::{read_app_metadata, rust_target_triple, target_bundle_section}; fn parse(src: &str) -> toml::Table { src.parse::().unwrap() } + #[test] + fn android_x86_64_uses_android_metadata_and_rust_target() { + assert_eq!( + target_bundle_section(Some("android-x86_64")), + Some("android") + ); + assert_eq!( + rust_target_triple(Some("android-x86_64")), + Some("x86_64-linux-android") + ); + } + #[test] fn reads_project_metadata_and_target_bundle_id() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index 98e4d24d28..1fe063e8e2 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -110,9 +110,14 @@ impl BuildCacheProbe { .join("build") .join(args.target.as_deref().unwrap_or("native")) .join(manifest_name); - let eligible = eligibility(args, project_root); + // Embed patterns, package hooks, and resource directories are rooted at + // the walked-up config root, not the entry file's parent directory. + // Keep cache eligibility/keying aligned with run_pipeline's embed + // resolution so `src/main.ts --embed ./dist/**` cannot reuse a binary + // keyed against the nonexistent `src/dist`. + let eligible = eligibility(args, cache_root); Self { - args_key: args_key(args, &output_path, project_root), + args_key: args_key(args, &output_path, cache_root), manifest_path, output_path, target_name: args.target.clone().unwrap_or_else(|| "native".to_string()), diff --git a/crates/perry/src/commands/compile/library_search.rs b/crates/perry/src/commands/compile/library_search.rs index 370b88ddb5..5128c9931d 100644 --- a/crates/perry/src/commands/compile/library_search.rs +++ b/crates/perry/src/commands/compile/library_search.rs @@ -29,7 +29,7 @@ use crate::OutputFormat; // `rust_target_triple` and `find_perry_workspace_root` still live in // the compile.rs orchestrator. Pull them in as private parent-module // items so the search helpers below can reach them. -use super::{find_perry_workspace_root, rust_target_triple}; +use super::{android_target, find_perry_workspace_root, is_android_target, rust_target_triple}; /// Resolve the host's Rust target triple by parsing `rustc -vV`. /// @@ -1223,8 +1223,8 @@ pub(super) fn find_ui_library(target: Option<&str>) -> Option { let lib_name = match target { Some("ios-simulator") | Some("ios") => "libperry_ui_ios.a", Some("visionos-simulator") | Some("visionos") => "libperry_ui_visionos.a", - // Wear OS reuses the Android View backend. - Some("android") | Some("wearos") => "libperry_ui_android.a", + // Wear OS and every Android architecture reuse the Android View backend. + target if is_android_target(target) => "libperry_ui_android.a", Some("watchos-simulator") | Some("watchos") => "libperry_ui_watchos.a", Some("tvos-simulator") | Some("tvos") => "libperry_ui_tvos.a", Some("linux") => "libperry_ui_gtk4.a", @@ -1332,11 +1332,10 @@ pub(super) fn find_harmonyos_sdk() -> Option { /// 24 (Android 7.0) — matches the existing `platform_cmd.rs` / /// `link/mod.rs` JNI stub compile invocations. pub(super) fn android_cross_env(ndk_home: &Path, target: Option<&str>) -> Vec<(String, String)> { - let (triple, clang_target) = match target { - Some("android-x86_64") => ("x86_64-linux-android", "x86_64-linux-android24"), - // `android` (default) is the arm64 device target. - _ => ("aarch64-linux-android", "aarch64-linux-android24"), - }; + let android = android_target(target) + .expect("android_cross_env must only be called for an Android target"); + let triple = android.rust_triple; + let clang_target = android.clang_target; // NDK ships per-host prebuilt toolchains. Tag must match the build // machine, NOT the target — Windows-host builds were falling through @@ -1399,6 +1398,29 @@ pub(super) fn android_cross_env(ndk_home: &Path, target: Option<&str>) -> Vec<(S ] } +#[cfg(test)] +mod android_cross_env_tests { + use super::android_cross_env; + use std::collections::HashMap; + use std::path::Path; + + #[test] + fn x86_64_uses_matching_ndk_wrappers_and_cargo_linker() { + let env: HashMap<_, _> = android_cross_env(Path::new("/ndk"), Some("android-x86_64")) + .into_iter() + .collect(); + let cc = env + .get("CC_x86_64-linux-android") + .expect("hyphenated cc-rs target key"); + assert!(cc.contains("x86_64-linux-android24-clang"), "{cc}"); + let linker = env + .get("CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER") + .expect("Cargo target linker"); + assert!(linker.contains("x86_64-linux-android24-clang"), "{linker}"); + assert!(env.contains_key("AR_x86_64_linux_android")); + } +} + /// Cross-compile env vars to pass to `cargo build` so `cc-rs` picks up the /// OHOS SDK's clang + musl sysroot for any C source in dependency build.rs /// scripts (notably `libmimalloc-sys`, which needs `pthread.h`). @@ -1558,7 +1580,7 @@ pub(super) fn find_geisterhand_ui(target: Option<&str>) -> Option { "libperry_ui_ios.a" } else if matches!(target, Some("visionos-simulator") | Some("visionos")) { return None; - } else if matches!(target, Some("android") | Some("wearos")) { + } else if is_android_target(target) { "libperry_ui_android.a" } else if matches!(target, Some("linux")) || cfg!(target_os = "linux") { "libperry_ui_gtk4.a" @@ -1581,7 +1603,7 @@ pub(super) fn build_geisterhand_libs(target: Option<&str>, format: OutputFormat) // Determine which UI crate to build based on target platform let ui_crate = match target { Some("ios-simulator") | Some("ios") => "perry-ui-ios", - Some("android") | Some("wearos") => "perry-ui-android", + target if is_android_target(target) => "perry-ui-android", Some("linux") => "perry-ui-gtk4", Some("windows-winui") => "perry-ui-windows-winui", Some("windows") => "perry-ui-windows", @@ -1655,10 +1677,7 @@ pub(super) fn build_geisterhand_libs(target: Option<&str>, format: OutputFormat) // `libperry_app.so` on Android; force global-dynamic TLS so the IE // model doesn't crash at load. (RUSTFLAGS scopes to the cross target, // so host build-scripts/proc-macros are unaffected.) - if matches!( - target, - Some("android") | Some("android-x86_64") | Some("wearos") - ) { + if is_android_target(target) { let tls_flag = super::optimized_libs::android_global_dynamic_tls_rustflag(&mut cargo_cmd); cargo_cmd.env("RUSTFLAGS", tls_flag); } diff --git a/crates/perry/src/commands/compile/link/build_and_run.rs b/crates/perry/src/commands/compile/link/build_and_run.rs index 80ee577879..d61a113d1d 100644 --- a/crates/perry/src/commands/compile/link/build_and_run.rs +++ b/crates/perry/src/commands/compile/link/build_and_run.rs @@ -59,8 +59,8 @@ pub(crate) fn build_and_run_link( let is_ios = matches!(target, Some("ios-simulator") | Some("ios")); let is_visionos = matches!(target, Some("visionos-simulator") | Some("visionos")); - // Wear OS links exactly like Android (same triple, NDK, cdylib + TLS model). - let is_android = matches!(target, Some("android") | Some("wearos")); + // Wear OS and every Android architecture share the NDK/cdylib link path. + let is_android = is_android_target(target); let is_harmonyos = matches!(target, Some("harmonyos") | Some("harmonyos-simulator")); let is_linux = matches!(target, Some(t) if t.starts_with("linux")) || (target.is_none() && cfg!(target_os = "linux")); @@ -889,8 +889,11 @@ pub(crate) fn build_and_run_link( // spawn here is silent: `stub_ok` goes false and the link then dies on an // undefined `JNI_GetCreatedJavaVMs`). `-target` is passed below. let ndk_clang = ndk_clang_path(&ndk_home); + let clang_target = android_target(target) + .expect("Android JNI stub branch requires a recognized Android target") + .clang_target; let stub_ok = Command::new(&ndk_clang) - .args(["-c", "-fPIC", "-target", "aarch64-linux-android24"]) + .args(["-c", "-fPIC", "-target", clang_target]) .arg("-o") .arg(&stub_o) .arg(&stub_c) @@ -1148,6 +1151,9 @@ pub(crate) fn build_and_run_link( "cargo build --release -p perry-ui-ios --target aarch64-apple-ios-sim", ) } else if is_android { + let triple = android_target(target) + .expect("Android library diagnostic requires a recognized target") + .rust_triple; ( "libperry_ui_android.a", // Two audiences here: a binary-install user (WinGet/Scoop/npm, @@ -1156,7 +1162,11 @@ pub(crate) fn build_and_run_link( // for workspace users. #1529 — the dlopen'd cdylib needs the // global-dynamic TLS model, and `tls-model` is `-Z`-gated, so // RUSTC_BOOTSTRAP=1 lets it through on a stable rustc. - "either (a) drop libperry_ui_android.a next to perry under aarch64-linux-android/release/ from the prebuilt perry-cross-aarch64-linux-android.tar.gz release asset, or (b) from a perry checkout: RUSTC_BOOTSTRAP=1 RUSTFLAGS=\"-Z tls-model=global-dynamic\" cargo build --release -p perry-ui-android --target aarch64-linux-android", + if triple == "x86_64-linux-android" { + "from a perry checkout: RUSTC_BOOTSTRAP=1 RUSTFLAGS=\"-Z tls-model=global-dynamic\" cargo build --release -p perry-ui-android --target x86_64-linux-android" + } else { + "either (a) drop libperry_ui_android.a next to perry under aarch64-linux-android/release/ from the prebuilt perry-cross-aarch64-linux-android.tar.gz release asset, or (b) from a perry checkout: RUSTC_BOOTSTRAP=1 RUSTFLAGS=\"-Z tls-model=global-dynamic\" cargo build --release -p perry-ui-android --target aarch64-linux-android" + }, ) } else if is_linux { ( @@ -1355,8 +1365,16 @@ pub(crate) fn build_and_run_link( super::super::optimized_libs::android_global_dynamic_tls_rustflag( &mut cargo_cmd, ); + let rustflags_env = format!( + "CARGO_TARGET_{}_RUSTFLAGS", + android_target(target) + .expect("Android native build requires a recognized target") + .rust_triple + .to_uppercase() + .replace('-', "_") + ); cargo_cmd.env( - "CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS", + rustflags_env, format!("-C link-arg=-Wl,-z,max-page-size=16384 {tls_flag}"), ); } diff --git a/crates/perry/src/commands/compile/link/mod.rs b/crates/perry/src/commands/compile/link/mod.rs index f800740a5c..80671ec47e 100644 --- a/crates/perry/src/commands/compile/link/mod.rs +++ b/crates/perry/src/commands/compile/link/mod.rs @@ -31,12 +31,13 @@ use std::process::Command; use crate::OutputFormat; use super::{ - apple_sdk_version, build_geisterhand_libs, dedup_native_lib_for_tier3, dedup_runtime_for_tier3, - dedup_stdlib_for_tier3, dedup_ui_lib_against_linked_libs, find_geisterhand_library, - find_geisterhand_runtime, find_geisterhand_stdlib, find_geisterhand_ui, find_lld_link, - find_llvm_tool, find_msvc_lib_paths, find_msvc_link_exe, find_perry_windows_sdk, - find_stdlib_library, find_ui_library, find_visionos_swift_runtime, find_watchos_swift_runtime, - localize_stdlib_stub_symbols, localize_stdlib_stub_symbols_for_windows, rust_target_triple, + android_target, apple_sdk_version, build_geisterhand_libs, dedup_native_lib_for_tier3, + dedup_runtime_for_tier3, dedup_stdlib_for_tier3, dedup_ui_lib_against_linked_libs, + find_geisterhand_library, find_geisterhand_runtime, find_geisterhand_stdlib, + find_geisterhand_ui, find_lld_link, find_llvm_tool, find_msvc_lib_paths, find_msvc_link_exe, + find_perry_windows_sdk, find_stdlib_library, find_ui_library, find_visionos_swift_runtime, + find_watchos_swift_runtime, is_android_target, localize_stdlib_stub_symbols, + localize_stdlib_stub_symbols_for_windows, rust_target_triple, strip_bundled_runtime_from_well_known_lib, strip_bundled_shared_deps_from_well_known_lib, strip_duplicate_objects_from_lib, strip_duplicate_objects_from_well_known_lib, windows_pe_subsystem_flag, windows_subsystem_needs_ui, CompilationContext, @@ -454,9 +455,9 @@ pub(super) fn response_file_contents(args: &[String], msvc: bool) -> String { /// path the NDK doesn't ship. /// /// #5740 — this drives `clang` itself rather than the NDK's per-target wrapper -/// (`aarch64-linux-android24-clang`), which is a two-line shim that execs -/// `clang --target=aarch64-linux-android24 "$@"`. Every caller here already -/// passes `-target aarch64-linux-android24` explicitly, so the two are +/// (`24-clang`), which is a two-line shim that execs +/// `clang --target=24 "$@"`. Every caller here already passes the +/// architecture-specific `-target` explicitly, so the two are /// equivalent — except on Windows, where the wrapper is a `.cmd` batch file: /// Rust's `Command` can only spawn a batch file through `cmd.exe`, which layers /// a second round of command-line quoting (plus the batch-escaping restrictions diff --git a/crates/perry/src/commands/compile/link/platform_cmd.rs b/crates/perry/src/commands/compile/link/platform_cmd.rs index 1acbdce425..04acc84924 100644 --- a/crates/perry/src/commands/compile/link/platform_cmd.rs +++ b/crates/perry/src/commands/compile/link/platform_cmd.rs @@ -590,17 +590,20 @@ pub fn select_linker_command( })?; // #1508 (per-host toolchain tag) + #5740 (drive `clang` directly rather // than the NDK's `.cmd`/shell wrapper) — see `ndk_clang_path`. The - // `-target aarch64-linux-android24` below is what the wrapper would have - // added, so nothing else changes. + // The explicit architecture-specific `-target` below is what the + // per-target wrapper would have added, so nothing else changes. let clang = ndk_clang_path(&ndk_home); if !PathBuf::from(&clang).exists() { return Err(anyhow!("Android NDK clang not found at: {}", clang)); } let mut c = Command::new(clang); + let clang_target = android_target(target) + .expect("Android linker branch requires a recognized Android target") + .clang_target; c.arg("-shared") .arg("-fPIC") .arg("-target") - .arg("aarch64-linux-android24") + .arg(clang_target) .arg("-Wl,-z,max-page-size=16384") .arg("-Wl,-z,separate-loadable-segments") // Prevent ELF symbol interposition: bind all symbols within the .so diff --git a/crates/perry/src/commands/compile/lock_scan.rs b/crates/perry/src/commands/compile/lock_scan.rs index ebee30985c..7faa9573f4 100644 --- a/crates/perry/src/commands/compile/lock_scan.rs +++ b/crates/perry/src/commands/compile/lock_scan.rs @@ -21,7 +21,7 @@ use anyhow::Result; use crate::OutputFormat; use super::resolve::{has_perry_native_library, parse_native_library_manifest}; -use super::CompilationContext; +use super::{is_android_target, CompilationContext}; /// #498 - discover every `perry.nativeLibrary` archive a build of /// the project at `project_root` would consume. Scans every @@ -142,6 +142,9 @@ fn derive_target_key(target: Option<&str>) -> String { "x86" => "i686", other => other, }; + if is_android_target(target) { + return "android".to_string(); + } match target { None => format!("{}-{}", std::env::consts::OS, arch), Some("macos") => format!("macos-{}", arch), @@ -159,8 +162,6 @@ fn derive_target_key(target: Option<&str>) -> String { Some("watchos-simulator") => "watchos-simulator".to_string(), Some("visionos") => "visionos".to_string(), Some("visionos-simulator") => "visionos-simulator".to_string(), - // Wear OS reuses Android's resolved native-dependency set. - Some("android") | Some("wearos") => "android".to_string(), Some("harmonyos") => "harmonyos".to_string(), Some("harmonyos-simulator") => "harmonyos-simulator".to_string(), Some("web") => "web".to_string(), @@ -261,6 +262,7 @@ mod lock_integration_tests { assert_eq!(derive_target_key(Some("tvos")), "tvos"); assert_eq!(derive_target_key(Some("watchos")), "watchos"); assert_eq!(derive_target_key(Some("android")), "android"); + assert_eq!(derive_target_key(Some("android-x86_64")), "android"); } #[test] diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index 5528dad927..f6f1c8dcbf 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -7,7 +7,9 @@ use crate::commands::stdlib_features::{compute_required_features, features_to_ca use crate::OutputFormat; use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env, host_target_triple}; -use super::super::{find_perry_workspace_root, rust_target_triple, CompilationContext}; +use super::super::{ + find_perry_workspace_root, is_android_target, rust_target_triple, CompilationContext, +}; /// Rebuild perry-runtime + perry-stdlib in a single cargo invocation with /// the chosen Cargo features and panic mode, and return paths to the @@ -828,10 +830,7 @@ pub(crate) fn build_optimized_libs( // #1508: same shape for Android — cc-rs can't find the NDK clang // otherwise (silent on Unix where `clang` happens to exist, hard fail // on Windows with `clang.exe not found`). - if matches!( - target, - Some("android") | Some("android-x86_64") | Some("wearos") - ) { + if is_android_target(target) { if let Some(ndk) = std::env::var_os("ANDROID_NDK_HOME") { for (k, v) in super::super::library_search::android_cross_env(std::path::Path::new(&ndk), target) @@ -911,10 +910,7 @@ pub(crate) fn build_optimized_libs( // shadow stack), so those IE TLS relocations get baked into the final // cdylib. Force global-dynamic so the dynamic linker can resolve TLS // slots after the process has started. - if matches!( - target, - Some("android") | Some("android-x86_64") | Some("wearos") - ) { + if is_android_target(target) { rustflags.push(android_global_dynamic_tls_rustflag(&mut cargo_cmd).to_string()); } // Set RUSTFLAGS whenever we have flags to pass, and also whenever a CPU @@ -1167,7 +1163,7 @@ pub(crate) fn build_optimized_libs( | Some("ios-widget") | Some("ios-widget-simulator") => "perry-ui-ios", Some("visionos-simulator") | Some("visionos") => "perry-ui-visionos", - Some("android") | Some("wearos") => "perry-ui-android", + target if is_android_target(target) => "perry-ui-android", Some("watchos-simulator") | Some("watchos") => "perry-ui-watchos", Some("tvos-simulator") | Some("tvos") => "perry-ui-tvos", Some("linux") => "perry-ui-gtk4", diff --git a/crates/perry/src/commands/compile/output_path.rs b/crates/perry/src/commands/compile/output_path.rs index e37f81f4dd..30ca8b5926 100644 --- a/crates/perry/src/commands/compile/output_path.rs +++ b/crates/perry/src/commands/compile/output_path.rs @@ -9,6 +9,8 @@ use std::path::PathBuf; +use super::is_android_target; + /// The output file a compile targets when no `-o` was given. /// /// `stem` is the already-sanitized entry-file stem (`app.ts` → `app`). @@ -41,20 +43,12 @@ pub(super) fn default_output_path( } else { PathBuf::from(format!("lib{}.a", stem)) } - } else if matches!( - target, - Some("harmonyos") - | Some("harmonyos-simulator") - // #5740 — Android (and Wear OS, which links identically: same NDK, - // same triple, same cdylib shape) links with `-shared` and ships as - // a `.so` that `PerryActivity` dlopens; there is no standalone - // executable shipping shape. Without this arm the default output was - // the bare stem (`app`), which fails the link outright in a stock - // Android project — `app/` is already a directory there, so lld - // reports `cannot open output file app: Is a directory`. - | Some("android") - | Some("wearos") - ) { + } else if matches!(target, Some("harmonyos") | Some("harmonyos-simulator")) + || is_android_target(target) + { + // #5740 — Android targets link with `-shared` and ship as a `.so` + // that `PerryActivity` dlopens; there is no standalone executable + // shipping shape. // HarmonyOS apps ship as .so loaded by the ArkTS runtime via // napi_module_register — there is no standalone executable // shipping shape. `lib` prefix matches the dlopen name used by @@ -109,6 +103,10 @@ mod tests { fn android_defaults_to_shared_library() { assert_eq!(exe(Some("android"), "app"), PathBuf::from("libapp.so")); assert_eq!(exe(Some("android"), "hello"), PathBuf::from("libhello.so")); + assert_eq!( + exe(Some("android-x86_64"), "app"), + PathBuf::from("libapp.so") + ); } /// Wear OS links exactly like Android (same NDK, triple, cdylib + TLS diff --git a/crates/perry/src/commands/compile/post_link.rs b/crates/perry/src/commands/compile/post_link.rs index 2b56302909..47dc3c1f83 100644 --- a/crates/perry/src/commands/compile/post_link.rs +++ b/crates/perry/src/commands/compile/post_link.rs @@ -9,7 +9,7 @@ use crate::OutputFormat; use std::fs; use std::path::{Path, PathBuf}; -use super::{CompilationContext, ObjectCache}; +use super::{is_android_target, CompilationContext, ObjectCache}; /// Strip debug symbols from the final binary (reduces size /// significantly). Skipped for: dylib output, every cross- @@ -41,10 +41,9 @@ pub(super) fn strip_final_binary( || is_tvos || is_watchos || is_harmonyos - || target == Some("android") - // Wear OS ships the same dlopen'd .so — stripping would drop the + // Android and Wear OS ship a dlopen'd .so — stripping would drop the // no_mangle JNI/FFI symbols PerryActivity resolves at load. - || target == Some("wearos") + || is_android_target(target) || std::env::var("PERRY_DEBUG_SYMBOLS").is_ok() { return; diff --git a/crates/perry/src/commands/compile/resolve/native_library.rs b/crates/perry/src/commands/compile/resolve/native_library.rs index 2ee050126a..c1d706ee8a 100644 --- a/crates/perry/src/commands/compile/resolve/native_library.rs +++ b/crates/perry/src/commands/compile/resolve/native_library.rs @@ -10,8 +10,8 @@ use perry_api_manifest::{ }; use super::super::{ - NativeBackend, NativeBackendConfig, NativeBackendPackageMetadata, NativeFunctionDecl, - NativeLibraryManifest, TargetNativeConfig, + android_target, is_android_target, NativeBackend, NativeBackendConfig, + NativeBackendPackageMetadata, NativeFunctionDecl, NativeLibraryManifest, TargetNativeConfig, }; pub(crate) fn validate_native_library_manifest_value( @@ -52,11 +52,12 @@ pub(crate) fn validate_native_library_manifest_value( } pub(super) fn native_manifest_target_key(target: Option<&str>) -> &'static str { + if is_android_target(target) { + return "android"; + } match target { Some("ios-simulator") | Some("ios") => "ios", Some("visionos-simulator") | Some("visionos") => "visionos", - // Wear OS resolves native-addon config from the [android] target. - Some("android") | Some("wearos") => "android", Some("tvos-simulator") | Some("tvos") => "tvos", Some("watchos-simulator") | Some("watchos") => "watchos", Some("harmonyos-simulator") | Some("harmonyos") => "harmonyos", @@ -1308,6 +1309,9 @@ fn arch_for_target_key(target: Option<&str>) -> Option<&'static str> { if target.is_none() { return Some(host_arch_token()); } + if let Some(android) = android_target(target) { + return Some(android.manifest_arch); + } match target { // OS-level targets where both arm64 and x64 are real distribution // targets — surface the arch so wrappers can ship per-arch @@ -1315,7 +1319,6 @@ fn arch_for_target_key(target: Option<&str>) -> Option<&'static str> { Some("macos") => Some("arm64"), Some("linux") => Some("x64"), Some("windows") | Some("windows-winui") => Some("x64"), - Some("android") | Some("wearos") => Some("arm64"), Some("harmonyos") => Some("arm64"), Some("harmonyos-simulator") => Some("x64"), // ios/tvos/watchos/visionos: device builds are always arm64 (or @@ -1341,6 +1344,20 @@ fn host_arch_token() -> &'static str { } } +#[cfg(test)] +mod android_target_tests { + use super::{arch_for_target_key, native_manifest_target_key}; + + #[test] + fn x86_64_android_uses_android_manifest_and_x64_override() { + assert_eq!( + native_manifest_target_key(Some("android-x86_64")), + "android" + ); + assert_eq!(arch_for_target_key(Some("android-x86_64")), Some("x64")); + } +} + /// Resolve a `prebuilt:` manifest entry to an absolute filesystem /// path. Returns `None` if the entry could not be resolved. /// diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index 542d8a21e9..33c925b6a6 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -1582,21 +1582,20 @@ pub fn run_with_parse_cache( .map(|f| f.trim().to_string()) .filter(|f| !f.is_empty()) .collect(); - let is_mobile = matches!( - target.as_deref(), - Some("ios") - | Some("ios-simulator") - | Some("visionos") - | Some("visionos-simulator") - | Some("android") - | Some("wearos") - | Some("watchos") - | Some("watchos-simulator") - | Some("tvos") - | Some("tvos-simulator") - | Some("harmonyos") - | Some("harmonyos-simulator") - ); + let is_mobile = is_android_target(target.as_deref()) + || matches!( + target.as_deref(), + Some("ios") + | Some("ios-simulator") + | Some("visionos") + | Some("visionos-simulator") + | Some("watchos") + | Some("watchos-simulator") + | Some("tvos") + | Some("tvos-simulator") + | Some("harmonyos") + | Some("harmonyos-simulator") + ); if is_mobile { features.retain(|f| f != "plugins"); } @@ -4719,7 +4718,7 @@ pub fn run_with_parse_cache( } // Platform detection for nm tool and symbol prefix let _is_ios = matches!(target.as_deref(), Some("ios-simulator") | Some("ios")); - let is_android = matches!(target.as_deref(), Some("android") | Some("wearos")); + let is_android = is_android_target(target.as_deref()); let is_harmonyos = matches!( target.as_deref(), Some("harmonyos") | Some("harmonyos-simulator") @@ -5219,7 +5218,11 @@ pub fn run_with_parse_cache( // `perry.embed` / `[compile] embed`, expand globs/directories relative to // the project root, and emit a registration object linked alongside the // user objects. The runtime serves these via `perry` / node:fs at runtime. - let embedded_assets = embed::resolve_embedded_assets(&args.embed, &project_root)?; + // `project_root` above is the entry file's directory. Embed patterns and + // config are package/project-root-relative, so use the same walked-up root + // as package.json, perry.toml, and the on-disk caches. Otherwise an entry + // at `src/main.ts` makes `--embed ./dist/**` silently search `src/dist`. + let embedded_assets = embed::resolve_embedded_assets(&args.embed, &ctx.cache_root)?; if !embedded_assets.is_empty() { if let Some(obj) = embed::generate_embedded_asset_object(&embedded_assets, &object_output_dir)? @@ -5257,7 +5260,7 @@ pub fn run_with_parse_cache( target.as_deref(), Some("visionos-simulator") | Some("visionos") ); - let is_android = matches!(target.as_deref(), Some("android") | Some("wearos")); + let is_android = is_android_target(target.as_deref()); let is_harmonyos = matches!( target.as_deref(), Some("harmonyos") | Some("harmonyos-simulator") diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 8992ae4e3f..578452c447 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -104,7 +104,7 @@ pub struct CompileArgs { pub enable_wasm_runtime: bool, /// Target platform: ios-simulator, ios, visionos-simulator, visionos, - /// android, ios-widget, ios-widget-simulator, watchos-widget, + /// android, android-x86_64, ios-widget, ios-widget-simulator, watchos-widget, /// watchos-widget-simulator, android-widget, wearos-tile, web, wasm, /// windows, linux (default: native host). See docs/src/cli/flags.md /// for the full target table. diff --git a/crates/perry/src/commands/run/entry.rs b/crates/perry/src/commands/run/entry.rs index ae703f0c17..e6daef034f 100644 --- a/crates/perry/src/commands/run/entry.rs +++ b/crates/perry/src/commands/run/entry.rs @@ -24,6 +24,10 @@ pub fn can_compile_locally(target: Option<&str>) -> bool { /// Map perry target names to Rust target triples pub fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { + if let Some(android) = crate::commands::compile::android_target::android_target(target) { + return Some(android.rust_triple); + } + match target { Some("ios-simulator") => Some("aarch64-apple-ios-sim"), Some("ios") => Some("aarch64-apple-ios"), @@ -31,9 +35,6 @@ pub fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { Some("visionos") => Some("aarch64-apple-visionos"), Some("tvos-simulator") => Some("aarch64-apple-tvos-sim"), Some("tvos") => Some("aarch64-apple-tvos"), - Some("android") => Some("aarch64-linux-android"), - // Wear OS is Android-on-a-watch: same arm64 Android toolchain/.so. - Some("wearos") => Some("aarch64-linux-android"), _ => None, } } @@ -268,3 +269,16 @@ pub fn resolve_target( None => Ok((None, None)), } } + +#[cfg(test)] +mod tests { + use super::rust_target_triple; + + #[test] + fn android_x86_64_uses_its_cross_runtime() { + assert_eq!( + rust_target_triple(Some("android-x86_64")), + Some("x86_64-linux-android") + ); + } +} diff --git a/crates/perry/tests/issue_5731_embedded_assets.rs b/crates/perry/tests/issue_5731_embedded_assets.rs index 7b2874a949..74f1ff9a06 100644 --- a/crates/perry/tests/issue_5731_embedded_assets.rs +++ b/crates/perry/tests/issue_5731_embedded_assets.rs @@ -23,11 +23,21 @@ fn perry_bin() -> PathBuf { fn embeds_assets_and_reads_them_back() { let dir = tempfile::tempdir().expect("tempdir"); let root = dir.path(); + std::fs::create_dir_all(root.join("src")).expect("mkdir src"); std::fs::create_dir_all(root.join("dist/assets")).expect("mkdir dist/assets"); std::fs::write(root.join("dist/index.html"), b"HELLO_EMBED").expect("write index.html"); std::fs::write(root.join("dist/assets/app.js"), b"console.log(1)").expect("write app.js"); + std::fs::write( + root.join("package.json"), + r#"{"name":"embed-regression","private":true}"#, + ) + .expect("write package.json"); - let entry = root.join("main.ts"); + // Keep the entry below the package root. The original regression used + // `src/embed_probe.ts` with `--embed ./drizzle/**`: resolving patterns + // relative to the entry directory silently searched `src/drizzle` and + // embedded zero files. + let entry = root.join("src/main.ts"); std::fs::write( &entry, r#" diff --git a/docs/src/cli/flags.md b/docs/src/cli/flags.md index 482e5e2d02..b6b69441f7 100644 --- a/docs/src/cli/flags.md +++ b/docs/src/cli/flags.md @@ -24,7 +24,8 @@ Use `--target` to cross-compile: | `ios` | iOS Device | ARM64 device binary | | `visionos-simulator` | visionOS Simulator | Apple Vision Pro simulator build | | `visionos` | visionOS Device | Apple Vision Pro device build | -| `android` | Android | ARM64/ARMv7 | +| `android` | Android | ARM64 device build | +| `android-x86_64` | Android | x86_64 emulator/device build | | `ios-widget` | iOS Widget | WidgetKit extension (requires `--app-bundle-id`) | | `ios-widget-simulator` | iOS Widget (Sim) | Widget for simulator | | `watchos-widget` | watchOS Complication | WidgetKit extension for Apple Watch |