Skip to content

Add Windows ARM64 build - #1937

Merged
oberstet merged 5 commits into
crossbario:masterfrom
ndabas:master
Sep 22, 2026
Merged

oberstet merged 5 commits into
crossbario:masterfrom
ndabas:master

Conversation

@ndabas

@ndabas ndabas commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Description

As the title says. I have added a few extra checks to make sure that the correct wheels are built, because Windows ARM64 will silently emulate AMD64, and tools like uv currently default to installing the emulated Python on WoA rather than the native one.

There is also a temporary step that I added specifically for Windows ARM64, to install a native OpenSSL build -- this is so that the cryptography package (which is a transitive dependency) can be built from source, as they currently have no win_arm64 wheel published. That wheel should be coming sometime soon, as the PR has already been merged. We can remove that step when that happens.


Related Issue(s)

Closes #1936.


Checklist

  • I have referenced relevant issue numbers above
  • I have performed a self-review of my code and it follows
    the style guidelines of this project
  • I have added new or used existing tests that prove my fix
    is effective or that my feature works
  • I have added necessary documentation (if appropriate) and
    updated the changelog
  • I have added an AI assistance disclosure file (required!)
    in this PR

@ndabas ndabas mentioned this pull request Sep 21, 2026
6 of 10 tasks
@oberstet

Copy link
Copy Markdown
Contributor

Thanks for this, @ndabas — and welcome! This is a well-scoped first contribution, and you've correctly identified the two subtle traps that make Windows-on-ARM builds go wrong, which most people miss:

  • the silent x64 emulation on WoA (and uv defaulting to the emulated interpreter), and
  • cryptography having no win_arm64 wheel yet, forcing a source build (Rust + OpenSSL).

A few things I'd like to confirm or tighten already at this point — mostly about making the native-ARM64 guarantee enforced rather than best-effort, since the failure mode here is silent (mis-tagged wheels), which is the worst kind IMO:

1. Make the native-arch check a hard failure. The emulation guard is the crux of the whole PR. Please have the build assert the interpreter's platform.machine() is ARM64 — print it in the job log and fail loudly if it's AMD64/emulated — so a future runner-image change can never silently produce amd64 bytes inside a win_arm64 wheel. A green build that quietly built the wrong thing is the nightmare case.

2. Confirm the wheels are actually tested on the native runner, not just built. Building isn't proof. Please run at least an import smoke test on windows-11-arm that exercises the real moving parts — autobahn, the NVX cffi accelerator, cryptography, txaio — and ideally a slice of the suite. NVX compiling under MSVC/ARM64 and then actually loading is the thing most likely to break, and a pure build step won't catch a bad extension.

3. Scope: CPython-only. PyPy has no Windows/ARM64 build, so the win_arm64 matrix should be CPython-only — please make sure nothing tries a PyPy win_arm64 wheel.

4. Track the temporary OpenSSL/cryptography step for removal. Good that you flagged it as temporary — let's not lose it: gate it on the cryptography floor in pyproject.toml, and open a small follow-up issue ("remove win_arm64 OpenSSL bootstrap once the cryptography floor ships win_arm64 wheels", referencing the upstream PR) so the tech-debt is tracked rather than forgotten. If what currently resolves already ships the win_arm64 wheel, the step may even be droppable now — worth checking what actually resolves.

5. Update the stale note + the platform docs. wheels.yml still carries a comment that "GitHub does NOT provide Windows ARM64 hosted runners" — this PR obsoletes it, so please remove/replace it (otherwise the file contradicts itself). And .github/workflows/README.md enumerates the full platform / arch / interpreter matrix — please add the win_arm64 row there too, or the docs drift immediately.

6. Wheel tag + repair. Please confirm the emitted wheel is tagged *-win_arm64 (not win_amd64), and that any wheel-repair/delocate step in the Windows path handles ARM64 correctly.

