diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77e98ae8e..df35b5191 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -308,6 +308,101 @@ jobs: python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python38.outputs.python-path }}" + windows-arm64: + runs-on: windows-11-arm + strategy: + fail-fast: false + matrix: + include: + - build-python: '3.11' + test-python: '3.11' + expected-tag: 'cp311-abi3-win_arm64' + - build-python: '3.11' + test-python: '3.12' + expected-tag: 'cp311-abi3-win_arm64' + - build-python: '3.13' + test-python: '3.13' + expected-tag: 'cp313-abi3-win_arm64' + - build-python: '3.13' + test-python: '3.14' + expected-tag: 'cp313-abi3-win_arm64' + - build-python: '3.13t' + test-python: '3.13t' + expected-tag: 'cp313-cp313t-win_arm64' + - build-python: '3.14t' + test-python: '3.14t' + expected-tag: 'cp314-cp314t-win_arm64' + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.build-python }} + architecture: arm64 + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: arm64 + - name: Build Windows ARM64 wheel + shell: pwsh + run: | + python -m pip install --upgrade build + python -m build --wheel + $wheel = @(Get-Item "dist\awscrt-*-${{ matrix.expected-tag }}.whl") + $allWheels = @(Get-Item dist\*.whl) + if ($wheel.Count -ne 1 -or $allWheels.Count -ne 1) { + throw 'Expected exactly one wheel with tag ${{ matrix.expected-tag }}' + } + python -c "import sys, zipfile; p=sys.argv[1]; expected='Tag: ' + sys.argv[2]; z=zipfile.ZipFile(p); wheel=z.read(next(n for n in z.namelist() if n.endswith('.dist-info/WHEEL'))).decode().splitlines(); assert expected in wheel, (expected, wheel)" $wheel[0].FullName '${{ matrix.expected-tag }}' + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.test-python }} + architecture: arm64 + - name: Test installed wheel + shell: pwsh + env: + AWS_TEST_LOCALHOST: '1' + run: | + python -m venv ..\awscrt-test + $wheel = @(Get-Item "dist\awscrt-*-${{ matrix.expected-tag }}.whl") + if ($wheel.Count -ne 1) { + throw 'Expected exactly one wheel with tag ${{ matrix.expected-tag }}' + } + ..\awscrt-test\Scripts\python.exe -m pip install $wheel[0].FullName + ..\awscrt-test\Scripts\python.exe -m pip install 'websockets>=13.1' 'h2==4.1.0' + Copy-Item test "$env:RUNNER_TEMP\test" -Recurse + $httpTests = "$env:RUNNER_TEMP\crt\aws-c-http\tests" + New-Item $httpTests -ItemType Directory -Force | Out-Null + Copy-Item crt\aws-c-http\tests\mock_server $httpTests -Recurse + Copy-Item crt\aws-c-http\tests\resources $httpTests -Recurse + Push-Location $env:RUNNER_TEMP + try { + # These tests depend on httpbin.org returning 404, but the service currently returns 503. + $testScript = @( + 'import unittest' + "excluded = {'test_aiohttp_client.AIOHttp2RemoteEndStreamTest.test_h2_remote_end_stream_ordering', 'test_http_client.Http2RemoteEndStreamTest.test_h2_remote_end_stream_ordering'}" + 'excluded_found = set()' + "discovered = unittest.defaultTestLoader.discover('test')" + 'filtered = unittest.TestSuite()' + 'def add_tests(suite):' + ' for test in suite:' + ' if isinstance(test, unittest.TestSuite):' + ' add_tests(test)' + ' elif test.id() in excluded:' + ' excluded_found.add(test.id())' + ' else:' + ' filtered.addTest(test)' + 'add_tests(discovered)' + "if excluded_found != excluded: raise RuntimeError(f'Expected exclusions {excluded}, found {excluded_found}')" + "if filtered.countTestCases() == 0: raise RuntimeError('No tests discovered')" + 'result = unittest.TextTestRunner(verbosity=2, failfast=True).run(filtered)' + 'raise SystemExit(0 if result.wasSuccessful() else 1)' + ) -join "`n" + & "$env:GITHUB_WORKSPACE\..\awscrt-test\Scripts\python.exe" -c $testScript + } finally { + Pop-Location + } + macos: runs-on: macos-14 # latest permissions: diff --git a/continuous-delivery/build-wheels-winarm64.bat b/continuous-delivery/build-wheels-winarm64.bat new file mode 100644 index 000000000..cff24192d --- /dev/null +++ b/continuous-delivery/build-wheels-winarm64.bat @@ -0,0 +1,37 @@ +@echo off + +if not defined PYTHON311_ARM64 goto missing_python +if not defined PYTHON313_ARM64 goto missing_python +if not defined PYTHON313T_ARM64 goto missing_python +if not defined PYTHON314T_ARM64 goto missing_python + +"%PYTHON311_ARM64%" -c "import sys, sysconfig; raise SystemExit(0 if sys.version_info[:2] == (3, 11) and sysconfig.get_platform() == 'win-arm64' and not sysconfig.get_config_var('Py_GIL_DISABLED') else 1)" || goto wrong_python +"%PYTHON313_ARM64%" -c "import sys, sysconfig; raise SystemExit(0 if sys.version_info[:2] == (3, 13) and sysconfig.get_platform() == 'win-arm64' and not sysconfig.get_config_var('Py_GIL_DISABLED') else 1)" || goto wrong_python +"%PYTHON313T_ARM64%" -c "import sys, sysconfig; raise SystemExit(0 if sys.version_info[:2] == (3, 13) and sysconfig.get_platform() == 'win-arm64' and sysconfig.get_config_var('Py_GIL_DISABLED') else 1)" || goto wrong_python +"%PYTHON314T_ARM64%" -c "import sys, sysconfig; raise SystemExit(0 if sys.version_info[:2] == (3, 14) and sysconfig.get_platform() == 'win-arm64' and sysconfig.get_config_var('Py_GIL_DISABLED') else 1)" || goto wrong_python + +"%PYTHON313_ARM64%" continuous-delivery\update-version.py || goto error + +:: The 3.11 stable ABI wheel also supports Python 3.12. +"%PYTHON311_ARM64%" -m build || goto error + +:: Use the 3.13 stable ABI from 3.13 onwards because of deprecated functions. +"%PYTHON313_ARM64%" -m build || goto error + +:: Free-threaded builds do not support the Limited C API or stable ABI. +"%PYTHON313T_ARM64%" -m build || goto error +"%PYTHON314T_ARM64%" -m build || goto error + +goto :EOF + +:missing_python +echo PYTHON311_ARM64, PYTHON313_ARM64, PYTHON313T_ARM64, and PYTHON314T_ARM64 must point to native ARM64 Python executables. +exit /b 1 + +:wrong_python +echo Configured Python executables must have the expected version, win-arm64 platform, and free-threaded status. +exit /b 1 + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% \ No newline at end of file diff --git a/setup.py b/setup.py index bcf0b0456..7a1e28809 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,10 @@ def is_32bit(): return is_64bit() == False +def is_windows_arm64(): + return sys.platform == 'win32' and sysconfig.get_platform() == 'win-arm64' + + # TODO: Fix this. Since adding pyproject.toml, it always returns False def is_development_mode(): """Return whether we're building in Development Mode (a.k.a. “Editable Installs”). @@ -196,6 +200,20 @@ def determine_generator_args(cmake_version=None, windows_sdk_version=None): print('Using Visual Studio', vs_version, vs_year) + if is_windows_arm64(): + if not shutil.which('clang-cl'): + raise RuntimeError('clang-cl is required to build for Windows ARM64') + print('Using Ninja and clang-cl for Windows ARM64') + return [ + '-G', 'Ninja', + f'-DCMAKE_SYSTEM_VERSION={windows_sdk_version}', + '-DCMAKE_C_COMPILER=clang-cl', + '-DCMAKE_CXX_COMPILER=clang-cl', + '-DCMAKE_ASM_COMPILER=clang-cl', + # The optimized ARM checksum sources do not compile with clang-cl. + '-DUSE_CPU_EXTENSIONS=OFF', + ] + if vs_year <= 2017: # For VS2017 and earlier, architecture goes at end of generator string if is_64bit(): @@ -325,6 +343,16 @@ def __init__(self, name, extra_cmake_args=[], libname=None): class awscrt_build_ext(setuptools.command.build_ext.build_ext): + def build_extensions(self): + if is_windows_arm64(): + if not self.compiler.initialized: + self.compiler.initialize() + clang_cl = shutil.which('clang-cl') + if not clang_cl: + raise RuntimeError('clang-cl is required to build for Windows ARM64') + self.compiler.cc = clang_cl + super().build_extensions() + def _build_dependencies_impl(self, build_dir, install_path, osx_arch=None): cmake = get_cmake_path()