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
8 changes: 8 additions & 0 deletions .audit/oberstet_fix_1834.md
Original file line number Diff line number Diff line change
@@ -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): #1834
Branch: oberstet:fix_1834
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
116 changes: 77 additions & 39 deletions src/autobahn/nvx/_compile_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------
Expand All @@ -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")
Expand All @@ -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']

Expand Down Expand Up @@ -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
--------
Expand All @@ -124,13 +135,21 @@

import os
import sys
import sysconfig
import platform


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
Expand Down Expand Up @@ -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 = [
Expand All @@ -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):
Expand Down
114 changes: 114 additions & 0 deletions src/autobahn/nvx/test/test_compile_args.py
Original file line number Diff line number Diff line change
@@ -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()
Loading