7. Where it sits in our wheel tiers. We're formalizing supported-vs-best-effort wheel tiers (#1935). Given a brand-new platform + the temporary crypto bootstrap + runner availability, I'd slot win_arm64 as best-effort (Tier 2) for now — built and smoke-tested, not release-gating — and note that in the docs. Happy to place it once the above is settled.

CI is still running as I write this — I'll do a final pass once it's green, and in particular I want to see in the logs that the win_arm64 job landed on a genuine ARM64 runner (per #1). Really nice first PR; the platform instincts here are exactly right.

(And thanks for including the AI-assistance disclosure per our AI_POLICY.md — appreciated.)

@oberstet

Copy link
Copy Markdown
Contributor

@ndabas — quick heads-up on the one red check: "Code Quality Checks" is on us, not you.

That job runs ty (we deliberately float it on the bleeding edge), and a recent ty update started flagging three pieces of pre-existing dead code in autobahn — two unused _unique_list helpers and one unreachable branch in the WebSocket opening-handshake path. None of it is related to your Windows ARM64 changes; your PR just happened to be the run that surfaced it.

I've opened #1938 to remove that dead code on master in a separate, fully-tested PR (keeping your PR focused on the wheels). Once that lands, please rebase on master — or I'll re-run CI — and this check goes green. Nothing for you to change here.

Thanks again for the contribution, and apologies for the red ✗ that isn't yours.

@ndabas

ndabas commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and the clarification on the failing CI job, @oberstet!

I don't think I've changed anything that should touch or cause the "wheels-arm64 / Build ARM64 wheels (pypy-3.11-bookworm-manylinux_2_36_aarch64" job to fail, but I'll double-check.

On the points you have noted, 3 and 5 are already done. For the rest I'll verify and add whatever is missing and let you know.

@ndabas

ndabas commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks again for the great review. On the remaining points:

  1. I already added a guard in justfile. It asserts on sysconfig.get_platform(), which is the actual value that determines the wheel tag. platform.machine() returns the actual CPU instead, even when running emulated, so using that would mean that we see ARM64 as expected but the wheels would still end up being x86_64 when run with an emulated Python. I'll also add some comments and debug prints around this to make this clear in case someone feels like 'fixing' it later.
  2. I'm adding the smoke tests now, and I see there is no existing smoke test for the built wheels on any platform currently in the CI. So I'm looking at writing the tests in Bash so they run on all platforms (including Windows of course) that we build wheels for. Does that sound okay?
  3. I already added a condition in justfile so we don't try to build PyPy win_arm64 wheels at all.
  4. As cryptography is not currently shipping a win_arm64 wheel, we cannot add a version floor because we don't know what they'll tag that release with. I'll open an issue though to keep track of it.
  5. I updated that note and added platform docs already.
  6. The Windows workflow (any platform) isn't currently doing any wheel repair, so there's nothing to do at the moment. If we ever add delvewheel for that purpose, it handles Windows ARM64 fine.
  7. I have added cpyNNN-win-arm64 to all three check-release-fileset target lists, let me know if you would like me to remove those in order to set this as a "Tier 2" target for now.

On the other failing job ("Build ARM64 wheels (pypy-3.11-bookworm-manylinux_2_36_aarch64)"), it looks like the current release of auditwheel requires a patchelf greater than what is already installed in the image, so the simple fix would be to pip install patchelf auditwheel instead of just installing auditwheel in docker\Dockerfile.pypy-bookworm-manylinux-arm64. Not really part of this PR but I can open another PR for that if you'd like.

oberstet added a commit that referenced this pull request Sep 21, 2026
* start new dev branch for #1938

* Remove dead code surfaced by ty (#1938)

Delete three pieces of pre-existing dead code that ty (floated on the
bleeding edge) flagged on the CI run for #1937, rather than growing the
ty-ignore list:

- src/autobahn/asyncio/component.py: unused private helper _unique_list
  (no call sites anywhere in the repo).
- src/autobahn/twisted/component.py: identical unused _unique_list.
- src/autobahn/websocket/protocol.py: the `if response_body:` block in the
  server opening handshake was unreachable - response_body is hard-set to
  None and never reassigned, and a 101 Switching Protocols response carries
  no body. Removed the block and the dead assignment.

Pure deletion (24 lines); all private/unreachable, no public API change.

Verified locally: check-typing clean (ruff + ty, all checks passed);
Twisted 357 tests PASSED (325 successes, 32 skips); asyncio 275 passed,
0 failures. Handshake output is unchanged (the removed block never ran).
Full wstest compliance + Crossbar smoke run in CI.

Note: This work was completed with AI assistance (Claude Code).
@oberstet

Copy link
Copy Markdown
Contributor

Thanks @ndabas — these are great responses; you addressed every point, and a couple are better than what I asked for. Where I've landed:

Please rebase on master first. Two of the red checks are pre-existing issues we've since fixed on master, and rebasing clears them:

  • Code Quality (ty) — fixed in Fix 1938 #1939.
  • ARM64-Linux auditwheel / patchelf — fixed in Fix 1941 #1942. That's the same "unrelated Docker/auditwheel bug" you spotted in your bonus note — already resolved, so no separate PR needed; the rebase picks it up. Good catch, and sorry it cost you the investigation.

On the individual points:

  1. Native-arch check — good call using sysconfig.get_platform() over platform.machine(); it's exactly the wheel-tag determinant, so an emulated amd64 interpreter is caught. One request: make it a hard failure (non-zero exit when it isn't win-arm64), not just debug output — the failure mode here (amd64 bytes silently landing in a win_arm64 wheel) is the kind that must fail loudly.
  2. Smoke tests — yes please, and you don't need to invent a harness: reuse the existing recipes — just test-wheel-install <wheel> plus test-import / test-smoke. Extending those keeps it consistent with how the rest of the matrix validates wheels (and avoids a bespoke Bash path).
  3. CPython-only — 👍 thanks.
  4. OpenSSL/cryptography step — agreed; a tracking issue is the right call (no win_arm64 cryptography wheel to floor against yet). Same shape as [BUG] [CI] PyPy-ARM64 (bookworm) wheel build fails: apt patchelf 0.14.3 too old for floated auditwheel (requires ≥ 0.14.5) #1941 — link it there for context.
  5. Docs / stale note — 👍
  6. Wheel repair — fine; Windows wheels don't need it today, delvewheel later if that changes.
  7. Wheel tiers — let's classify win_arm64 as best-effort (Tier 2) for now, since it depends on the temporary OpenSSL source-build. I'll take it out of the required release-fileset so it can't gate releases; keep building + smoke-testing it, just non-gating until cryptography ships a win_arm64 wheel and the bootstrap step goes away.

After the rebase it should run green — with one known exception: the emulated pypy-3.11-bookworm-manylinux_2_36_aarch64 job still has a transient QEMU segfault (exit 139) that's unrelated to your change and tracked separately in #1940. If it trips, a re-run usually clears it; we're making that job non-blocking as part of #1940, so don't worry if that single check flakes.

Really nice contribution — thanks for the care on the WoA emulation traps.

@ndabas

ndabas commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the clear responses -- I have updated the code to match.

I used the existing recipes for the smoke tests, and added a couple of tests there. Let me know if that looks okay. CI is all green on my fork now, thanks for pushing those fixes so quickly.

On the wheel tiers, the win_arm64 wheels are in the fileset lists at the moment, I thought about removing those but the process seems to clean up any extra wheels it finds. So those would never get released then. I did see the allow-extra-wheels: true option but that might allow a stray wheel through. Let me know how you would like to handle this.

On a side note, we could consolidate much of the Windows-specific/other platform-specific code since most of those are doing exactly the same thing on every platform. Both pwsh and bash are available on all three platforms. Probably something for another PR if you want that although I could roll some of the trivial ones into this PR, let me know what you think.

@oberstet

Copy link
Copy Markdown
Contributor

thanks a lot! "windows on arm64" coming. \-/ / ;)

