diff --git a/.github/actions/setup-llvm22/action.yml b/.github/actions/setup-llvm22/action.yml index f4ee9631c7..91b541a232 100644 --- a/.github/actions/setup-llvm22/action.yml +++ b/.github/actions/setup-llvm22/action.yml @@ -90,7 +90,8 @@ runs: run: | $ErrorActionPreference = "Stop" $ver = $env:LLVM_VERSION - $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$ver/clang+llvm-$ver-x86_64-pc-windows-msvc.tar.xz" + $llvmArch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -eq "Arm64") { "aarch64" } else { "x86_64" } + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$ver/clang+llvm-$ver-$llvmArch-pc-windows-msvc.tar.xz" Write-Host "downloading $url" curl.exe -sSL --retry 3 -o "$env:RUNNER_TEMP\llvm.tar.xz" $url New-Item -ItemType Directory -Force -Path C:\llvm | Out-Null diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 6bb9b5dcf2..57823aeea2 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -296,6 +296,9 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc artifact: perry-windows-x86_64 + - os: windows-11-arm + target: aarch64-pc-windows-msvc + artifact: perry-windows-aarch64 runs-on: ${{ matrix.os }} env: @@ -609,22 +612,14 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # #4823 (option 1): the Android UI backend so a WinGet/Scoop-installed # perry can link `perry/ui` apps for android without a source tree. - # Best-effort: aws-lc-rs cross-from-Windows can be finicky; a failure - # here must not sink the whole release, because the npm path backfills - # libperry_ui_android.a from the ubuntu `perry-cross-*` bundle and the - # zip still ships runtime+stdlib. The UI staging below is guarded on - # the lib actually existing. + # This is mandatory for every Windows archive: matrix legs package + # independently, so another leg cannot backfill a missing library. # Mirrors the ubuntu cross-bundle leg: `--profile dist` build for the # staticlib (UI crates are already staticlib — no -static wrapper); the # global-dynamic TLS rustflag only matters at the consumer's final # cdylib link, not here. - # A native non-zero exit isn't a terminating error in pwsh, so guard - # on $LASTEXITCODE explicitly and reset it so the step stays green. cargo build --profile dist --target aarch64-linux-android -p perry-ui-android - if ($LASTEXITCODE -ne 0) { - Write-Warning "perry-ui-android android cross-build failed (exit $LASTEXITCODE) — zip will omit the UI lib; npm backfills it from perry-cross-aarch64-linux-android." - $global:LASTEXITCODE = 0 - } + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # #4856 — defense in depth behind the cache eviction above: assert # every runtime archive this leg built (host triple + any Apple/ @@ -770,7 +765,7 @@ jobs: $signtool = $signtoolCommand.Source } else { $kitsBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin' - $signtool = Get-ChildItem "$kitsBin\*\x64\signtool.exe" | + $signtool = Get-ChildItem "$kitsBin\*\arm64\signtool.exe", "$kitsBin\*\x64\signtool.exe" | Sort-Object { [version]$_.Directory.Parent.Name } -Descending | Select-Object -First 1 -ExpandProperty FullName } @@ -817,14 +812,14 @@ jobs: # subdir lives alongside `perry.exe`. $androidLibDir = "staging/aarch64-linux-android/release" New-Item -ItemType Directory -Force -Path $androidLibDir | Out-Null - # #4823: libperry_ui_android.a is best-effort (the cross-build above - # may have skipped it) — Test-Path guards it just like the others. $androidLibs = @("libperry_runtime.a", "libperry_stdlib.a", "libperry_ui_android.a") foreach ($lib in $androidLibs) { $src = "target/aarch64-linux-android/dist/$lib" - if (Test-Path $src) { - Copy-Item $src $androidLibDir + if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { + Write-Error "Required Android library missing from ${{ matrix.artifact }}: $src" + exit 1 } + Copy-Item $src $androidLibDir } Compress-Archive -Path staging/* -DestinationPath "${{ matrix.artifact }}.zip" @@ -1066,15 +1061,18 @@ jobs: ui_lib: libperry_ui_android.a tier3: false # --- Windows MSVC (native runner) --------------------------------- - # Only x86_64 today. perry's compile.rs maps `--target windows` - # to `x86_64-pc-windows-msvc` (compile.rs:1009); ARM64 Windows - # support is a separate follow-up — re-add `aarch64-pc-windows-msvc` - # here once the CLI knows about it. + # Both native Windows architectures use their matching hosted runner + # so Rust, LLVM, MSVC, and the packaged executable all agree. - os: windows-latest target: x86_64-pc-windows-msvc ui_crate: perry-ui-windows ui_lib: perry_ui_windows.lib tier3: false + - os: windows-11-arm + target: aarch64-pc-windows-msvc + ui_crate: perry-ui-windows + ui_lib: perry_ui_windows.lib + tier3: false runs-on: ${{ matrix.os }} env: @@ -1999,9 +1997,9 @@ jobs: echo "Hub response: ${RESPONSE}" # --------------------------------------------------------------------------- - # Publish npm packages (@perryts/perry + 7 per-platform packages) + # Publish npm packages (@perryts/perry + 8 per-platform packages) # - # Uses OIDC / Trusted Publishers (no long-lived NPM_TOKEN). Each of the 8 + # Uses OIDC / Trusted Publishers (no long-lived NPM_TOKEN). Each of the 9 # package names must be registered on npmjs.com with this repo + workflow # as a Trusted Publisher. See npm/README.md for the one-time setup. # --------------------------------------------------------------------------- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9294acdce1..2985de4148 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1263,6 +1263,51 @@ jobs: exit 1 } + # Native Windows ARM64 coverage for #4482. The x64 Windows gate above cannot + # execute an ARM binary, so keep a focused native job that proves the + # compiler, runtime ABI, final linker, and advertised target all agree. + windows-arm64-build: + runs-on: windows-11-arm + timeout-minutes: 75 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - uses: ./.github/actions/setup-llvm22 + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "${{ runner.os }}-${{ runner.arch }}-perry" + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Build ARM64 compiler and static libraries (perry-dev) + run: cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static + + - name: Run ARM64 setjmp ABI tests + run: 'cargo test --profile perry-dev --lib -p perry-runtime ffi::setjmp::tests:: -- --test-threads=1' + + - name: Compile and run a Perry ARM64 executable + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + # Keep this a runtime-only smoke. The full runtime+stdlib COFF archive + # composition has its own gate; this job's contract is the ARM ABI, + # target selection, SDK discovery, final PE link, and execution. + New-Item -ItemType Directory -Force 'target/windows-arm64-smoke-libs' | Out-Null + Copy-Item 'target/perry-dev/perry_runtime.lib' 'target/windows-arm64-smoke-libs/' + $env:PERRY_RUNTIME_DIR = (Resolve-Path 'target/windows-arm64-smoke-libs').Path + target/perry-dev/perry.exe compile tests/release/link_smoke/fixture.ts ` + --target windows-aarch64 ` + --no-auto-optimize ` + -o target/windows-arm64-smoke.exe + $output = target/windows-arm64-smoke.exe + if ($output.Trim() -ne 'ok') { + throw "unexpected ARM64 smoke output: '$output'" + } + # --------------------------------------------------------------------------- # GC write-barrier stress (optional / non-blocking) # diff --git a/changelog.d/8027-windows-arm64.md b/changelog.d/8027-windows-arm64.md new file mode 100644 index 0000000000..9251bdcd68 --- /dev/null +++ b/changelog.d/8027-windows-arm64.md @@ -0,0 +1,11 @@ +### Add native Windows ARM64 builds and packages + +Perry now supports `--target windows-aarch64` (with `windows-arm64` as an +alias), selects matching MSVC and Windows SDK tools and libraries, and ships a +native `@perryts/perry-win32-arm64` package and GitHub release archive. The +plain `windows` target follows the host architecture on Windows while keeping +its existing x64 behavior when invoked from another operating system. + +The Windows runtime also uses the ARM64 VCRuntime `setjmp` implementation, and +a native Windows ARM runner now builds Perry and executes a generated ARM64 +smoke binary in pull-request CI. diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index 88118bf5cd..7a093a9e9b 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -825,6 +825,8 @@ pub(crate) fn default_target_triple() -> String { "x86_64-unknown-linux-gnu".to_string() } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { "aarch64-unknown-linux-gnu".to_string() + } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) { + "aarch64-pc-windows-msvc".to_string() } else if cfg!(target_os = "windows") { "x86_64-pc-windows-msvc".to_string() } else { @@ -890,7 +892,11 @@ pub fn resolve_target_triple(name: &str) -> Option { "linux-aarch64-musl" => Some("aarch64-unknown-linux-musl".to_string()), "macos" => Some("arm64-apple-macosx15.0.0".to_string()), "macos-x86_64" => Some("x86_64-apple-macosx15.0.0".to_string()), - "windows" | "windows-winui" => Some("x86_64-pc-windows-msvc".to_string()), + "windows" | "windows-winui" if cfg!(target_os = "windows") => Some(default_target_triple()), + "windows" | "windows-winui" | "windows-x86_64" => { + Some("x86_64-pc-windows-msvc".to_string()) + } + "windows-aarch64" | "windows-arm64" => Some("aarch64-pc-windows-msvc".to_string()), _ => None, } } @@ -1702,6 +1708,20 @@ mod resolve_target_triple_tests { // Unknown targets still fall through to None. assert_eq!(resolve_target_triple("android-mips"), None); } + + #[test] + fn explicit_windows_targets_resolve_to_coff_triples() { + assert_eq!( + resolve_target_triple("windows-x86_64").as_deref(), + Some("x86_64-pc-windows-msvc") + ); + for target in ["windows-aarch64", "windows-arm64"] { + assert_eq!( + resolve_target_triple(target).as_deref(), + Some("aarch64-pc-windows-msvc") + ); + } + } } #[cfg(test)] diff --git a/crates/perry-runtime/src/ffi/setjmp.rs b/crates/perry-runtime/src/ffi/setjmp.rs index f49542ec22..34ffe3b124 100644 --- a/crates/perry-runtime/src/ffi/setjmp.rs +++ b/crates/perry-runtime/src/ffi/setjmp.rs @@ -5,7 +5,7 @@ //! with conflicting parameter types — `*mut u64` vs `*mut i32`. The Rust //! compiler emitted a `clashing_extern_declarations` warning. Both //! declarations linked to the same C symbol (`_setjmp` on Apple, -//! `setjmp` elsewhere), so any one of them necessarily disagreed with +//! `setjmp` on most other targets), so any one of them necessarily disagreed with //! the real libc signature; we got away with it on macOS/aarch64 only //! because the ABI happens to pass the pointer in `x0` regardless of //! pointee type and the C side only reads/writes a fixed number of bytes. @@ -51,7 +51,29 @@ extern "C" { pub fn setjmp(env: *mut c_int) -> c_int; } -#[cfg(not(target_vendor = "apple"))] +#[cfg(all(target_os = "windows", target_arch = "aarch64"))] +#[link(name = "libvcruntime")] +extern "C" { + /// MSVC implements ARM64 `setjmp` as a compiler intrinsic. Clang lowers + /// the public `_setjmp` spelling to `__intrinsic_setjmpex`, passing the + /// current frame as a hidden second argument. Rust does not perform that + /// lowering, so calling the ordinary CRT `setjmp` thunk leaves an + /// unresolved `__intrinsic_setjmp` reference when lld links the final PE. + /// + /// Perry deliberately uses the non-unwinding form of `longjmp` and clears + /// `_JUMP_BUFFER::Frame` before jumping (see the Windows caveat below). + /// Consequently the hidden frame argument is not observed: as on x64, its + /// unspecified register value is stored and then cleared before `longjmp`. + /// Linking the static VCRuntime implementation makes the intrinsic + /// available to Perry's manually linked executables. + #[link_name = "__intrinsic_setjmpex"] + pub fn setjmp(env: *mut c_int) -> c_int; +} + +#[cfg(all( + not(target_vendor = "apple"), + not(all(target_os = "windows", target_arch = "aarch64")) +))] extern "C" { /// `setjmp(3)`. On glibc Linux this already doesn't save the /// signal mask, so it's the same fast path we want. @@ -88,6 +110,7 @@ extern "C" { /// - Windows x64 MSVC: 16 doubles = 128 bytes for `_JBLEN`, padded /// to 256 bytes of `_JUMP_BUFFER` — and the buffer must be /// **16-byte aligned** (aligned XMM stores; see the extern's docs). +/// - Windows arm64 MSVC: `_JBLEN = 24` `u64`s = 192 bytes. /// /// We surface 192 here so callers can `const_assert!` against it. pub const JMP_BUF_MIN_BYTES: usize = 192; diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index b8d70ff996..4be74233ae 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -44,6 +44,7 @@ mod post_link; mod precompile_capture; mod reachability; mod update_config; +mod windows_target; // pub(crate): commands/deps.rs (the `check --check-deps` dependency checker) // reuses the subpath-imports + tsconfig-paths resolvers for `#` specifiers. pub(crate) mod resolve; @@ -117,6 +118,9 @@ use targets::{ apple_sdk_version, find_visionos_swift_runtime, find_watchos_swift_runtime, generate_embedded_js_object, generate_js_bundle, }; +use windows_target::{ + is_native_windows_target, is_windows_target, windows_target_arch, WindowsTargetArch, +}; // Codegen-backend entrypoints (#5422) — only present when their backend feature // is compiled in. The matching `--target` routing below errors cleanly when a // backend was built out. diff --git a/crates/perry/src/commands/compile/app_metadata.rs b/crates/perry/src/commands/compile/app_metadata.rs index e31f5da4c9..e66eda9b0c 100644 --- a/crates/perry/src/commands/compile/app_metadata.rs +++ b/crates/perry/src/commands/compile/app_metadata.rs @@ -9,20 +9,21 @@ use std::fs; use std::path::Path; -use super::{android_target, is_android_target}; +use super::{android_target, is_android_target, is_windows_target, windows_target_arch}; pub(super) fn target_bundle_section(target: Option<&str>) -> Option<&'static str> { if is_android_target(target) { return Some("android"); } + if is_windows_target(target) { + return Some("windows"); + } 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("macos") => Some("macos"), - // WinUI shares the [windows] perry.toml section (#4680). - Some("windows") | Some("windows-winui") => Some("windows"), // musl variants share the [linux] perry.toml section (#4826). Some("linux") | Some("linux-musl") @@ -159,6 +160,9 @@ pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { if let Some(android) = android_target(target) { return Some(android.rust_triple); } + if let Some(windows) = windows_target_arch(target) { + return Some(windows.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"), @@ -183,7 +187,6 @@ pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> { // .a files for these triples are built by release-packages.yml. Some("linux-musl") | Some("linux-x86_64-musl") => Some("x86_64-unknown-linux-musl"), Some("linux-aarch64-musl") => Some("aarch64-unknown-linux-musl"), - Some("windows") | Some("windows-winui") => Some("x86_64-pc-windows-msvc"), Some("macos") => Some("aarch64-apple-darwin"), _ => None, } @@ -209,6 +212,21 @@ mod app_metadata_tests { ); } + #[test] + fn windows_aarch64_uses_windows_metadata_and_rust_target() { + for target in ["windows-aarch64", "windows-arm64"] { + assert_eq!(target_bundle_section(Some(target)), Some("windows")); + assert_eq!( + rust_target_triple(Some(target)), + Some("aarch64-pc-windows-msvc") + ); + } + assert_eq!( + rust_target_triple(Some("windows-x86_64")), + Some("x86_64-pc-windows-msvc") + ); + } + #[test] fn reads_project_metadata_and_target_bundle_id() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs index 8a2200e48d..c8cd6bf624 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs @@ -942,8 +942,10 @@ const _cjs = (function() {{ } fn target_node_platform(target: Option<&str>) -> Option<&'static str> { + if super::super::is_windows_target(target) { + return Some("win32"); + } match target { - Some("windows") | Some("windows-winui") => Some("win32"), Some("linux") | Some("linux-x86_64") | Some("linux-arm64") | Some("linux-aarch64") // musl shares node's `process.platform === "linux"` (#4826). | Some("linux-musl") | Some("linux-x86_64-musl") | Some("linux-aarch64-musl") => { diff --git a/crates/perry/src/commands/compile/library_search.rs b/crates/perry/src/commands/compile/library_search.rs index 1572cfa35a..1bfc705d73 100644 --- a/crates/perry/src/commands/compile/library_search.rs +++ b/crates/perry/src/commands/compile/library_search.rs @@ -29,7 +29,12 @@ 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::{android_target, find_perry_workspace_root, is_android_target, rust_target_triple}; +#[cfg(target_os = "windows")] +use super::is_native_windows_target; +use super::{ + android_target, find_perry_workspace_root, is_android_target, is_windows_target, + rust_target_triple, windows_target_arch, WindowsTargetArch, +}; /// Resolve the host's Rust target triple by parsing `rustc -vV`. /// @@ -111,8 +116,7 @@ pub(crate) fn locate_native_lib_artifact( /// cargo lib name (refs issue #792). fn lib_name_variants(lib_name: &str, target: Option<&str>) -> Vec { let mut out = vec![lib_name.to_string()]; - let is_windows = matches!(target, Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); + let is_windows = is_windows_target(target); let is_macos = matches!( target, Some("ios") @@ -311,13 +315,18 @@ fn versioned_llvm_bin_dirs(parent: &str, prefix: &str, names: &[String]) -> Vec< /// On Windows, the PATH may contain a GNU `link` utility (e.g. from Git Bash/MSYS2) /// which is not the MSVC linker. This function searches for the real MSVC link.exe. #[cfg(target_os = "windows")] -pub(super) fn msvc_vswhere_installation_path_args() -> [&'static str; 8] { +pub(super) fn msvc_vswhere_installation_path_args( + target_arch: WindowsTargetArch, +) -> [&'static str; 8] { [ "-products", "*", // Without the VC tools filter, `-latest` can select Management Studio. "-requires", - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + match target_arch { + WindowsTargetArch::X86_64 => "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + WindowsTargetArch::Aarch64 => "Microsoft.VisualStudio.Component.VC.Tools.ARM64", + }, "-latest", "-property", "installationPath", @@ -326,7 +335,8 @@ pub(super) fn msvc_vswhere_installation_path_args() -> [&'static str; 8] { } #[cfg(target_os = "windows")] -pub(super) fn find_msvc_link_exe() -> Option { +pub(super) fn find_msvc_link_exe(target: Option<&str>) -> Option { + let target_arch = windows_target_arch(target)?; // Try vswhere.exe first (most reliable) let vswhere_paths = [ PathBuf::from(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"), @@ -335,20 +345,33 @@ pub(super) fn find_msvc_link_exe() -> Option { for vswhere in &vswhere_paths { if vswhere.exists() { if let Ok(output) = Command::new(vswhere) - .args(msvc_vswhere_installation_path_args()) + .args(msvc_vswhere_installation_path_args(target_arch)) .output() { let install_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !install_path.is_empty() { - // Search for link.exe under VC/Tools/MSVC/*/bin/Hostx64/x64/ + // Prefer a linker native to the host, but allow the other + // host binary because Windows can run x64 tools on ARM64. let msvc_dir = PathBuf::from(&install_path).join(r"VC\Tools\MSVC"); if let Ok(entries) = std::fs::read_dir(&msvc_dir) { let mut versions: Vec<_> = entries.filter_map(|e| e.ok()).collect(); versions.sort_by(|a, b| b.file_name().cmp(&a.file_name())); + let host_dirs = if cfg!(target_arch = "aarch64") { + ["Hostarm64", "Hostx64"] + } else { + ["Hostx64", "Hostarm64"] + }; for entry in versions { - let link = entry.path().join(r"bin\Hostx64\x64\link.exe"); - if link.exists() { - return Some(link); + for host_dir in host_dirs { + let link = entry + .path() + .join("bin") + .join(host_dir) + .join(target_arch.msvc_dir()) + .join("link.exe"); + if link.exists() { + return Some(link); + } } } } @@ -360,7 +383,7 @@ pub(super) fn find_msvc_link_exe() -> Option { } #[cfg(not(target_os = "windows"))] -pub(super) fn find_msvc_link_exe() -> Option { +pub(super) fn find_msvc_link_exe(_target: Option<&str>) -> Option { find_llvm_tool("lld-link") } @@ -407,7 +430,7 @@ pub(super) fn find_lld_link() -> Option { /// Windows-target archive is built with llvm-lib / llvm-ar instead. #[cfg(target_os = "windows")] pub(super) fn find_msvc_lib_exe() -> Option { - if let Some(link_exe) = find_msvc_link_exe() { + if let Some(link_exe) = find_msvc_link_exe(None) { let lib = link_exe.with_file_name("lib.exe"); if lib.exists() { return Some(lib); @@ -496,8 +519,8 @@ pub(super) fn windows_archiver_command(archiver: &WindowsArchiver, out: &Path) - } /// Location where `perry setup windows` writes the xwin'd Microsoft CRT + -/// Windows SDK. Returns `Some(root)` only when `/crt/lib/x86_64` exists, -/// so callers can treat `Some` as "toolchain is complete and ready to link." +/// Windows SDK. Returns `Some(root)` only when a supported architecture's CRT +/// directory exists, so callers can treat `Some` as "toolchain is complete." /// /// Default location is `%LOCALAPPDATA%\perry\windows-sdk` on Windows; can be /// overridden via `PERRY_WINDOWS_SYSROOT` (same env var already used by the @@ -514,8 +537,9 @@ pub(super) fn find_perry_windows_sdk() -> Option { // Sanity-check: xwin splat populates crt/lib/x86_64 (or crt/lib/x64 with // --preserve-ms-arch-notation). If neither exists, the directory isn't a // completed xwin output — skip it. - if candidate.join("crt").join("lib").join("x86_64").exists() - || candidate.join("crt").join("lib").join("x64").exists() + if ["x86_64", "x64", "aarch64", "arm64"] + .iter() + .any(|arch| candidate.join("crt").join("lib").join(arch).exists()) { return Some(candidate); } @@ -587,35 +611,36 @@ pub(super) fn windows_pe_subsystem_flag(needs_ui: bool, min_windows_version: &st /// Given a sysroot directory populated by `xwin splat` (or a compatible layout), /// return the lib search paths for MSVC / lld-link's LIB env var. Callers pass /// the directory root (e.g. `%LOCALAPPDATA%\perry\windows-sdk`) and get back a -/// `Vec` of absolute lib dirs: `/crt/lib/x86_64`, -/// `/sdk/lib/um/x86_64`, `/sdk/lib/ucrt/x86_64`. Falls through to +/// `Vec` of absolute lib dirs for the selected target architecture. +/// Falls through to /// `/lib` and finally `` itself if the structured layout isn't /// present (e.g. a user pointed PERRY_WINDOWS_SYSROOT at a custom dir). -pub(super) fn xwin_sysroot_lib_paths(root: &Path) -> Vec { +pub(super) fn xwin_sysroot_lib_paths(root: &Path, target_arch: WindowsTargetArch) -> Vec { let mut paths = Vec::new(); // xwin default layout — also covers --preserve-ms-arch-notation (x64 suffix). - for (crt_sub, um_sub, ucrt_sub) in &[ - ("crt/lib/x86_64", "sdk/lib/um/x86_64", "sdk/lib/ucrt/x86_64"), - ("crt/lib/x64", "sdk/lib/um/x64", "sdk/lib/ucrt/x64"), - ] { - let crt = root.join(crt_sub); - let um = root.join(um_sub); - let ucrt = root.join(ucrt_sub); - if crt.exists() || um.exists() || ucrt.exists() { - if crt.exists() { - paths.push(crt.to_string_lossy().to_string()); - } - if um.exists() { - paths.push(um.to_string_lossy().to_string()); - } - if ucrt.exists() { - paths.push(ucrt.to_string_lossy().to_string()); - } + for arch in [target_arch.xwin_dir(), target_arch.msvc_dir()] { + let crt = root.join("crt").join("lib").join(arch); + let um = root.join("sdk").join("lib").join("um").join(arch); + let ucrt = root.join("sdk").join("lib").join("ucrt").join(arch); + if crt.is_dir() && um.is_dir() && ucrt.is_dir() { + paths.push(crt.to_string_lossy().to_string()); + paths.push(um.to_string_lossy().to_string()); + paths.push(ucrt.to_string_lossy().to_string()); return paths; } } + // A structured xwin sysroot that contains only a different architecture + // is not a valid flat fallback. Returning the root here would make the + // linker search it directly and hide the missing target CRT. + if root.join("crt").join("lib").is_dir() + || root.join("sdk").join("lib").join("um").is_dir() + || root.join("sdk").join("lib").join("ucrt").is_dir() + { + return paths; + } + let flat_lib = root.join("lib"); if flat_lib.exists() { paths.push(flat_lib.to_string_lossy().to_string()); @@ -633,12 +658,13 @@ pub(super) fn xwin_sysroot_lib_paths(root: &Path) -> Vec { /// (matches the "lightweight toolchain" opt-in mental model), then falls back /// to vswhere-located Visual Studio install paths. #[cfg(target_os = "windows")] -pub(super) fn find_msvc_lib_paths() -> Option { +pub(super) fn find_msvc_lib_paths(target: Option<&str>) -> Option { + let target_arch = windows_target_arch(target)?; // If the user ran `perry setup windows`, use that sysroot — they've // expressed intent to use the lightweight LLVM + xwin path even if MSVC // is also installed. Same precedence as find_msvc_link_exe_or_lld_link(). if let Some(sysroot) = find_perry_windows_sdk() { - let paths = xwin_sysroot_lib_paths(&sysroot); + let paths = xwin_sysroot_lib_paths(&sysroot, target_arch); if !paths.is_empty() { return Some(paths.join(";")); } @@ -654,7 +680,7 @@ pub(super) fn find_msvc_lib_paths() -> Option { for vswhere in &vswhere_paths { if vswhere.exists() { if let Ok(output) = Command::new(vswhere) - .args(msvc_vswhere_installation_path_args()) + .args(msvc_vswhere_installation_path_args(target_arch)) .output() { let install_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); @@ -664,7 +690,7 @@ pub(super) fn find_msvc_lib_paths() -> Option { let mut versions: Vec<_> = entries.filter_map(|e| e.ok()).collect(); versions.sort_by(|a, b| b.file_name().cmp(&a.file_name())); if let Some(entry) = versions.first() { - let lib_path = entry.path().join(r"lib\x64"); + let lib_path = entry.path().join("lib").join(target_arch.msvc_dir()); if lib_path.exists() { paths.push(lib_path.to_string_lossy().to_string()); } @@ -711,8 +737,8 @@ pub(super) fn find_msvc_lib_paths() -> Option { let mut versions: Vec<_> = entries.filter_map(|e| e.ok()).collect(); versions.sort_by(|a, b| b.file_name().cmp(&a.file_name())); if let Some(entry) = versions.first() { - let um_path = entry.path().join(r"um\x64"); - let ucrt_path = entry.path().join(r"ucrt\x64"); + let um_path = entry.path().join("um").join(target_arch.msvc_dir()); + let ucrt_path = entry.path().join("ucrt").join(target_arch.msvc_dir()); if um_path.exists() { paths.push(um_path.to_string_lossy().to_string()); sdk_added = true; @@ -733,13 +759,14 @@ pub(super) fn find_msvc_lib_paths() -> Option { // about user32.lib. Tell the user exactly what we tried and what // the workarounds are. eprintln!( - "Warning: Windows SDK lib path (Windows Kits\\10\\Lib\\\\um\\x64) not found.\n\ + "Warning: Windows SDK lib path (Windows Kits\\10\\Lib\\\\um\\{}) not found.\n\ Tried: {}\n\ Linker will likely fail with LNK1181 (e.g. cannot open user32.lib).\n\ Workarounds:\n\ - Run `vcvars64.bat` before `perry compile` (sets `LIB` env)\n\ - Install Windows 10/11 SDK via Visual Studio Installer\n\ - - Set the `LIB` env var manually to your SDK's `um\\x64;ucrt\\x64` paths", + - Set the `LIB` env var manually to your SDK's matching `um` and `ucrt` paths", + target_arch.msvc_dir(), sdk_roots .iter() .map(|p| p.display().to_string()) @@ -830,7 +857,7 @@ pub(super) fn find_windows_sdk_mt_dir() -> Option { } /// Given a Windows SDK `bin` root, return the arch dir holding `mt.exe`: the -/// newest versioned subdir's `x64` (then `x86`, which runs fine on x64 hosts), +/// newest versioned subdir's native architecture (then compatible fallbacks), /// falling back to the unversioned `bin\` layout of pre-10.0.15063 SDKs. /// The newest-version pick mirrors the descending file-name sort /// `find_msvc_lib_paths` uses for `Lib\`. Host-independent so the @@ -843,14 +870,24 @@ pub(super) fn newest_mt_dir_under(bin_root: &Path) -> Option { }; version_dirs.sort_by(|a, b| b.file_name().cmp(&a.file_name())); for dir in version_dirs { - for arch in ["x64", "x86"] { + let arches = if cfg!(target_arch = "aarch64") { + ["arm64", "x64", "x86"] + } else { + ["x64", "arm64", "x86"] + }; + for arch in arches { let cand = dir.join(arch); if cand.join("mt.exe").is_file() { return Some(cand); } } } - for arch in ["x64", "x86"] { + let arches = if cfg!(target_arch = "aarch64") { + ["arm64", "x64", "x86"] + } else { + ["x64", "arm64", "x86"] + }; + for arch in arches { let cand = bin_root.join(arch); if cand.join("mt.exe").is_file() { return Some(cand); @@ -860,7 +897,8 @@ pub(super) fn newest_mt_dir_under(bin_root: &Path) -> Option { } #[cfg(not(target_os = "windows"))] -pub(super) fn find_msvc_lib_paths() -> Option { +pub(super) fn find_msvc_lib_paths(target: Option<&str>) -> Option { + let target_arch = windows_target_arch(target)?; let sysroot = std::env::var("PERRY_WINDOWS_SYSROOT").ok()?; let root = PathBuf::from(&sysroot); if !root.exists() { @@ -871,7 +909,7 @@ pub(super) fn find_msvc_lib_paths() -> Option { return None; } - Some(xwin_sysroot_lib_paths(&root).join(";")) + Some(xwin_sysroot_lib_paths(&root, target_arch).join(";")) } /// Find a library by name, optionally searching cross-compilation target directories. @@ -1008,7 +1046,7 @@ pub(super) fn collect_library_candidates(name: &str, target: Option<&str>) -> Ve // also check the default target/release/ directory since native builds // put libraries there without the triple subdirectory. #[cfg(target_os = "windows")] - if matches!(target, Some("windows") | Some("windows-winui")) { + if is_native_windows_target(target) { candidates.push(PathBuf::from(format!("target/release/{}", name))); candidates.push(PathBuf::from(format!("target/debug/{}", name))); candidates.extend(winget_lib_candidates(name)); @@ -1135,11 +1173,10 @@ pub(super) fn collect_library_candidates(name: &str, target: Option<&str>) -> Ve /// Find the runtime library for linking pub(super) fn find_runtime_library(target: Option<&str>) -> Result { - let lib_name = match target { - Some("windows") | Some("windows-winui") => "perry_runtime.lib", - #[cfg(target_os = "windows")] - None => "perry_runtime.lib", - _ => "libperry_runtime.a", + let lib_name = if is_windows_target(target) { + "perry_runtime.lib" + } else { + "libperry_runtime.a" }; find_library_with_candidates(lib_name, target).map_err(|searched| { let extra = if target.is_some() { @@ -1176,11 +1213,7 @@ pub(super) fn find_runtime_library(target: Option<&str>) -> Result { /// reachable and stdlib is not linked). Unix-only: Windows always links /// stdlib, which is built panic=unwind. pub(super) fn find_runtime_abort_library(target: Option<&str>) -> Option { - if matches!(target, Some("windows") | Some("windows-winui")) { - return None; - } - #[cfg(target_os = "windows")] - if target.is_none() { + if is_windows_target(target) { return None; } find_library("libperry_runtime_abort.a", target) @@ -1188,11 +1221,10 @@ pub(super) fn find_runtime_abort_library(target: Option<&str>) -> Option) -> Option { - let lib_name = match target { - Some("windows") | Some("windows-winui") => "perry_stdlib.lib", - #[cfg(target_os = "windows")] - None => "perry_stdlib.lib", - _ => "libperry_stdlib.a", + let lib_name = if is_windows_target(target) { + "perry_stdlib.lib" + } else { + "libperry_stdlib.a" }; find_library(lib_name, target) } @@ -1200,11 +1232,10 @@ pub(super) fn find_stdlib_library(target: Option<&str>) -> Option { /// Find the wasmi-based WebAssembly host library (optional — only needed /// when `--enable-wasm-runtime` is set, see issue #76). pub(super) fn find_wasm_host_library(target: Option<&str>) -> Option { - let lib_name = match target { - Some("windows") | Some("windows-winui") => "perry_wasm_host.lib", - #[cfg(target_os = "windows")] - None => "perry_wasm_host.lib", - _ => "libperry_wasm_host.a", + let lib_name = if is_windows_target(target) { + "perry_wasm_host.lib" + } else { + "libperry_wasm_host.a" }; find_library(lib_name, target) } @@ -1282,11 +1313,10 @@ pub(super) fn build_wasm_host_library( // CARGO_TARGET_DIR + cross-target triple subdir) so the returned archive // is unambiguously the one we built. Fall back to the standard search so a // non-standard layout still resolves. - let lib_name = match target { - Some("windows") | Some("windows-winui") => "perry_wasm_host.lib", - #[cfg(target_os = "windows")] - None => "perry_wasm_host.lib", - _ => "libperry_wasm_host.a", + let lib_name = if is_windows_target(target) { + "perry_wasm_host.lib" + } else { + "libperry_wasm_host.a" }; let mut release_dir = match std::env::var_os("CARGO_TARGET_DIR") { Some(raw) if !raw.is_empty() => { @@ -1338,9 +1368,7 @@ pub(super) fn find_ui_library(target: Option<&str>) -> Option { // perry-ui-windows Win32 symbols today (scaffold), so the FFI surface // is identical to the `windows` lib. Some("windows-winui") => "perry_ui_windows_winui.lib", - Some("windows") => "perry_ui_windows.lib", - #[cfg(target_os = "windows")] - None => "perry_ui_windows.lib", + target if is_windows_target(target) => "perry_ui_windows.lib", _ => { if cfg!(target_os = "linux") { "libperry_ui_gtk4.a" @@ -1637,9 +1665,7 @@ pub(super) fn find_geisterhand_lib(name: &str, target: Option<&str>) -> Option

) -> Option { - let name = if matches!(target, Some("windows") | Some("windows-winui")) - || cfg!(target_os = "windows") - { + let name = if is_windows_target(target) { "perry_ui_geisterhand.lib" } else { "libperry_ui_geisterhand.a" @@ -1648,9 +1674,7 @@ pub(super) fn find_geisterhand_library(target: Option<&str>) -> Option } pub(super) fn find_geisterhand_runtime(target: Option<&str>) -> Option { - let name = if matches!(target, Some("windows") | Some("windows-winui")) - || cfg!(target_os = "windows") - { + let name = if is_windows_target(target) { "perry_runtime.lib" } else { "libperry_runtime.a" @@ -1668,9 +1692,7 @@ pub(super) fn find_geisterhand_stdlib(target: Option<&str>) -> Option { // so we never select the auto-optimized stdlib, whose feature set is // computed from the app's TS imports and omits async-runtime when the async // surface comes from a native binding — the #1383 link failure. - let name = if matches!(target, Some("windows") | Some("windows-winui")) - || cfg!(target_os = "windows") - { + let name = if is_windows_target(target) { "perry_stdlib.lib" } else { "libperry_stdlib.a" @@ -1689,7 +1711,7 @@ pub(super) fn find_geisterhand_ui(target: Option<&str>) -> Option { "libperry_ui_gtk4.a" } else if matches!(target, Some("windows-winui")) { "perry_ui_windows_winui.lib" - } else if matches!(target, Some("windows")) || cfg!(target_os = "windows") { + } else if is_windows_target(target) { "perry_ui_windows.lib" } else { "libperry_ui_macos.a" @@ -1709,7 +1731,7 @@ pub(super) fn build_geisterhand_libs(target: Option<&str>, format: OutputFormat) 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", + target if is_windows_target(target) => "perry-ui-windows", _ if cfg!(target_os = "linux") => "perry-ui-gtk4", _ if cfg!(target_os = "windows") => "perry-ui-windows", _ => "perry-ui-macos", diff --git a/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs b/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs index eab4b141c7..7c7992d3ff 100644 --- a/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs +++ b/crates/perry/src/commands/compile/library_search/windows_toolchain_tests.rs @@ -1,9 +1,12 @@ -use super::msvc_vswhere_installation_path_args; +use super::{ + collect_library_candidates, msvc_vswhere_installation_path_args, xwin_sysroot_lib_paths, + WindowsTargetArch, +}; #[test] fn vswhere_query_requires_msvc_tools_component() { assert_eq!( - msvc_vswhere_installation_path_args(), + msvc_vswhere_installation_path_args(WindowsTargetArch::X86_64), [ "-products", "*", @@ -16,3 +19,57 @@ fn vswhere_query_requires_msvc_tools_component() { ] ); } + +#[test] +fn arm64_vswhere_query_requires_arm64_tools_component() { + let args = msvc_vswhere_installation_path_args(WindowsTargetArch::Aarch64); + assert_eq!(args[3], "Microsoft.VisualStudio.Component.VC.Tools.ARM64"); +} + +#[test] +fn arm64_xwin_sysroot_selects_only_arm64_libraries() { + let temp = tempfile::tempdir().expect("tempdir"); + for arch in ["x86_64", "aarch64"] { + for component in ["crt/lib", "sdk/lib/um", "sdk/lib/ucrt"] { + std::fs::create_dir_all(temp.path().join(component).join(arch)).expect("sysroot dir"); + } + } + + let paths = xwin_sysroot_lib_paths(temp.path(), WindowsTargetArch::Aarch64); + assert_eq!(paths.len(), 3); + assert!(paths.iter().all(|path| path.contains("aarch64"))); + assert!(paths.iter().all(|path| !path.contains("x86_64"))); +} + +#[test] +fn x64_only_xwin_sysroot_is_not_reused_for_arm64() { + let temp = tempfile::tempdir().expect("tempdir"); + for component in ["crt/lib", "sdk/lib/um", "sdk/lib/ucrt"] { + std::fs::create_dir_all(temp.path().join(component).join("x86_64")).expect("sysroot dir"); + } + + assert!(xwin_sysroot_lib_paths(temp.path(), WindowsTargetArch::Aarch64).is_empty()); +} + +#[test] +fn partial_arm64_xwin_sysroot_is_rejected_instead_of_hiding_fallback() { + let temp = tempfile::tempdir().expect("tempdir"); + std::fs::create_dir_all(temp.path().join("crt/lib/aarch64")).expect("partial sysroot"); + // A structured but incomplete sysroot must not degrade to its flat lib + // directory; the Windows lookup can then fall back to Visual Studio/SDK. + std::fs::create_dir_all(temp.path().join("lib")).expect("flat lib dir"); + + assert!(xwin_sysroot_lib_paths(temp.path(), WindowsTargetArch::Aarch64).is_empty()); +} + +#[cfg(target_arch = "x86_64")] +#[test] +fn arm64_target_never_falls_back_to_bare_x64_artifact_dirs() { + let candidates = collect_library_candidates("perry_stdlib.lib", Some("windows-aarch64")); + assert!(!candidates + .iter() + .any(|path| path == std::path::Path::new("target/release/perry_stdlib.lib"))); + assert!(!candidates + .iter() + .any(|path| path == std::path::Path::new("target/debug/perry_stdlib.lib"))); +} 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 fa5443fd80..9a306b3cce 100644 --- a/crates/perry/src/commands/compile/link/build_and_run.rs +++ b/crates/perry/src/commands/compile/link/build_and_run.rs @@ -92,8 +92,7 @@ pub(crate) fn build_and_run_link( (glibc) Linux target, or drop perry/ui for a headless musl build." ); } - let is_windows = matches!(target, Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); + let is_windows = is_windows_target(target); let is_cross_windows = is_windows && !cfg!(target_os = "windows"); let is_cross_ios = is_ios && !cfg!(target_os = "macos"); let is_cross_visionos = is_visionos && !cfg!(target_os = "macos"); @@ -928,7 +927,7 @@ pub(crate) fn build_and_run_link( cmd.arg("-lssl").arg("-lcrypto"); } } else if is_windows { - windows_link::add_system_libs(&mut cmd); + windows_link::add_system_libs(&mut cmd, target); windows_link::embed_app_manifest(&mut cmd, ctx.needs_ui); } else { // macOS frameworks for the stdlib (rustls platform verifier / @@ -1139,6 +1138,14 @@ pub(crate) fn build_and_run_link( OutputFormat::Json => {} } } else { + let windows_rust_triple = windows_target_arch(target) + .map(WindowsTargetArch::rust_triple) + .unwrap_or("x86_64-pc-windows-msvc"); + let windows_ui_build_cmd = + format!("cargo build --release -p perry-ui-windows --target {windows_rust_triple}"); + let windows_winui_build_cmd = format!( + "cargo build --release -p perry-ui-windows-winui --target {windows_rust_triple}" + ); let (lib_name, build_cmd) = if is_watchos { ( "libperry_ui_watchos.a", @@ -1182,13 +1189,10 @@ pub(crate) fn build_and_run_link( } else if matches!(target, Some("windows-winui")) { ( "perry_ui_windows_winui.lib", - "cargo build --release -p perry-ui-windows-winui --target x86_64-pc-windows-msvc", + windows_winui_build_cmd.as_str(), ) } else if is_windows { - ( - "perry_ui_windows.lib", - "cargo build --release -p perry-ui-windows --target x86_64-pc-windows-msvc", - ) + ("perry_ui_windows.lib", windows_ui_build_cmd.as_str()) } else { ( "libperry_ui_macos.a", diff --git a/crates/perry/src/commands/compile/link/mod.rs b/crates/perry/src/commands/compile/link/mod.rs index 80671ec47e..0cd203a118 100644 --- a/crates/perry/src/commands/compile/link/mod.rs +++ b/crates/perry/src/commands/compile/link/mod.rs @@ -36,11 +36,12 @@ use super::{ 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, + find_watchos_swift_runtime, is_android_target, is_native_windows_target, is_windows_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, + windows_pe_subsystem_flag, windows_subsystem_needs_ui, windows_target_arch, CompilationContext, + WindowsTargetArch, }; mod build_and_run; diff --git a/crates/perry/src/commands/compile/link/platform_cmd.rs b/crates/perry/src/commands/compile/link/platform_cmd.rs index 3369d4eb68..19f0470ad8 100644 --- a/crates/perry/src/commands/compile/link/platform_cmd.rs +++ b/crates/perry/src/commands/compile/link/platform_cmd.rs @@ -793,7 +793,7 @@ pub fn select_linker_command( )); } } - } else if let Some(path) = find_msvc_link_exe() { + } else if let Some(path) = find_msvc_link_exe(target) { path } else if is_cross_windows { eprintln!("Warning: lld-link not found for cross-compilation. Install: rustup component add llvm-tools"); @@ -852,9 +852,11 @@ pub fn select_linker_command( // silently ignores /IGNORE codes it doesn't implement, so the flag // is safe on both linker paths. .arg("/IGNORE:4006"); - // Set up MSVC library search paths if LIB env isn't already configured - if std::env::var("LIB").is_err() { - if let Some(lib_paths) = find_msvc_lib_paths() { + // A developer shell's LIB points at the host architecture. Always + // replace it for cross-architecture Windows targets; otherwise an x64 + // prompt silently feeds x64 import libraries to an ARM64 link. + if std::env::var("LIB").is_err() || !is_native_windows_target(target) { + if let Some(lib_paths) = find_msvc_lib_paths(target) { c.env("LIB", lib_paths); } else if is_cross_windows { eprintln!("Warning: No Windows SDK library paths found. Set PERRY_WINDOWS_SYSROOT to your xwin sysroot."); diff --git a/crates/perry/src/commands/compile/link/windows_link.rs b/crates/perry/src/commands/compile/link/windows_link.rs index 9739234346..bb3a8627f9 100644 --- a/crates/perry/src/commands/compile/link/windows_link.rs +++ b/crates/perry/src/commands/compile/link/windows_link.rs @@ -28,7 +28,7 @@ pub(crate) const WINDOWS_APP_MANIFEST: &str = include_str!("windows_app.manifest /// API libs the Rust runtime pulls in. Always emitted on Windows targets (the /// `whoami`/`winhttp`/etc. symbols are needed even by console binaries through /// `perry-stdlib`). -pub(super) fn add_system_libs(cmd: &mut Command) { +pub(super) fn add_system_libs(cmd: &mut Command, target: Option<&str>) { // Win32 GUI + shell system libraries. cmd.arg("user32.lib") .arg("gdi32.lib") @@ -81,6 +81,12 @@ pub(super) fn add_system_libs(cmd: &mut Command) { // through perry-ui-windows's `staticlib` crate-type to perry's final // link line. Closes #732. .arg("winhttp.lib"); + if super::windows_target_arch(target) == Some(super::WindowsTargetArch::Aarch64) { + // MSVC's ARM64 setjmp is a compiler intrinsic whose implementation + // lives in the static VCRuntime archive. Perry's Rust staticlib cannot + // propagate that native-link directive through to this final PE link. + cmd.arg("libvcruntime.lib"); + } } #[cfg(test)] @@ -90,7 +96,7 @@ mod tests { #[test] fn windows_ui_system_imports_survive_the_staticlib_boundary() { let mut command = Command::new("link.exe"); - add_system_libs(&mut command); + add_system_libs(&mut command, Some("windows-aarch64")); let args: Vec<_> = command .get_args() .map(|arg| arg.to_string_lossy().into_owned()) @@ -100,6 +106,11 @@ mod tests { assert!(args.iter().any(|arg| arg == "uxtheme.lib")); assert!(args.iter().any(|arg| arg == "winspool.lib")); assert!(args.iter().any(|arg| arg == "rpcrt4.lib")); + assert!(args.iter().any(|arg| arg == "libvcruntime.lib")); + + let mut x64_command = Command::new("link.exe"); + add_system_libs(&mut x64_command, Some("windows-x86_64")); + assert!(!x64_command.get_args().any(|arg| arg == "libvcruntime.lib")); } } diff --git a/crates/perry/src/commands/compile/lock_scan.rs b/crates/perry/src/commands/compile/lock_scan.rs index 7faa9573f4..d3fe955abe 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::{is_android_target, CompilationContext}; +use super::{is_android_target, windows_target_arch, CompilationContext}; /// #498 - discover every `perry.nativeLibrary` archive a build of /// the project at `project_root` would consume. Scans every @@ -145,6 +145,9 @@ fn derive_target_key(target: Option<&str>) -> String { if is_android_target(target) { return "android".to_string(); } + if let Some(windows) = windows_target_arch(target) { + return format!("windows-{}", windows.lock_token()); + } match target { None => format!("{}-{}", std::env::consts::OS, arch), Some("macos") => format!("macos-{}", arch), @@ -153,7 +156,6 @@ fn derive_target_key(target: Option<&str>) -> String { Some("linux-musl") | Some("linux-x86_64-musl") | Some("linux-aarch64-musl") => { format!("linux-{}", arch) } - Some("windows") | Some("windows-winui") => format!("windows-{}", arch), Some("ios") => "ios".to_string(), Some("ios-simulator") => "ios-simulator".to_string(), Some("tvos") => "tvos".to_string(), @@ -253,6 +255,7 @@ mod lock_integration_tests { assert!(derive_target_key(Some("macos")).starts_with("macos-")); assert!(derive_target_key(Some("linux")).starts_with("linux-")); assert!(derive_target_key(Some("windows")).starts_with("windows-")); + assert_eq!(derive_target_key(Some("windows-aarch64")), "windows-arm64"); } #[test] diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index 0dbf1f4b2b..a344e64066 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -8,7 +8,8 @@ use crate::OutputFormat; use super::super::library_search::{find_harmonyos_sdk, harmonyos_cross_env, host_target_triple}; use super::super::{ - find_perry_workspace_root, is_android_target, rust_target_triple, CompilationContext, + find_perry_workspace_root, is_android_target, is_windows_target, rust_target_triple, + CompilationContext, }; /// Rebuild perry-runtime + perry-stdlib in a single cargo invocation with @@ -547,14 +548,10 @@ pub(crate) fn build_optimized_libs( ); } if matches!(format, OutputFormat::Text) && verbose > 0 { - let (rt_name, std_name) = match target { - Some("windows") | Some("windows-winui") => { - ("perry_runtime.lib", "perry_stdlib.lib") - } - None if cfg!(target_os = "windows") => { - ("perry_runtime.lib", "perry_stdlib.lib") - } - _ => ("libperry_runtime.a", "libperry_stdlib.a"), + let (rt_name, std_name) = if is_windows_target(target) { + ("perry_runtime.lib", "perry_stdlib.lib") + } else { + ("libperry_runtime.a", "libperry_stdlib.a") }; eprintln!( " auto-optimize: Perry workspace source not found, \ @@ -668,17 +665,15 @@ pub(crate) fn build_optimized_libs( } else { target_dir.join("release") }; - let runtime_name = match target { - Some("windows") | Some("windows-winui") => "perry_runtime.lib", - #[cfg(target_os = "windows")] - None => "perry_runtime.lib", - _ => "libperry_runtime.a", + let runtime_name = if is_windows_target(target) { + "perry_runtime.lib" + } else { + "libperry_runtime.a" }; - let stdlib_name = match target { - Some("windows") | Some("windows-winui") => "perry_stdlib.lib", - #[cfg(target_os = "windows")] - None => "perry_stdlib.lib", - _ => "libperry_stdlib.a", + let stdlib_name = if is_windows_target(target) { + "perry_stdlib.lib" + } else { + "libperry_stdlib.a" }; let runtime_path = release_dir.join(runtime_name); let stdlib_path = release_dir.join(stdlib_name); @@ -1211,7 +1206,7 @@ pub(crate) fn build_optimized_libs( Some("tvos-simulator") | Some("tvos") => "perry-ui-tvos", Some("linux") => "perry-ui-gtk4", Some("windows-winui") => "perry-ui-windows-winui", - Some("windows") => "perry-ui-windows", + target if is_windows_target(target) => "perry-ui-windows", Some("macos") => "perry-ui-macos", _ => { if cfg!(target_os = "linux") { diff --git a/crates/perry/src/commands/compile/output_path.rs b/crates/perry/src/commands/compile/output_path.rs index 30ca8b5926..caec87c6b3 100644 --- a/crates/perry/src/commands/compile/output_path.rs +++ b/crates/perry/src/commands/compile/output_path.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; -use super::is_android_target; +use super::{is_android_target, is_windows_target}; /// The output file a compile targets when no `-o` was given. /// @@ -62,12 +62,6 @@ pub(super) fn default_output_path( } } -/// `--target windows*`, or a native build on a Windows host. -fn is_windows_target(target: Option<&str>) -> bool { - matches!(target, Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")) -} - /// `--target macos` or any embedded-Apple target, or a native build on a /// macOS host — the platforms whose shared-library convention is `.dylib`. fn is_apple_target(target: Option<&str>) -> bool { @@ -130,6 +124,10 @@ mod tests { fn windows_target_defaults_to_exe() { assert_eq!(exe(Some("windows"), "app"), PathBuf::from("app.exe")); assert_eq!(exe(Some("windows-winui"), "app"), PathBuf::from("app.exe")); + assert_eq!( + exe(Some("windows-aarch64"), "app"), + PathBuf::from("app.exe") + ); } /// The unix/native executable shape must stay a bare, extension-less name. diff --git a/crates/perry/src/commands/compile/resolve/native_library.rs b/crates/perry/src/commands/compile/resolve/native_library.rs index c1d706ee8a..53e278a844 100644 --- a/crates/perry/src/commands/compile/resolve/native_library.rs +++ b/crates/perry/src/commands/compile/resolve/native_library.rs @@ -55,6 +55,9 @@ pub(super) fn native_manifest_target_key(target: Option<&str>) -> &'static str { if is_android_target(target) { return "android"; } + if super::super::is_windows_target(target) { + return "windows"; + } match target { Some("ios-simulator") | Some("ios") => "ios", Some("visionos-simulator") | Some("visionos") => "visionos", @@ -69,9 +72,6 @@ pub(super) fn native_manifest_target_key(target: Option<&str>) -> &'static str { | Some("linux-musl") | Some("linux-x86_64-musl") | Some("linux-aarch64-musl") => "linux", - // WinUI (#4680) is the same OS/arch as the Win32 target for native - // library resolution (D3d12/Vulkan backends, x64 prebuilts). - Some("windows") | Some("windows-winui") => "windows", Some("web") => "web", Some("macos") => "macos", None if cfg!(target_os = "linux") => "linux", @@ -1312,13 +1312,15 @@ fn arch_for_target_key(target: Option<&str>) -> Option<&'static str> { if let Some(android) = android_target(target) { return Some(android.manifest_arch); } + if let Some(windows) = super::super::windows_target_arch(target) { + return Some(windows.manifest_token()); + } match target { // OS-level targets where both arm64 and x64 are real distribution // targets — surface the arch so wrappers can ship per-arch // prebuilts. Some("macos") => Some("arm64"), Some("linux") => Some("x64"), - Some("windows") | Some("windows-winui") => Some("x64"), Some("harmonyos") => Some("arm64"), Some("harmonyos-simulator") => Some("x64"), // ios/tvos/watchos/visionos: device builds are always arm64 (or diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index d8d22dad91..d8c52cee46 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -5166,9 +5166,7 @@ pub fn run_with_parse_cache( .or_else(|| find_runtime_library(target.as_deref()).ok()); let stdlib_lib_path = stdlib_lib_resolved.clone(); // Check if stdlib will be linked - if so, it provides perry_runtime symbols (no stubs needed) - let target_is_windows = - matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (cfg!(target_os = "windows") && target.is_none()); + let target_is_windows = is_windows_target(target.as_deref()); let will_link_stdlib = (ctx.needs_stdlib || target_is_windows) && stdlib_lib_path.is_some(); // Issue #76 — when the wasm host is // being linked, scan its archive so the `perry_wasm_host_*` symbols @@ -5210,8 +5208,7 @@ pub fn run_with_parse_cache( ); let is_linux = matches!(target.as_deref(), Some(t) if t.starts_with("linux")) || (!cfg!(target_os = "macos") && !cfg!(target_os = "windows") && target.is_none()); - let is_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (cfg!(target_os = "windows") && target.is_none()); + let is_windows = is_windows_target(target.as_deref()); // Symbol prefix depends on object format: // Mach-O targets (macOS, iOS, watchOS, tvOS): nm shows `_` prefix // COFF (Windows targets): no prefix @@ -5523,9 +5520,7 @@ pub fn run_with_parse_cache( // `-o app.appx` is respected verbatim). Non-Windows targets keep the // bare name — Unix executables are conventionally extension-less. Some(p) => { - let is_windows_output = - matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); + let is_windows_output = is_windows_target(target.as_deref()); if is_windows_output && p.extension().is_none() { p.with_extension(windows_default_output_extension(is_dylib, is_staticlib)) } else { @@ -5752,8 +5747,7 @@ pub fn run_with_parse_cache( ); let is_linux = matches!(target.as_deref(), Some(t) if t.starts_with("linux")) || (target.is_none() && cfg!(target_os = "linux")); - let _is_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); + let _is_windows = is_windows_target(target.as_deref()); // is_watchos / is_tvos are defined below (near the per-platform link step). // The is_cross_* bindings used to live here, but they're now derived // inside `link::build_and_run_link` which is the only consumer. @@ -5765,13 +5759,11 @@ pub fn run_with_parse_cache( // emits `perry_module_init` instead of `main` (see is_dylib branch in // codegen/entry.rs, which now also covers `staticlib`). if is_staticlib { - let is_windows_target = - matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); + let windows_target = is_windows_target(target.as_deref()); // Best-effort: drop a stale archive first so `ar` doesn't append to a // previous build's contents. let _ = fs::remove_file(&exe_path); - let mut cmd = if is_windows_target { + let mut cmd = if windows_target { // Archiver precedence (2026-07 audit): MSVC `lib.exe` when a // Visual Studio install (or a vcvars prompt) provides one, else // LLVM's `llvm-lib` — a drop-in lib.exe replacement that ships @@ -5927,8 +5919,7 @@ pub fn run_with_parse_cache( // #6222: the link below runs the HOST toolchain. Reject a target it cannot // honour rather than emitting a host-arch dylib from a cross-compiled object. verify_dylib_target_linkable(target.as_deref())?; - let is_dylib_windows = matches!(target.as_deref(), Some("windows") | Some("windows-winui")) - || (target.is_none() && cfg!(target_os = "windows")); + let is_dylib_windows = is_windows_target(target.as_deref()); let has_plugin_deactivate = ctx .native_modules .values() @@ -5969,7 +5960,7 @@ pub fn run_with_parse_cache( // See also the cross-linker note in `select_linker_command`. let Some(linker) = find_lld_link() .or_else(|| find_llvm_tool("lld-link")) - .or_else(find_msvc_link_exe) + .or_else(|| find_msvc_link_exe(target.as_deref())) else { return Err(anyhow!( "Building a Windows plugin .dll requires a COFF linker and none was \ @@ -5983,10 +5974,10 @@ pub fn run_with_parse_cache( }; let mut c = Command::new(linker); // Both linkers need the CRT + SDK lib dirs for /defaultlib:libcmt. - // Mirror `select_linker_command`: leave a user-provided LIB alone, - // otherwise resolve it (xwin sysroot first, then vswhere). - if std::env::var("LIB").is_err() { - if let Some(lib_paths) = find_msvc_lib_paths() { + // Mirror `select_linker_command`: a host-architecture LIB is valid + // only for a native Windows target. + if std::env::var("LIB").is_err() || !is_native_windows_target(target.as_deref()) { + if let Some(lib_paths) = find_msvc_lib_paths(target.as_deref()) { c.env("LIB", lib_paths); } } @@ -6650,7 +6641,9 @@ fn apple_sdk_sysroot(sdk: &str) -> Result { fn verify_dylib_target_linkable(target: Option<&str>) -> Result<()> { let Some(t) = target else { return Ok(()) }; let host_ok = match t { - "windows" | "windows-winui" => true, + "windows" | "windows-winui" | "windows-x86_64" | "windows-aarch64" | "windows-arm64" => { + true + } t if t.starts_with("linux") => cfg!(target_os = "linux"), t if apple_dylib_cross_target(t).is_some() => cfg!(target_os = "macos"), _ => false, diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 7dbe0054a6..f0974b1c6e 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -106,7 +106,8 @@ pub struct CompileArgs { /// Target platform: ios-simulator, ios, visionos-simulator, visionos, /// 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 + /// windows, windows-x86_64, windows-aarch64, linux (default: native host). + /// See docs/src/cli/flags.md /// for the full target table. #[arg(long)] pub target: Option, diff --git a/crates/perry/src/commands/compile/windows_target.rs b/crates/perry/src/commands/compile/windows_target.rs new file mode 100644 index 0000000000..712435af65 --- /dev/null +++ b/crates/perry/src/commands/compile/windows_target.rs @@ -0,0 +1,124 @@ +//! Windows target-name and architecture resolution. +//! +//! `windows` remains the native/default spelling: it follows the host +//! architecture on Windows and preserves the historical x64 default when +//! cross-compiling from another OS. Explicit architecture spellings make +//! cross-compilation deterministic. + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum WindowsTargetArch { + X86_64, + Aarch64, +} + +impl WindowsTargetArch { + pub(super) const fn rust_triple(self) -> &'static str { + match self { + Self::X86_64 => "x86_64-pc-windows-msvc", + Self::Aarch64 => "aarch64-pc-windows-msvc", + } + } + + pub(super) const fn msvc_dir(self) -> &'static str { + match self { + Self::X86_64 => "x64", + Self::Aarch64 => "arm64", + } + } + + pub(super) const fn xwin_dir(self) -> &'static str { + match self { + Self::X86_64 => "x86_64", + Self::Aarch64 => "aarch64", + } + } + + pub(super) const fn manifest_token(self) -> &'static str { + match self { + Self::X86_64 => "x64", + Self::Aarch64 => "arm64", + } + } + + pub(super) const fn lock_token(self) -> &'static str { + match self { + Self::X86_64 => "x86_64", + Self::Aarch64 => "arm64", + } + } +} + +const fn native_windows_arch() -> WindowsTargetArch { + if cfg!(target_arch = "aarch64") { + WindowsTargetArch::Aarch64 + } else { + WindowsTargetArch::X86_64 + } +} + +/// Resolve a Perry target to its Windows machine architecture. +pub(super) fn windows_target_arch(target: Option<&str>) -> Option { + match target { + Some("windows-aarch64") | Some("windows-arm64") => Some(WindowsTargetArch::Aarch64), + Some("windows-x86_64") => Some(WindowsTargetArch::X86_64), + Some("windows") | Some("windows-winui") => { + if cfg!(target_os = "windows") { + Some(native_windows_arch()) + } else { + // Preserve the long-standing cross-host meaning of `windows`. + Some(WindowsTargetArch::X86_64) + } + } + None if cfg!(target_os = "windows") => Some(native_windows_arch()), + _ => None, + } +} + +pub(super) fn is_windows_target(target: Option<&str>) -> bool { + windows_target_arch(target).is_some() +} + +/// True when the requested Windows target matches this compiler process. +/// Cross-architecture builds must never fall back to bare host artifact dirs. +pub(super) fn is_native_windows_target(target: Option<&str>) -> bool { + cfg!(target_os = "windows") && windows_target_arch(target) == Some(native_windows_arch()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_windows_architectures_are_stable() { + assert_eq!( + windows_target_arch(Some("windows-x86_64")), + Some(WindowsTargetArch::X86_64) + ); + for target in ["windows-aarch64", "windows-arm64"] { + assert_eq!( + windows_target_arch(Some(target)), + Some(WindowsTargetArch::Aarch64) + ); + assert_eq!( + windows_target_arch(Some(target)).map(WindowsTargetArch::rust_triple), + Some("aarch64-pc-windows-msvc") + ); + } + } + + #[test] + fn non_windows_targets_are_rejected() { + assert_eq!(windows_target_arch(Some("linux")), None); + assert!(!is_windows_target(Some("macos"))); + } + + #[test] + fn explicit_other_architecture_is_not_native() { + let other = if cfg!(target_arch = "aarch64") { + "windows-x86_64" + } else { + "windows-aarch64" + }; + assert!(!is_native_windows_target(Some(other))); + } +} diff --git a/crates/perry/src/commands/setup/windows.rs b/crates/perry/src/commands/setup/windows.rs index 0d6a7ca61c..92dc8146c3 100644 --- a/crates/perry/src/commands/setup/windows.rs +++ b/crates/perry/src/commands/setup/windows.rs @@ -88,7 +88,7 @@ pub fn windows_wizard(accept_license: bool) -> Result<()> { let mut cmd = Command::new(&xwin); cmd.arg("--accept-license") .arg("--arch") - .arg("x86_64") + .arg("x86_64,aarch64") .arg("splat") .arg("--disable-symlinks") .arg("--output") diff --git a/crates/perry/src/update_checker.rs b/crates/perry/src/update_checker.rs index 36f7c88b05..f936660ae5 100644 --- a/crates/perry/src/update_checker.rs +++ b/crates/perry/src/update_checker.rs @@ -564,6 +564,10 @@ pub fn platform_artifact_name() -> Option<&'static str> { { return Some("perry-windows-x86_64.zip"); } + #[cfg(all(target_os = "windows", target_arch = "aarch64"))] + { + return Some("perry-windows-aarch64.zip"); + } #[allow(unreachable_code)] None } diff --git a/docs/examples/updater/snippets.ts b/docs/examples/updater/snippets.ts index 3f3851a1c5..a3fcb1c93f 100644 --- a/docs/examples/updater/snippets.ts +++ b/docs/examples/updater/snippets.ts @@ -156,7 +156,7 @@ if (raw) { // ANCHOR: manifest-shape // The manifest is a single JSON file you serve over HTTPS. Each platform -// triple is `-` (darwin-aarch64, darwin-x86_64, windows-x86_64, +// triple is `-` (darwin-aarch64, windows-x86_64, windows-aarch64, // linux-x86_64, linux-aarch64). The wrapper picks the entry matching the // running host and ignores the rest. // diff --git a/docs/src/cli/flags.md b/docs/src/cli/flags.md index 006b68fe58..9cd6646f43 100644 --- a/docs/src/cli/flags.md +++ b/docs/src/cli/flags.md @@ -34,7 +34,9 @@ Use `--target` to cross-compile: | `wearos-tile` | Wear OS Tile | Wear OS Tile (TileService) | | `wasm` | WebAssembly | Self-contained HTML with WASM or raw `.wasm` binary | | `web` | Web | Outputs HTML file with JS | -| `windows` | Windows | Win32/GDI executable (default Windows backend) | +| `windows` | Windows | Win32/GDI executable for the native Windows host architecture; x64 when cross-compiling from another OS | +| `windows-x86_64` | Windows x64 | Explicit x64 MSVC target | +| `windows-aarch64` | Windows ARM64 | Explicit ARM64 MSVC target (`windows-arm64` is accepted as an alias) | | `windows-winui` | Windows (Fluent) | Opt-in WinUI 3 / Fluent backend (#4680). **Scaffold:** currently renders via Win32 while the XAML widget mapping lands incrementally; selects the `perry-ui-windows-winui` static library. Build that lib first: `cargo build --release -p perry-ui-windows-winui`. | | `linux` | Linux | GTK4 executable | diff --git a/docs/src/updater/overview.md b/docs/src/updater/overview.md index 3252ca150b..59e9808ef8 100644 --- a/docs/src/updater/overview.md +++ b/docs/src/updater/overview.md @@ -73,6 +73,7 @@ Platform keys are canonical Rust-style triples: | Apple Silicon mac | `darwin-aarch64` | | Intel mac | `darwin-x86_64` | | Windows 10/11 64-bit | `windows-x86_64` | +| Windows 11 ARM64 | `windows-aarch64` | | Linux 64-bit | `linux-x86_64` | | Linux ARM64 | `linux-aarch64` | diff --git a/npm/perry-win32-arm64/package.json.tmpl b/npm/perry-win32-arm64/package.json.tmpl new file mode 100644 index 0000000000..e576bccad7 --- /dev/null +++ b/npm/perry-win32-arm64/package.json.tmpl @@ -0,0 +1,25 @@ +{ + "name": "@perryts/perry-win32-arm64", + "version": "__VERSION__", + "description": "Perry native TypeScript compiler — Windows ARM64 binary + static libraries.", + "os": ["win32"], + "cpu": ["arm64"], + "files": [ + "bin/", + "lib/", + "README.md", + "LICENSE" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/PerryTS/perry.git" + }, + "homepage": "https://github.com/PerryTS/perry#readme", + "bugs": { + "url": "https://github.com/PerryTS/perry/issues" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/perry/README.md b/npm/perry/README.md index 7f6c858ea0..f01a0afc65 100644 --- a/npm/perry/README.md +++ b/npm/perry/README.md @@ -23,6 +23,7 @@ Installing picks the right prebuilt binary for your platform automatically — ` | Linux x64 (musl / Alpine) | `@perryts/perry-linux-x64-musl` | | Linux arm64 (musl / Alpine) | `@perryts/perry-linux-arm64-musl` | | Windows x64 | `@perryts/perry-win32-x64` | +| Windows arm64 | `@perryts/perry-win32-arm64` | ### Linux: glibc version diff --git a/npm/perry/bin/detect.cjs b/npm/perry/bin/detect.cjs index 86500c34cb..aacc3521c2 100644 --- a/npm/perry/bin/detect.cjs +++ b/npm/perry/bin/detect.cjs @@ -13,6 +13,7 @@ const PLATFORM_PACKAGES = { "linux-x64": "@perryts/perry-linux-x64", "linux-x64-musl": "@perryts/perry-linux-x64-musl", "win32-x64": "@perryts/perry-win32-x64", + "win32-arm64": "@perryts/perry-win32-arm64", }; // Minimum glibc the prebuilt *glibc* Linux binaries can run on. diff --git a/npm/perry/package.json.tmpl b/npm/perry/package.json.tmpl index fe17fa0e17..85918fe99a 100644 --- a/npm/perry/package.json.tmpl +++ b/npm/perry/package.json.tmpl @@ -17,7 +17,8 @@ "@perryts/perry-linux-arm64": "__VERSION__", "@perryts/perry-linux-x64-musl": "__VERSION__", "@perryts/perry-linux-arm64-musl": "__VERSION__", - "@perryts/perry-win32-x64": "__VERSION__" + "@perryts/perry-win32-x64": "__VERSION__", + "@perryts/perry-win32-arm64": "__VERSION__" }, "keywords": [ "typescript", diff --git a/npm/perry/test/detect.test.cjs b/npm/perry/test/detect.test.cjs index f2443168e9..17e96888b2 100644 --- a/npm/perry/test/detect.test.cjs +++ b/npm/perry/test/detect.test.cjs @@ -98,6 +98,10 @@ check("win32-x64 unaffected", () => { const got = detectPlatform({ platform: "win32", arch: "x64", hasGlibcField: false }); assert.deepStrictEqual(got.candidates, ["win32-x64"]); }); +check("win32-arm64 selects its native package", () => { + const got = detectPlatform({ platform: "win32", arch: "arm64", hasGlibcField: false }); + assert.deepStrictEqual(got.candidates, ["win32-arm64"]); +}); console.log("\nhosts that don't report a glibc version"); check("no report header + alpine os-release → musl", () => { @@ -170,8 +174,8 @@ check("every platform package is an optionalDependency of @perryts/perry", () => assert.ok(optional.includes(pkg), `${pkg} missing from optionalDependencies`); } }); -check("the musl packages the fallback targets have publishable manifests", () => { - for (const key of ["linux-x64-musl", "linux-arm64-musl"]) { +check("every detected platform package has a publishable manifest", () => { + for (const key of Object.keys(PLATFORM_PACKAGES)) { const dir = path.join(REPO_ROOT, "npm", "perry-" + key); const tmpl = JSON.parse( fs.readFileSync(path.join(dir, "package.json.tmpl"), "utf8").replace(/__VERSION__/g, "0.0.0") diff --git a/scripts/stage-npm.sh b/scripts/stage-npm.sh index 0c8ece87d5..b9aae64bb3 100755 --- a/scripts/stage-npm.sh +++ b/scripts/stage-npm.sh @@ -68,6 +68,7 @@ PLATFORMS=( "linux-x64-musl:perry-linux-x86_64-musl:libperry_ui_gtk4.a" "linux-arm64-musl:perry-linux-aarch64-musl:libperry_ui_gtk4.a" "win32-x64:perry-windows-x86_64:perry_ui_windows.lib" + "win32-arm64:perry-windows-aarch64:perry_ui_windows.lib" ) # Unix libs shared across platforms (runtime + stdlib). The UI lib is