diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77e98ae8e..a431138eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -308,6 +308,90 @@ 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: + python: ['3.11', '3.13'] + permissions: + id-token: write # This is required for requesting the JWT + steps: + - uses: actions/setup-python@v5 + id: python + with: + python-version: ${{ matrix.python }} + architecture: arm64 + - name: configure AWS credentials (containers) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.CRT_CI_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: Build ${{ env.PACKAGE_NAME }} + consumers + run: | + 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.python.outputs.python-path }}" + + windows-arm64-wheel: + # Verify the wheel we'd ship is tagged for win_arm64, and that an abi3 wheel + # built on one python installs and imports on a newer one. + name: windows-arm64-wheel (${{ matrix.build-python }} → ${{ matrix.test-python }}) + runs-on: windows-11-arm + permissions: + contents: read # only needs to check out the source; no AWS creds, no OIDC + strategy: + fail-fast: false + matrix: + include: + - build-python: '3.11' + test-python: '3.12' + expected-tag: 'cp311-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 + - name: Build wheel + shell: pwsh + run: | + python -m pip install --upgrade build + python -m build --wheel + $wheels = @(Get-Item dist\*.whl) + if ($wheels.Count -ne 1) { throw "Expected exactly one wheel, got $($wheels.Count)" } + $expected = 'Tag: ${{ matrix.expected-tag }}' + python -c "import sys, zipfile; z=zipfile.ZipFile(sys.argv[1]); w=z.read(next(n for n in z.namelist() if n.endswith('.dist-info/WHEEL'))).decode().splitlines(); assert sys.argv[2] in w, (sys.argv[2], w)" $wheels[0].FullName $expected + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.test-python }} + architecture: arm64 + - name: Install wheel and import + shell: pwsh + run: | + python -m venv ${{ runner.temp }}\venv + $py = "${{ runner.temp }}\venv\Scripts\python.exe" + & $py -m pip install @(Get-Item dist\*.whl)[0].FullName + # Import from outside the checkout, else "python -c" resolves awscrt's + # python files from the source tree instead of the installed wheel. + Push-Location ${{ runner.temp }} + try { + & $py -c "import awscrt, awscrt.io, awscrt.http, awscrt.s3, awscrt.aio, awscrt.eventstream; print(awscrt.__version__, awscrt.__file__)" + } finally { + Pop-Location + } + + macos: runs-on: macos-14 # latest permissions: diff --git a/setup.py b/setup.py index bcf0b0456..1e7162ac1 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,23 @@ # Regex to match a Windows SDK version directory name in the format of major.minor.build.revision SDK_VERSION_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$') +# On Windows ARM64 we build with clang-cl, which accepts MSVC-only flags and then +# ignores them, warning once per translation unit. We get /MP from the CRT's CMake +# and /GL from distutils. Those are two independent compiler invocations that share +# no configuration, so these have to be passed to each of them separately. +# +# Keep this a list, one flag per entry: extra_compile_args entries each become a +# single argv element, so "-Wa -Wb" in one entry reaches clang-cl as one unknown +# option and is silently ignored. +# +# Note these only reach the CRT build via CMAKE_C_FLAGS, which CMake emits *before* +# a target's own options. So a flag here cannot turn off anything the CRT's /W4 +# re-enables (i.e. anything in -Wall or -Wextra) -- that has to be fixed in +# aws-c-common's AwsCFlags.cmake instead. +CLANG_CL_IGNORED_FLAG_WARNINGS = [ + '-Wno-unused-command-line-argument', +] + def parse_version(version_string): return tuple(int(x) for x in version_string.split(".")) @@ -48,6 +65,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 +217,28 @@ 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', + # Silences /MP, which the CRT's CMake adds. This lands ahead of the + # /W4 that aws_set_common_properties() adds, which is fine: /W4 maps + # to -Wall -Wextra, and neither group includes this warning, so they + # do not turn it back on. + # Appends to, rather than replaces, what CMake would have taken from + # the CFLAGS env var -- an explicit -D on the command line wins over + # that, and the extension half already honors CFLAGS (see awscrt_ext). + '-DCMAKE_C_FLAGS=' + ' '.join([os.environ.get('CFLAGS', '')] + CLANG_CL_IGNORED_FLAG_WARNINGS).strip(), + ] + if vs_year <= 2017: # For VS2017 and earlier, architecture goes at end of generator string if is_64bit(): @@ -325,6 +368,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() @@ -540,6 +593,12 @@ def awscrt_ext(): extra_compile_args += ['/Z7'] extra_link_args += ['/DEBUG'] + if is_windows_arm64(): + # Silences /GL, which distutils hardcodes into its MSVC defaults + # ("/O2 /W3 /GL /DNDEBUG /MD"). This is a separate compiler invocation + # from the CMake one above, so it needs the flag of its own. + extra_compile_args += CLANG_CL_IGNORED_FLAG_WARNINGS + elif sys.platform == 'darwin': extra_link_args += ['-framework', 'Security']