side question: are you only after autobahn-python, or zlmdb or crossbar as well for win/arm64?

also, I took the chance to open issues to improve docs as well now: #1946 (and crossbario/zlmdb#147, and crossbario/crossbar#2287)

@oberstet
oberstet merged commit 1e1be1b into crossbario:master Sep 22, 2026
57 of 58 checks passed
@oberstet

Copy link
Copy Markdown
Contributor
image

https://github.com/crossbario/autobahn-python/actions/runs/35770259383/job/106890015630

@oberstet

Copy link
Copy Markdown
Contributor

@ndabas : https://github.com/crossbario/autobahn-python/releases/tag/master-202609230557

this now includes win_arm64 wheels! for cpython. if you have a chance to actually try these on your side, would be curious to know! I don't have any win machine ...

image

@ndabas

ndabas commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Excellent, thanks @oberstet! I tested the nightly on my Windows ARM64 laptop, works fine -- here's the script I used to do that here, in case someone wants to use it (because we need that OpenSSL build for now until cryptography releases win_arm64 wheels):

<#
.SYNOPSIS
    Install and verify autobahn on native Windows ARM64 (win_arm64).

.DESCRIPTION
    autobahn publishes win_arm64 wheels, but its `cryptography` dependency does not:
    no cryptography release since 46.0.3 ships a win_arm64 wheel, and autobahn
    requires cryptography>=50. pip therefore has to build cryptography from source,
    which needs Rust, the MSVC ARM64 toolchain and OpenSSL headers/libs.

    This script provisions OpenSSL via vcpkg, creates a clean virtual environment,
    installs the autobahn wheel from a GitHub release, and runs a set of functional
    checks that do not depend on the autobahn source tree.

    Everything it creates is confined to -VcpkgRoot and -VenvPath. It does not modify
    the machine-wide environment; OPENSSL_DIR is set for this process only.

