From 74b0cd99fffc80fc0f17eea1f2d5d41108939dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 22:43:05 +0200 Subject: [PATCH 1/2] fix(run): discover bundled cross-target runtime libraries --- changelog.d/10209-local-runtime-discovery.md | 5 + crates/perry/src/commands/run/entry.rs | 109 ++++++++++++++++--- 2 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 changelog.d/10209-local-runtime-discovery.md diff --git a/changelog.d/10209-local-runtime-discovery.md b/changelog.d/10209-local-runtime-discovery.md new file mode 100644 index 0000000000..4ca6b6e8c4 --- /dev/null +++ b/changelog.d/10209-local-runtime-discovery.md @@ -0,0 +1,5 @@ +Fix `perry run --local` rejecting prebuilt installations whose cross-compiled +runtime libraries are bundled next to the executable. Local/remote selection +now uses the compiler's library search, including target-specific install +directories, compressed archives, Apple platform suffixes, and +`PERRY_RUNTIME_DIR` / `PERRY_LIB_DIR` overrides. diff --git a/crates/perry/src/commands/run/entry.rs b/crates/perry/src/commands/run/entry.rs index 406422a319..e60e01a472 100644 --- a/crates/perry/src/commands/run/entry.rs +++ b/crates/perry/src/commands/run/entry.rs @@ -3,23 +3,13 @@ use super::*; /// Check if we have the cross-compiled runtime libraries for a target. -/// Uses the same search logic as compile.rs find_library(). +/// Use the compiler's lookup so prebuilt installs, compressed archives, and +/// explicit library-directory overrides are available to `run` as well. pub fn can_compile_locally(target: Option<&str>) -> bool { - let triple = match rust_target_triple(target) { - Some(t) => t, - None => return true, // host build, always available - }; - // Check CWD (running from source tree) - let cwd_path = format!("target/{triple}/release/libperry_runtime.a"); - if Path::new(&cwd_path).exists() { - return true; + if rust_target_triple(target).is_none() { + return true; // host build, always available } - // Check original source tree (when cargo install'd) - let source_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../target") - .join(triple) - .join("release/libperry_runtime.a"); - source_path.exists() + crate::commands::compile::find_library("libperry_runtime.a", target).is_some() } /// Map perry target names to Rust target triples @@ -274,7 +264,94 @@ pub fn resolve_target( #[cfg(test)] mod tests { - use super::{resolve_entry_file, rust_target_triple}; + use super::{can_compile_locally, resolve_entry_file, rust_target_triple}; + + #[test] + fn local_runtime_discovery_uses_install_layouts() { + const CHILD_TARGET: &str = "PERRY_TEST_LOCAL_RUNTIME_TARGET"; + const CHILD_EXPECTED: &str = "PERRY_TEST_LOCAL_RUNTIME_EXPECTED"; + if let Ok(target) = std::env::var(CHILD_TARGET) { + let expected = std::env::var(CHILD_EXPECTED).unwrap() == "true"; + // Check the fixture is visible to the linker before checking the + // run command's decision. No Android SDK or device is needed. + assert_eq!( + crate::commands::compile::find_library("libperry_runtime.a", Some(&target)) + .is_some(), + expected + ); + assert_eq!(can_compile_locally(Some(&target)), expected); + return; + } + + // Re-execute the test binary from a temporary install, with an empty + // project directory. This exercises current_exe() and isolates the + // environment overrides from other tests in this process. + let install = tempfile::tempdir().unwrap(); + let project = tempfile::tempdir().unwrap(); + let executable = std::env::current_exe().unwrap(); + let installed_exe = install.path().join(executable.file_name().unwrap()); + std::fs::copy(&executable, &installed_exe).unwrap(); + let run = |target: &str, expected: bool, override_dir: Option<(&str, &std::path::Path)>| { + let mut command = std::process::Command::new(&installed_exe); + command + .args([ + "--exact", + "commands::run::entry::tests::local_runtime_discovery_uses_install_layouts", + "--nocapture", + ]) + .current_dir(project.path()) + .env_remove("PERRY_RUNTIME_DIR") + .env_remove("PERRY_LIB_DIR") + .env("PERRY_LIB_CACHE_DIR", install.path().join("cache")) + .env(CHILD_TARGET, target) + .env(CHILD_EXPECTED, expected.to_string()); + if let Some((name, dir)) = override_dir { + command.env(name, dir); + } + let output = command.output().unwrap(); + assert!( + String::from_utf8_lossy(&output.stdout).contains("running 1 test"), + "the installed test binary must execute the discovery probe" + ); + assert!( + output.status.success(), + "target={target}, expected={expected}:\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + }; + + let bundled = install.path().join("aarch64-linux-android/release"); + std::fs::create_dir_all(&bundled).unwrap(); + let runtime = bundled.join("libperry_runtime.a"); + std::fs::write(&runtime, b"!\n").unwrap(); + run("android", true, None); + run("wearos", true, None); + // A different architecture must not pick up the ARM64 archive. + run("android-x86_64", false, None); + + std::fs::remove_file(&runtime).unwrap(); + run("android", false, None); + // npm's compressed archive layout goes through the same discovery. + let compressed = zstd::stream::encode_all(&b"!\n"[..], 1).unwrap(); + std::fs::write(runtime.with_extension("a.zst"), compressed).unwrap(); + run("android", true, None); + + let overrides = tempfile::tempdir().unwrap(); + std::fs::write(overrides.path().join("libperry_runtime.a"), b"!\n").unwrap(); + for name in ["PERRY_RUNTIME_DIR", "PERRY_LIB_DIR"] { + run("android-x86_64", true, Some((name, overrides.path()))); + } + + // Flat Apple bundles use target-suffixed archive names. + std::fs::write( + install.path().join("libperry_runtime_ios_sim.a"), + b"!\n", + ) + .unwrap(); + run("ios-simulator", true, None); + assert!(can_compile_locally(None)); + } #[test] fn directory_input_resolves_default_entry() { From d674d871893ce20168bfe225961e821c7084f8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 22:43:28 +0200 Subject: [PATCH 2/2] docs: key runtime discovery changelog to PR 10224 --- ...ocal-runtime-discovery.md => 10224-local-runtime-discovery.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{10209-local-runtime-discovery.md => 10224-local-runtime-discovery.md} (100%) diff --git a/changelog.d/10209-local-runtime-discovery.md b/changelog.d/10224-local-runtime-discovery.md similarity index 100% rename from changelog.d/10209-local-runtime-discovery.md rename to changelog.d/10224-local-runtime-discovery.md