From c66bb40d8a465db0163d779c41792a8bf1d618bb Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Tue, 16 Jun 2026 12:48:24 +0200 Subject: [PATCH 1/3] start new dev branch; add audit file --- .audit/oberstet_fix_1834.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .audit/oberstet_fix_1834.md diff --git a/.audit/oberstet_fix_1834.md b/.audit/oberstet_fix_1834.md new file mode 100644 index 000000000..445f53deb --- /dev/null +++ b/.audit/oberstet_fix_1834.md @@ -0,0 +1,8 @@ +- [ ] I did **not** use any AI-assistance tools to help create this pull request. +- [x] I **did** use AI-assistance tools to *help* create this pull request. +- [x] I have read, understood and followed the projects' [AI Policy](https://github.com/crossbario/autobahn-python/blob/main/AI_POLICY.md) when creating code, documentation etc. for this pull request. + +Submitted by: @oberstet +Date: 2026-06-16 +Related issue(s): #1835 +Branch: oberstet:fix_1835 From 3f36a63fa0aa3ad74ec345bb58e568bd252621d8 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Tue, 16 Jun 2026 12:49:34 +0200 Subject: [PATCH 2/3] correct audit file --- .audit/oberstet_fix_1834.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.audit/oberstet_fix_1834.md b/.audit/oberstet_fix_1834.md index 445f53deb..21e83fbc0 100644 --- a/.audit/oberstet_fix_1834.md +++ b/.audit/oberstet_fix_1834.md @@ -4,5 +4,5 @@ Submitted by: @oberstet Date: 2026-06-16 -Related issue(s): #1835 -Branch: oberstet:fix_1835 +Related issue(s): #1834 +Branch: oberstet:fix_1834 From 2c3765448ad5682f6518def48798de59a6b6cd4c Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Tue, 16 Jun 2026 12:58:55 +0200 Subject: [PATCH 3/3] Default NVX builds to a portable arch baseline; -march=native opt-in (#1834) Fixes cross-compilation of the NVX CFFI extensions (e.g. Buildroot/Yocto targeting aarch64 on an x86-64 host), which failed because the cross toolchain was handed the host-only -march=native flag: cc1: error: unknown value 'native' for '-march' Changes (implementing the design agreed in PR #1835): - get_compile_args(): the portable, safe architecture baseline is now the default for ALL build contexts (wheels, local source installs, and cross-compilation). -march=native is no longer the implicit default for local source builds; it is available opt-in via AUTOBAHN_ARCH_TARGET=native (build host == run host only). An unknown/cross target arch emits no -march flag, letting the toolchain defaults / distro CFLAGS decide. - Detect the TARGET architecture via sysconfig.get_platform() instead of platform.machine() (which reports the build host under cross-compilation). Approach contributed by @jameshilliard in PR #1835. - is_building_wheel() is retained for backward compatibility but no longer gates the default (documented as such). - Add src/autobahn/nvx/test/test_compile_args.py covering the default-safe / opt-in-native behavior, the arch->flag mapping, target detection, and the unknown-target (no -march) cross-compile case. Thanks to @jameshilliard for the original report (#1834) and PR #1835. Note: This work was completed with AI assistance (Claude Code). --- docs/changelog.rst | 1 + src/autobahn/nvx/_compile_args.py | 116 ++++++++++++++------- src/autobahn/nvx/test/test_compile_args.py | 114 ++++++++++++++++++++ 3 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 src/autobahn/nvx/test/test_compile_args.py diff --git a/docs/changelog.rst b/docs/changelog.rst index d662da1ea..4f4cc8b0f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,7 @@ Changelog * Fix ``scripts/update_flatbuffers.sh`` git-version capture for submodule checkouts (``.git`` is a file, not a directory) (#1853) * Bump the ``.cicd`` (wamp-cicd) submodule to pick up the script/shell-injection fix in the shared ``identifiers.yml`` reusable workflow (untrusted GitHub event fields are now passed via ``env:`` as quoted data with a fail-closed branch-name allowlist) (#1856) * Fail wheel builds hard when NVX was requested (``AUTOBAHN_USE_NVX``) but the CFFI extension did not compile, instead of silently degrading to a pure-Python (``py3-none-any``) wheel. A transient native-compile crash (e.g. a ``gcc`` SIGSEGV under QEMU ARM64 emulation) now aborts the build with a non-zero exit so CI can retry it, rather than uploading a structurally valid but unintended artifact. Building with ``AUTOBAHN_USE_NVX=0`` still produces a pure-Python wheel as before (#1856) +* Fix NVX native-extension builds breaking under cross-compilation (e.g. Buildroot/Yocto for aarch64), where the cross toolchain rejected the host-only ``-march=native`` flag (``unknown value 'native' for '-march'``). The default architecture target is now the portable baseline for *all* build contexts (wheels, local source installs, and cross-compilation), with ``-march=native`` available opt-in via ``AUTOBAHN_ARCH_TARGET=native``. The target architecture is detected via ``sysconfig.get_platform()`` so the correct baseline is chosen when cross-compiling. Thanks to @jameshilliard for the original report and approach (#1834, #1835) 25.12.2 ------- diff --git a/src/autobahn/nvx/_compile_args.py b/src/autobahn/nvx/_compile_args.py index e17d568fe..2a35ab310 100644 --- a/src/autobahn/nvx/_compile_args.py +++ b/src/autobahn/nvx/_compile_args.py @@ -34,13 +34,15 @@ Strategy -------- -For **WHEEL BUILDS** (distribution via PyPI): - Use safe, portable baseline architectures to ensure wheels work on a wide - range of CPUs without causing SIGILL (Illegal Instruction) crashes. +**By default** - for *every* build context (PyPI wheels, local source installs, +and cross-compilation) - a safe, portable baseline architecture is used. This +keeps the resulting binaries runnable on a wide range of CPUs without SIGILL +(Illegal Instruction) crashes, and never hands a cross-compilation toolchain the +host-only ``-march=native`` flag (which it rejects). -For **SOURCE BUILDS** (local installation): - Use -march=native to generate optimal code for the specific CPU where - the build is happening, maximizing performance. +Maximum-performance ``-march=native`` code generation is **opt-in** via +``AUTOBAHN_ARCH_TARGET=native``; use it only when the build host is also the run +host (e.g. Gentoo/Arch packages, dedicated single-machine deployments). Architecture Baselines ---------------------- @@ -61,9 +63,10 @@ AUTOBAHN_ARCH_TARGET : str, optional User/distro override for architecture target: - - "native" : Force -march=native (maximum performance, may break portability) - - "safe" : Force portable baseline (ensures compatibility) - - Not set : Auto-detect based on build context (recommended) + - "native" : Force -march=native (maximum performance, build host only; + unsafe for distributed wheels and cross-compilation) + - "safe" : Force portable baseline (explicit; same as the default) + - Not set : Portable baseline (the safe default; works for cross-compilation) AUTOBAHN_WHEEL_BUILD : str, optional Explicit marker for wheel builds ("true" or "1") @@ -81,13 +84,19 @@ Examples -------- -**GitHub Actions building wheels:** - >>> # Automatically detects CI=true, uses -march=x86-64-v2 +**GitHub Actions building wheels (default):** + >>> # Default is the portable baseline >>> get_compile_args() ['-std=c99', '-Wall', '-Wno-strict-prototypes', '-O3', '-march=x86-64-v2'] -**User installing from source:** - >>> # Detects local build, uses -march=native +**User installing from source (default):** + >>> # Default is the portable baseline (safe for cross-compilation too) + >>> get_compile_args() + ['-std=c99', '-Wall', '-Wno-strict-prototypes', '-O3', '-march=x86-64-v2'] + +**Opting in to -march=native (build host == run host):** + >>> import os + >>> os.environ['AUTOBAHN_ARCH_TARGET'] = 'native' >>> get_compile_args() ['-std=c99', '-Wall', '-Wno-strict-prototypes', '-O3', '-march=native'] @@ -115,6 +124,8 @@ Related Issues -------------- - #1717: SIGILL crashes from -march=native in distributed wheels +- #1834: -march=native breaks cross-compilation; the default is now the portable + baseline for all build contexts, with -march=native available opt-in. See Also -------- @@ -124,6 +135,7 @@ import os import sys +import sysconfig import platform @@ -131,6 +143,13 @@ def is_building_wheel(): """ Detect if we're building a wheel for distribution vs. a local source install. + .. note:: + + As of #1834 the default architecture target is the portable baseline for + *every* build context (wheels, local source installs, cross-compilation), + so this helper no longer influences :func:`get_compile_args`. It is + retained for backward compatibility and for external callers/tooling. + Returns ------- bool @@ -194,7 +213,7 @@ def get_compile_args(): return ["/O2", "/W3"] # GCC/Clang on POSIX (Linux, macOS, *BSD) - machine = platform.machine().lower() + machine = _get_target_machine() # Base flags for all POSIX platforms base_args = [ @@ -208,34 +227,53 @@ def get_compile_args(): arch_override = os.environ.get("AUTOBAHN_ARCH_TARGET", "").lower() if arch_override == "native": - # User explicitly wants -march=native (maximum performance) - # Use case: Gentoo, Arch Linux, performance-critical deployments + # Explicit opt-in only: -march=native generates code for the exact CPU of + # the *build* host (maximum performance). Use this when the build host is + # also the run host (e.g. Gentoo, Arch Linux, performance-critical + # deployments). It is NOT safe for distributed wheels or cross-compilation. return base_args + ["-march=native"] - elif arch_override == "safe": - # User explicitly wants portable baseline - # Use case: Debian, Ubuntu, RHEL package builds - arch_flag = _get_safe_march_flag(machine) - if arch_flag: - return base_args + [arch_flag] - else: - # Unknown arch: use compiler defaults - return base_args - - elif is_building_wheel(): - # Building wheel for distribution: use portable baseline - # These wheels may run on CPUs different from build machine - arch_flag = _get_safe_march_flag(machine) - if arch_flag: - return base_args + [arch_flag] - else: - # Unknown arch: use compiler defaults - return base_args + # Default for everyone (AUTOBAHN_ARCH_TARGET unset or "safe"): a portable + # baseline architecture. Defaulting to "safe" rather than -march=native is + # what makes cross-compilation work out of the box - a cross toolchain + # rejects -march=native ("unknown value 'native' for '-march'", #1834) - and + # also prevents SIGILL crashes from over-optimized distributed wheels (#1717). + arch_flag = _get_safe_march_flag(machine) + if arch_flag: + return base_args + [arch_flag] - else: - # Building from source locally: use -march=native for maximum performance - # Build machine = runtime machine, so native optimizations are safe and optimal - return base_args + ["-march=native"] + # Unknown / cross target architecture: emit no -march flag and let the + # toolchain defaults (or distro-supplied CFLAGS, e.g. Buildroot/Yocto) + # govern code generation. + return base_args + + +def _get_target_machine(): + """ + Return the *target* machine architecture for the build. + + Uses ``sysconfig.get_platform()`` rather than ``platform.machine()`` so that + the correct architecture is detected when cross-compiling (e.g. + Buildroot/Yocto building aarch64 on an x86-64 host): ``platform.machine()`` + reports the build host (``uname``), whereas ``sysconfig`` reflects the + interpreter's configured target platform. Falls back to + ``platform.machine()`` when no architecture token can be derived. + + (Target-detection approach contributed in PR #1835 by @jameshilliard.) + + Returns + ------- + str + Lower-cased target architecture token, e.g. "x86_64", "aarch64", + "arm64". + """ + plat = sysconfig.get_platform().lower() + # Examples: 'linux-x86_64', 'linux-aarch64', 'macosx-11.0-arm64', + # 'win-amd64', 'win32'. + if plat == "win32": + return "x86" + machine = plat.rsplit("-", 1)[-1] + return machine or platform.machine().lower() def _get_safe_march_flag(machine): diff --git a/src/autobahn/nvx/test/test_compile_args.py b/src/autobahn/nvx/test/test_compile_args.py new file mode 100644 index 000000000..e103f966c --- /dev/null +++ b/src/autobahn/nvx/test/test_compile_args.py @@ -0,0 +1,114 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### + +import os +import sys +import unittest +from unittest import mock + +from autobahn.nvx import _compile_args +from autobahn.nvx._compile_args import ( + get_compile_args, + _get_safe_march_flag, + _get_target_machine, +) + + +class TestSafeMarchFlag(unittest.TestCase): + """Architecture -> safe -march flag mapping (#1834, #1717).""" + + def test_x86_64_variants(self): + for machine in ("x86_64", "amd64", "x64"): + self.assertEqual(_get_safe_march_flag(machine), "-march=x86-64-v2") + + def test_arm64_variants(self): + for machine in ("aarch64", "arm64"): + self.assertEqual(_get_safe_march_flag(machine), "-march=armv8-a") + + def test_unknown_arch_returns_none(self): + # Unknown architectures must not get a -march flag (let the toolchain + # defaults / CFLAGS decide), so cross-compilation is never broken. + self.assertIsNone(_get_safe_march_flag("riscv64")) + + +class TestTargetMachine(unittest.TestCase): + def test_returns_nonempty_lowercase(self): + machine = _get_target_machine() + self.assertIsInstance(machine, str) + self.assertTrue(machine) + self.assertEqual(machine, machine.lower()) + + def test_uses_sysconfig_target_arch(self): + # When cross-compiling, sysconfig.get_platform() reflects the *target* + # (e.g. aarch64) even on an x86-64 build host - this is the whole point + # of preferring it over platform.machine() (#1834 / PR #1835). + with mock.patch( + "autobahn.nvx._compile_args.sysconfig.get_platform", + return_value="linux-aarch64", + ): + self.assertEqual(_get_target_machine(), "aarch64") + + +class TestGetCompileArgs(unittest.TestCase): + """Default-safe / opt-in-native behaviour (#1834).""" + + def test_default_is_never_native(self): + # The core #1834 guard: with no override, -march=native must NEVER be + # emitted (it breaks cross-compilation and can SIGILL distributed wheels). + with mock.patch.dict("os.environ", {}, clear=False): + os.environ.pop("AUTOBAHN_ARCH_TARGET", None) + self.assertNotIn("-march=native", get_compile_args()) + + def test_safe_matches_default(self): + with mock.patch.dict("os.environ", {}, clear=False): + os.environ.pop("AUTOBAHN_ARCH_TARGET", None) + default_args = get_compile_args() + with mock.patch.dict("os.environ", {"AUTOBAHN_ARCH_TARGET": "safe"}): + self.assertEqual(get_compile_args(), default_args) + + @unittest.skipIf(sys.platform == "win32", "MSVC path uses /arch, not -march") + def test_native_is_opt_in(self): + with mock.patch.dict("os.environ", {"AUTOBAHN_ARCH_TARGET": "native"}): + self.assertIn("-march=native", get_compile_args()) + + @unittest.skipIf(sys.platform == "win32", "MSVC path uses /arch, not -march") + def test_unknown_target_emits_no_march(self): + # An unrecognized cross-compile target must yield no -march flag at all. + with mock.patch.dict("os.environ", {}, clear=False): + os.environ.pop("AUTOBAHN_ARCH_TARGET", None) + with mock.patch.object( + _compile_args, "_get_target_machine", return_value="riscv64" + ): + args = get_compile_args() + self.assertFalse(any(a.startswith("-march") for a in args)) + + @unittest.skipIf(sys.platform != "win32", "MSVC-specific flags") + def test_windows_uses_msvc_flags(self): + self.assertEqual(get_compile_args(), ["/O2", "/W3"]) + + +if __name__ == "__main__": + unittest.main()