.PARAMETER ReleaseTag
    Tag of the GitHub release to pull the wheel from.

.PARAMETER PythonVersion
    CPython feature version to install into the venv (3.11 - 3.14).

.PARAMETER VenvPath
    Directory for the throwaway virtual environment.

.PARAMETER VcpkgRoot
    Directory for the vcpkg checkout. Reused if it already exists.

.PARAMETER PythonExe
    Optional path to an existing native ARM64 python.exe. If omitted, uv is used to
    fetch a matching interpreter (recommended).

.EXAMPLE
    .\install-win-arm64.ps1

.EXAMPLE
    .\install-win-arm64.ps1 -PythonVersion 3.12 -VenvPath C:\venvs\autobahn
#>
[CmdletBinding()]
param(
    [string]$ReleaseTag = 'master-202609230557',
    [ValidateSet('3.11', '3.12', '3.13', '3.14')]
    [string]$PythonVersion = '3.14',
    [string]$VenvPath = (Join-Path $env:TEMP 'autobahn-win-arm64'),
    [string]$VcpkgRoot = (Join-Path $env:USERPROFILE 'vcpkg'),
    [string]$PythonExe
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'   # Invoke-WebRequest is ~10x slower with the progress bar

$Repo = 'crossbario/autobahn-python'
$Triplet = 'arm64-windows-static-md'

function Write-Step { param([string]$Message) Write-Host "`n==> $Message" -ForegroundColor Cyan }
function Write-Info { param([string]$Message) Write-Host "    $Message" -ForegroundColor DarkGray }
function Die { param([string]$Message) Write-Host "`nERROR: $Message" -ForegroundColor Red; exit 1 }

# ---------------------------------------------------------------- preflight --

Write-Step 'Checking prerequisites'

if ($env:PROCESSOR_ARCHITECTURE -ne 'ARM64') {
    Die "this script must run in a native ARM64 shell (found $env:PROCESSOR_ARCHITECTURE). If you are in an x64 PowerShell on an ARM64 device, launch the ARM64 build instead."
}
Write-Info "host architecture: ARM64"

if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
    Die 'git not found. Install Git for Windows: winget install Git.Git'
}

