Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/actions/setup-llvm22/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 20 additions & 22 deletions .github/workflows/release-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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/
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
# ---------------------------------------------------------------------------
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
#
Expand Down
11 changes: 11 additions & 0 deletions changelog.d/8027-windows-arm64.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 21 additions & 1 deletion crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -890,7 +892,11 @@ pub fn resolve_target_triple(name: &str) -> Option<String> {
"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,
}
}
Expand Down Expand Up @@ -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)]
Expand Down
27 changes: 25 additions & 2 deletions crates/perry-runtime/src/ffi/setjmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions crates/perry/src/commands/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
26 changes: 22 additions & 4 deletions crates/perry/src/commands/compile/app_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"),
Expand All @@ -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,
}
Expand All @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion crates/perry/src/commands/compile/cjs_wrap/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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") => {
Expand Down
Loading
Loading