From dfde337e91d5a9323c508e8cfbd56a28bfa5c11b Mon Sep 17 00:00:00 2001 From: Johnny <22727137+johnnynunez@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:28:38 +0200 Subject: [PATCH 1/3] ci: build and test native Windows ARM64 wheels --- .github/workflows/windows-arm64.yml | 63 ++++++++++++++++++++++++ extensions.py | 5 +- test_scripts/native_wheel_smoke.py | 75 +++++++++++++++++++++++++++++ test_scripts/test_build_commands.py | 46 ++++++++++++++++++ 4 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/windows-arm64.yml create mode 100644 test_scripts/native_wheel_smoke.py create mode 100644 test_scripts/test_build_commands.py diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml new file mode 100644 index 0000000..6a93bcf --- /dev/null +++ b/.github/workflows/windows-arm64.yml @@ -0,0 +1,63 @@ +name: Windows ARM64 wheels + +on: + pull_request: + branches: [master] + push: + branches: [ci/windows-arm64-wheels] + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: windows-11-arm + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + python-version: ['3.12', '3.13'] + env: + CMAKE_GENERATOR: Ninja + CMAKE_BUILD_PARALLEL_LEVEL: '2' + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: arm64 + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: arm64 + - name: Install build tools + run: python -m pip install 'cmake<4' ninja swig 'numpy>=2,<3' setuptools wheel + - name: Check build command regression + run: python -m unittest discover -s test_scripts -p test_build_commands.py -v + - name: Build wheel + run: python setup.py bdist_wheel -d dist + - name: Install and test the wheel in a clean environment + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + python -m venv wheel-test + if ($LASTEXITCODE -ne 0) { throw 'venv creation failed' } + $python = Join-Path $PWD 'wheel-test/Scripts/python.exe' + $wheels = @(Get-ChildItem 'dist/nlopt-*-win_arm64.whl') + if ($wheels.Count -ne 1) { throw 'Expected exactly one ARM64 wheel' } + & $python -m pip install $wheels[0].FullName + if ($LASTEXITCODE -ne 0) { throw 'wheel installation failed' } + & $python extern/nlopt/test/t_python.py + if ($LASTEXITCODE -ne 0) { throw 'upstream Python tests failed' } + & $python test_scripts/native_wheel_smoke.py --require-arm64 + if ($LASTEXITCODE -ne 0) { throw 'native numerical/PE checks failed' } + # Kept separate from the existing PyPI deployment until maintainers enable it. + - uses: actions/upload-artifact@v4 + with: + name: windows-arm64-${{ matrix.python-version }} + path: dist/*.whl + if-no-files-found: error diff --git a/extensions.py b/extensions.py index 0c599c5..4a42845 100644 --- a/extensions.py +++ b/extensions.py @@ -49,6 +49,7 @@ def build_extension(self, ext: Extension): cmd = [ "cmake", "-LAH", + f"-DCMAKE_BUILD_TYPE={self.config}", f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={_ed}", f"-DPython_EXECUTABLE={sys.executable}", "-DNLOPT_GUILE=OFF", @@ -79,8 +80,8 @@ def build_extension(self, ext: Extension): ".", "--config", self.config, - "--", - "-m" if platform.system() == "Windows" else "-j2", + "--parallel", + os.environ.get("CMAKE_BUILD_PARALLEL_LEVEL", "2"), ], cwd=build_dir, ) diff --git a/test_scripts/native_wheel_smoke.py b/test_scripts/native_wheel_smoke.py new file mode 100644 index 0000000..be48a3c --- /dev/null +++ b/test_scripts/native_wheel_smoke.py @@ -0,0 +1,75 @@ +"""Installed-wheel numerical and native Windows ARM64 regression checks.""" +import argparse +import json +import struct +import sys +import sysconfig +from pathlib import Path + +import nlopt +import nlopt._nlopt +import numpy as np + + +def pe_machine(path): + data = Path(path).read_bytes() + assert data[:2] == b"MZ", path + offset = struct.unpack_from(" 0 + assert calls[0] > 1 + + # A derivative-free bound-constrained problem exercises another algorithm. + bounded = nlopt.opt(nlopt.LN_COBYLA, 3) + bounded.set_lower_bounds([0.0, 0.0, 0.0]) + bounded.set_min_objective(objective) + bounded.set_xtol_rel(1e-9) + bounded.set_maxeval(1000) + bounded_result = bounded.optimize([4.0, 3.0, 2.0]) + np.testing.assert_allclose(bounded_result, [1.25, 0.0, 0.75], atol=1e-5, rtol=0) + assert abs(bounded.last_optimum_value() - 6.25) < 1e-8 + assert bounded.last_optimize_result() > 0 + + binaries = {} + if args.require_arm64: + assert sysconfig.get_platform() == "win-arm64", sysconfig.get_platform() + paths = [Path(sys.executable), Path(nlopt._nlopt.__file__)] + paths += list(Path(np.__file__).parent.rglob("*.pyd")) + assert len(paths) > 2, "NumPy extensions not found" + for path in paths: + machine = pe_machine(path) + assert machine == 0xAA64, (path, hex(machine)) + binaries[path.name] = hex(machine) + print(json.dumps({"python": sys.version, "platform": sysconfig.get_platform(), + "nlopt": nlopt.__version__, "numpy": np.__version__, + "quadratic": result.tolist(), "bounded": bounded_result.tolist(), + "objective_calls": calls[0], "pe_machine": binaries})) + + +if __name__ == "__main__": + main() diff --git a/test_scripts/test_build_commands.py b/test_scripts/test_build_commands.py new file mode 100644 index 0000000..7f8792e --- /dev/null +++ b/test_scripts/test_build_commands.py @@ -0,0 +1,46 @@ +"""Check CMake command construction without invoking the native toolchain.""" +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from setuptools import Distribution + +from extensions import NLOptBuild, NLOptBuildExtension + + +class BuildCommandsTest(unittest.TestCase): + def test_generator_neutral_parallelism_and_build_type(self): + for system in ('Windows', 'Linux'): + for debug in (False, True): + with self.subTest(system=system, debug=debug), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + command = NLOptBuild(Distribution({'version': '2.11.0'})) + command.build_temp = str(root / 'temp') + command.build_lib = str(root / 'lib') + command.debug = debug + extension = NLOptBuildExtension('nlopt._nlopt', '2.11.0') + extension_path = root / 'lib' / 'nlopt' / '_nlopt.pyd' + extension_path.parent.mkdir(parents=True) + calls = [] + + def execute(cmd, cwd, calls=calls, **kwargs): + calls.append(cmd) + if '--build' in cmd: + wrapper = cwd / 'extern/nlopt/src/swig/python/nlopt.py' + wrapper.parent.mkdir(parents=True) + wrapper.write_text('# generated test fixture\n') + + with patch('extensions.platform.system', return_value=system), \ + patch.object(command, 'get_ext_fullpath', return_value=str(extension_path)), \ + patch('extensions.execute_command', side_effect=execute), \ + patch.dict(os.environ, {'CMAKE_BUILD_PARALLEL_LEVEL': '2'}): + command.build_extension(extension) + config = 'Debug' if debug else 'Release' + self.assertIn(f'-DCMAKE_BUILD_TYPE={config}', calls[0]) + self.assertEqual(calls[1], ['cmake', '--build', '.', '--config', config, '--parallel', '2']) + + +if __name__ == '__main__': + unittest.main() From 4c64e5fc0378f881c2a299996b7ad7b3ebf969bb Mon Sep 17 00:00:00 2001 From: Johnny <22727137+johnnynunez@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:31:49 +0200 Subject: [PATCH 2/3] fix: disable unrelated Java bindings in Python wheel builds --- extensions.py | 1 + test_scripts/test_build_commands.py | 1 + 2 files changed, 2 insertions(+) diff --git a/extensions.py b/extensions.py index 4a42845..9f6c0ae 100644 --- a/extensions.py +++ b/extensions.py @@ -53,6 +53,7 @@ def build_extension(self, ext: Extension): f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={_ed}", f"-DPython_EXECUTABLE={sys.executable}", "-DNLOPT_GUILE=OFF", + "-DNLOPT_JAVA=OFF", "-DNLOPT_MATLAB=OFF", "-DNLOPT_OCTAVE=OFF", ext.source_dir.as_posix(), diff --git a/test_scripts/test_build_commands.py b/test_scripts/test_build_commands.py index 7f8792e..b31cf00 100644 --- a/test_scripts/test_build_commands.py +++ b/test_scripts/test_build_commands.py @@ -38,6 +38,7 @@ def execute(cmd, cwd, calls=calls, **kwargs): patch.dict(os.environ, {'CMAKE_BUILD_PARALLEL_LEVEL': '2'}): command.build_extension(extension) config = 'Debug' if debug else 'Release' + self.assertIn('-DNLOPT_JAVA=OFF', calls[0]) self.assertIn(f'-DCMAKE_BUILD_TYPE={config}', calls[0]) self.assertEqual(calls[1], ['cmake', '--build', '.', '--config', config, '--parallel', '2']) From 364b56c8e6cb27712c5d13909e8f84e9f95f319c Mon Sep 17 00:00:00 2001 From: Johnny <22727137+johnnynunez@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:31:23 +0200 Subject: [PATCH 3/3] ci: build and test Windows ARM64 wheels for CPython 3.11-3.14 Match the versions built in build.yml. CPython 3.10 is left out because NumPy, which is needed to build and test the wheel, has no Windows ARM64 wheels for it. Drop the push trigger for the PR branch. --- .github/workflows/windows-arm64.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml index 6a93bcf..389bf73 100644 --- a/.github/workflows/windows-arm64.yml +++ b/.github/workflows/windows-arm64.yml @@ -3,8 +3,6 @@ name: Windows ARM64 wheels on: pull_request: branches: [master] - push: - branches: [ci/windows-arm64-wheels] workflow_dispatch: release: types: [published] @@ -19,7 +17,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.12', '3.13'] + # Same versions as build.yml, except 3.10: NumPy (needed to build and + # test) publishes no Windows ARM64 wheels for CPython 3.10. + python-version: ['3.11', '3.12', '3.13', '3.14'] env: CMAKE_GENERATOR: Ninja CMAKE_BUILD_PARALLEL_LEVEL: '2'