# cryptography is a Rust extension; it needs cargo and the ARM64 target.
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
    Die 'Rust not found; cryptography must be compiled from source. Install it with: winget install Rustlang.Rustup'
}
Write-Info "rust: $((rustc --version) -join '')"

# openssl-sys and cryptography's cdylib both need the MSVC ARM64 compiler.
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
if (-not (Test-Path $vswhere)) {
    Die 'Visual Studio Installer not found. Install VS Build Tools with the "MSVC v143 - VS 2022 C++ ARM64 build tools" component.'
}
$vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64 -property installationPath
if (-not $vsPath) {
    Die 'MSVC ARM64 build tools not installed. Add the "MSVC v143 - VS 2022 C++ ARM64 build tools" component via the Visual Studio Installer.'
}
Write-Info "msvc: $vsPath"

if (-not $PythonExe -and -not (Get-Command uv -ErrorAction SilentlyContinue)) {
    Die 'uv not found and -PythonExe not given. Install uv (winget install astral-sh.uv) or pass -PythonExe pointing at a native ARM64 python.exe'
}

# ------------------------------------------------------------------- vcpkg --

Write-Step "Provisioning OpenSSL via vcpkg (triplet: $Triplet)"

$vcpkgExe = Join-Path $VcpkgRoot 'vcpkg.exe'
if (-not (Test-Path $vcpkgExe)) {
    if (-not (Test-Path $VcpkgRoot)) {
        Write-Info "cloning vcpkg into $VcpkgRoot"
        git clone --depth 1 https://github.com/microsoft/vcpkg.git $VcpkgRoot
        if ($LASTEXITCODE -ne 0) { Die 'git clone of vcpkg failed' }
    }
    Write-Info 'bootstrapping vcpkg (downloads its own cmake/ninja)'
    & (Join-Path $VcpkgRoot 'bootstrap-vcpkg.bat') -disableMetrics
    if ($LASTEXITCODE -ne 0) { Die 'vcpkg bootstrap failed' }
}
Write-Info "vcpkg: $vcpkgExe"

$opensslDir = Join-Path $VcpkgRoot "installed\$Triplet"
if (Test-Path (Join-Path $opensslDir 'include\openssl\ssl.h')) {
    Write-Info 'openssl already present, skipping build'
}
else {
    Write-Info 'building openssl - this takes a while on first run'
    & $vcpkgExe install --triplet $Triplet --clean-after-build openssl
    if ($LASTEXITCODE -ne 0) { Die 'vcpkg install openssl failed' }
}

# Consumed by the openssl-sys crate during the cryptography build. Process-scoped.
$env:OPENSSL_DIR = $opensslDir
Write-Info "OPENSSL_DIR = $env:OPENSSL_DIR"

# -------------------------------------------------------------------- venv --

Write-Step "Creating a clean virtual environment ($VenvPath)"

if (Test-Path $VenvPath) {
    Write-Info 'removing previous environment'
    Remove-Item -Recurse -Force $VenvPath
}

if ($PythonExe) {
    & $PythonExe -m venv $VenvPath
    if ($LASTEXITCODE -ne 0) { Die "venv creation failed using $PythonExe" }
}
else {
    # The platform suffix is required: on Windows ARM64 a bare "cpython-3.14"
    # request resolves to an x86_64 interpreter running under emulation.
    uv venv --seed --python "cpython-$PythonVersion-windows-aarch64-none" $VenvPath
    if ($LASTEXITCODE -ne 0) { Die 'uv venv creation failed' }
}

$py = Join-Path $VenvPath 'Scripts\python.exe'
if (-not (Test-Path $py)) { Die "no interpreter at $py" }

$platform = & $py -c "import sysconfig; print(sysconfig.get_platform())"
if ($platform -ne 'win-arm64') {
    Die "interpreter is '$platform', expected 'win-arm64'. An emulated x86_64 Python cannot load win_arm64 wheels."
}
$pyTag = & $py -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')"
Write-Info "interpreter: $platform ($pyTag)"

# ------------------------------------------------------------------- wheel --

Write-Step "Downloading the $pyTag win_arm64 wheel from release '$ReleaseTag'"

$headers = @{ 'User-Agent' = 'autobahn-win-arm64-installer' }
try {
    $release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/tags/$ReleaseTag" -Headers $headers
}
catch {
    Die "could not read release '$ReleaseTag': $($_.Exception.Message)"
}

$asset = $release.assets | Where-Object { $_.name -like "*-$pyTag-$pyTag-win_arm64.whl" } | Select-Object -First 1
if (-not $asset) {
    $have = ($release.assets | Where-Object { $_.name -like '*win_arm64*' } | ForEach-Object { $_.name }) -join ', '
    Die "no $pyTag win_arm64 wheel in '$ReleaseTag'. Available: $(if ($have) { $have } else { '(none)' })"
}

$wheel = Join-Path $VenvPath $asset.name
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $wheel -Headers $headers
Write-Info ("{0} ({1:N0} KB)" -f $asset.name, ((Get-Item $wheel).Length / 1KB))

# ----------------------------------------------------------------- install --

Write-Step 'Installing the wheel (cryptography is compiled from source here)'

& $py -m pip install --upgrade pip setuptools wheel --quiet
if ($LASTEXITCODE -ne 0) { Die 'failed to update base packaging tools' }

& $py -m pip install $wheel
if ($LASTEXITCODE -ne 0) {
    Die 'installation failed. If the cryptography build is the cause, confirm OPENSSL_DIR above points at a vcpkg tree containing include\openssl\ssl.h.'
}

# ------------------------------------------------------------------- tests --

Write-Step 'Running functional checks'

$verifier = Join-Path $VenvPath 'verify_autobahn.py'
Set-Content -Path $verifier -Encoding UTF8 -Value @'
"""Post-install verification for autobahn on Windows ARM64.

Self-contained: exercises only the installed package, never the source tree.
"""

import os
import pathlib
import struct
import subprocess
import sys
import sysconfig

RESULTS = []


def check(name):
    def decorate(fn):
        try:
            detail = fn()
            RESULTS.append(True)
            print(f"  [PASS] {name}" + (f" -- {detail}" if detail else ""))
        except Exception as exc:
            RESULTS.append(False)
            print(f"  [FAIL] {name} -- {type(exc).__name__}: {exc}")
        return fn
    return decorate


@check("interpreter is native ARM64")
def _():
    platform = sysconfig.get_platform()
    assert platform == "win-arm64", f"got {platform}"
    return platform


@check("autobahn imports")
def _():
    import autobahn
    # Guard against accidentally importing a source checkout instead of the wheel.
    location = pathlib.Path(autobahn.__file__).parent
    assert "site-packages" in str(location), f"imported from {location}"
    return f"version {autobahn.__version__}"


@check("NVX native acceleration is built and active")
def _():
    from autobahn.websocket import HAS_NVX, USES_NVX
    assert HAS_NVX, "NVX not built into this wheel"
    assert USES_NVX, "NVX built but inactive (is AUTOBAHN_USE_NVX=0 set?)"
    import _nvx_xormasker
    name = pathlib.Path(_nvx_xormasker.__file__).name
    assert "win_arm64" in name, f"unexpected module tag: {name}"
    return name


@check("shipped binaries are ARM64 machine code")
def _():
    import autobahn
    import _nvx_xormasker
    targets = [
        pathlib.Path(_nvx_xormasker.__file__),
        pathlib.Path(autobahn.__file__).parent / "_flatc" / "bin" / "flatc.exe",
    ]
    for path in targets:
        with open(path, "rb") as handle:
            handle.seek(0x3C)
            pe_offset = struct.unpack("<I", handle.read(4))[0]
            handle.seek(pe_offset)
            assert handle.read(4) == b"PE\0\0", f"{path.name} is not a PE image"
            machine = struct.unpack("<H", handle.read(2))[0]
        # 0xAA64 == IMAGE_FILE_MACHINE_ARM64
        assert machine == 0xAA64, f"{path.name} has PE machine 0x{machine:04X}, expected 0xAA64"
    return f"{len(targets)} binaries verified"


@check("XOR masker round-trips")
def _():
    from autobahn.websocket.xormasker import create_xor_masker
    payload = bytes(range(256)) * 31
    mask = b"\xde\xad\xbe\xef"
    masked = create_xor_masker(mask, len(payload)).process(payload)
    assert masked != payload, "masking was a no-op"
    unmasked = create_xor_masker(mask, len(payload)).process(masked)
    assert unmasked == payload, "round-trip mismatch"
    return f"{len(payload)} bytes"


@check("UTF-8 validator accepts valid and rejects invalid input")
def _():
    from autobahn.websocket.utf8validator import Utf8Validator
    validator = Utf8Validator()

    validator.reset()
    assert validator.validate("Hello, ARM64 \u2713 \U0001f600".encode())[0], "rejected valid UTF-8"

    validator.reset()
    # 0xC0 0x80 is an overlong encoding of NUL and must be rejected.
    assert not validator.validate(b"\xc0\x80")[0], "accepted an overlong encoding"
    return "accept/reject correct"


@check("bundled flatc runs")
def _():
    import autobahn
    flatc = pathlib.Path(autobahn.__file__).parent / "_flatc" / "bin" / "flatc.exe"
    assert flatc.is_file(), f"missing {flatc}"
    out = subprocess.run([str(flatc), "--version"], capture_output=True, text=True, timeout=60)
    assert out.returncode == 0, f"exit {out.returncode}: {out.stderr.strip()}"
    return out.stdout.strip()


@check("cryptography works against the vcpkg OpenSSL")
def _():
    from cryptography.fernet import Fernet
    from cryptography.hazmat.backends.openssl.backend import backend
    secret = b"windows arm64"
    key = Fernet.generate_key()
    assert Fernet(key).decrypt(Fernet(key).encrypt(secret)) == secret, "Fernet round-trip failed"
    return backend.openssl_version_text()


@check("WebSocket echo over loopback")
def _():
    import asyncio
    from autobahn.asyncio.websocket import (
        WebSocketClientFactory, WebSocketClientProtocol,
        WebSocketServerFactory, WebSocketServerProtocol,
    )

    payload = b"autobahn on windows arm64 " * 64
    received = {}

    class Echo(WebSocketServerProtocol):
        def onMessage(self, msg, isBinary):
            self.sendMessage(msg, isBinary)

    class Probe(WebSocketClientProtocol):
        def onOpen(self):
            self.sendMessage(payload, True)

        def onMessage(self, msg, isBinary):
            future = received["future"]
            if not future.done():
                future.set_result(msg)
            self.sendClose()

    async def exercise():
        loop = asyncio.get_running_loop()
        received["future"] = loop.create_future()

        server_factory = WebSocketServerFactory()
        server_factory.protocol = Echo
        server = await loop.create_server(server_factory, "127.0.0.1", 0)
        port = server.sockets[0].getsockname()[1]

        client_factory = WebSocketClientFactory(f"ws://127.0.0.1:{port}")
        client_factory.protocol = Probe
        transport, _ = await loop.create_connection(client_factory, "127.0.0.1", port)

        try:
            echoed = await asyncio.wait_for(received["future"], timeout=30)
        finally:
            transport.close()
            server.close()
            await server.wait_closed()

        assert echoed == payload, "echoed payload differs from what was sent"
        return f"{len(payload)} bytes echoed on port {port}"

    return asyncio.run(exercise())


passed = sum(RESULTS)
total = len(RESULTS)
print()
print("=" * 72)
if passed == total:
    print(f"ALL CHECKS PASSED ({passed}/{total})")
else:
    print(f"CHECKS FAILED ({passed}/{total} passed)")
print("=" * 72)
sys.exit(0 if passed == total else 1)
'@

& $py $verifier
$testsOk = ($LASTEXITCODE -eq 0)

# ------------------------------------------------------------------ report --

Write-Step 'Summary'
Write-Info "environment : $VenvPath"
Write-Info "activate    : $VenvPath\Scripts\Activate.ps1"
Write-Info "openssl     : $opensslDir"

if (-not $testsOk) {
    Die 'one or more functional checks failed (see output above)'
}

Write-Host "`nautobahn is installed and working on Windows ARM64.`n" -ForegroundColor Green

@ndabas

ndabas commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

zlmdb and crossbar aren't on my list at the moment, but I would be happy to contribute if you're looking to get Windows ARM64 support for those as well!

@oberstet

oberstet commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

@ndabas — the native win_arm64 wheels are building and shipping in the nightly master-* GitHub
release now, and they'll land on PyPI with the next tagged release. Really solid work — the tricky
Windows-on-ARM parts (the native-arch guard so WoA doesn't silently emulate x64, the temporary
OpenSSL/cryptography bootstrap, CPython-only scoping) were all handled just right.

And yes — absolutely, welcome aboard! I'd love to get Windows ARM64 support across the rest of the
WAMP stack, and your offer to help with zlmdb and crossbar is very much appreciated. Let's do it.

A bit of framing so we pick good targets:

  • zlmdb is the natural next step: like autobahn it has a native component (vendored LMDB + a
    FlatBuffers runtime - as autobahn for reasons, both WAMP fbs-in-transit and fbs-at-rest, must land paired/in-sync, see [Security] Bump vendored FlatBuffers past v25.12.19 for verifier/memory-safety fixes (coordinated with zlmdb) #1951 and [Security] Bump vendored FlatBuffers past v25.12.19 for verifier/memory-safety fixes (coordinated with autobahn-python) zlmdb#148) and it already uses the same shared wamp-cicd wheel/CI scaffolding, and wamp-ai audit file provenance tracking, so the win_arm64 job should port over closely to what you just did here. A WoA wheel there gets us most of the way to a fully installable stack on Windows ARM64.
  • wamp-xbr is also needed: Crossbar.io includes Ethereum integration for decentralized WAMP for connecting WAMP router nodes operated by different ops (and it also includes support for what is called "WAMP Router-to-Router" for distributed WAMP, connecting WAMP router nodes operated by the same ops), repo is wamp-proto/wamp-xbr. So wamp-xbr, wamp-cicd, wamp-ai are all shared and part of the WAMP proto project itself. the challenge with wamp-xbr at the technical integration level (win arm64) will be: make all deps fly! mostly crypto centric stuff. but the whole repo/project is again reusing our shared infra and workflows!
  • crossbar is a bit different — it's a pure-Python application (Twisted), so it isn't about
    compiling crossbar itself but about making sure the whole dependency stack has WoA wheels so
    pip install crossbar "just works" on Windows ARM64. That's really autobahn (done) + zlmdb (next) +
    the upstream natives (cryptography etc.). Same Tier-2 caveat likely applies until those upstream
    wheels exist — worth tracking the same way you did here.

Process-wise, we work issue-first per repo (each change starts from a GitHub issue the PR references).
Happy to open tracking issues on zlmdb and crossbar for the Windows ARM64 work so you've got a
clear home to PR against — I'll cc you. If you'd rather scope them yourself first, that's great too;
just say which you'd like to start with.

Thanks again for #1937, and welcome to the WAMP crew. 🚀

@ndabas

ndabas commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Great, thanks again @oberstet! Glad to see things moving for Windows ARM64 users.

If you could open tracking issues in these projects and cc me -- that would be great, I'll take a look and scope out the work needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add Windows ARM64 wheels

2 participants