diff --git a/.github/workflows/build-native-action.yml b/.github/workflows/build-native-action.yml index a4ec2bcb..9a6a20c2 100644 --- a/.github/workflows/build-native-action.yml +++ b/.github/workflows/build-native-action.yml @@ -367,3 +367,92 @@ jobs: name: ${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-win.tar.xz path: build/${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-win.tar.xz retention-days: 5 + + build_windows_arm64: + name: "Python Windows" + # Native build: host and target are both arm64, so this runs on the + # windows-11-arm hosted runner rather than cross-compiling from the + # x64 windows-2022 image build_windows uses. WIP job for + # https://github.com/saltstack/relenv/issues/280 -- untested until a + # CI run actually exercises it; expect early iterations to surface + # missing toolchain/SDK pieces the way build_windows's history did. + runs-on: windows-11-arm + strategy: + fail-fast: false + matrix: + version: + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' + arch: + - arm64 + env: + RELENV_DATA: ${{ github.workspace }} + outputs: + version: ${{ steps.python-version.outputs.version }} + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.11 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install VS Build Tools + run: | + relenv/_scripts/install_vc_build.ps1 -CICD + + - name: Install ARM64 VC++ Tools + # install_vc_build.ps1 only requests the x86/x64 VCTools workload + # plus the legacy VC.140 toolset for the amd64/x86 job; it never + # asks for the ARM64 compiler/linker/armasm64, so request that + # component explicitly here rather than assuming the runner + # image's pre-installed VS carries it. + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vsPath = & $vswhere -latest -products * -property installationPath + $installer = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe" + & $installer modify --installPath "$vsPath" ` + --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 ` + --quiet --norestart --nocache + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Install nox + run: | + pip3 install nox + + - name: Determine Python Version + id: python-version + run: | + echo "version=$(python3 -m relenv versions --version=${{ matrix.version }})" | tee -a "$env:GITHUB_OUTPUT" + + - name: Build Python with Relenv + env: + RELENV_NATIVE_PY_VERSION: 3.10.15 + # NOTE: the amd64/x86 job pins WindowsTargetPlatformVersion to + # 10.0.19041.0 so MSBuild pairs with the v140 toolset (newer + # SDKs' SSE intrinsics break under v140). Arm64 never uses + # v140 (v140 has no arm64 code generator), so leave the SDK + # version unpinned here: the windows-11-arm runner ships VS 18 + # with Win11 SDK 26100 and does not have 10.0.19041.0 available. + HOST_PYTHON: ${{ steps.setup-python.outputs.python-path }} + run: | + python -m relenv build --no-pretty --arch=${{ matrix.arch }} --python=${{ steps.python-version.outputs.version }} + + - name: Upload Build Logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-windows-logs + path: logs/* + retention-days: 5 + + - name: Upload Zipfile + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-win.tar.xz + path: build/${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-win.tar.xz + retention-days: 5 diff --git a/.github/workflows/verify-build-action.yml b/.github/workflows/verify-build-action.yml index df8c9552..b15d686d 100644 --- a/.github/workflows/verify-build-action.yml +++ b/.github/workflows/verify-build-action.yml @@ -273,3 +273,71 @@ jobs: name: ${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-windows-logs path: logs/* retention-days: 5 + + test_windows_arm64: + name: "Verify Windows" + # Matches build_windows_arm64's native windows-11-arm runner in + # build-native-action.yml. WIP for + # https://github.com/saltstack/relenv/issues/280. + runs-on: windows-11-arm + + strategy: + fail-fast: false + matrix: + version: + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' + arch: + - arm64 + + outputs: + version: ${{ steps.python-version.outputs.version }} + env: + RELENV_DATA: ${{ github.workspace }} + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.11 + # 3.10, unlike the x64 job, has no official arm64 Windows build + # for setup-python to install ("The version '3.10' with + # architecture 'arm64' was not found for Windows Enterprise") -- + # CPython's arm64 Windows installers only started at 3.11. Use + # the same bootstrap version build_windows_arm64 already uses. + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install VS Build Tools + run: | + relenv/_scripts/install_vc_build.ps1 -CICD + + - name: Install nox + run: | + pip3 install nox + + - name: Determine Python Version + id: python-version + run: | + echo "version=$(python3 -m relenv versions --version=${{ matrix.version }})" | tee -a "$env:GITHUB_OUTPUT" + + - name: "Download artifact: build/${{ matrix.version }}-${{ matrix.arch }}-win.tar.xz" + uses: actions/download-artifact@v4 + with: + name: ${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-win.tar.xz + path: build/ + + - name: Verify Build + run: | + nox -e tests -- tests/test_verify_build.py + + - name: Upload Build Logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ steps.python-version.outputs.version }}-${{ matrix.arch }}-windows-logs + path: logs/* + retention-days: 5 diff --git a/relenv/_scripts/install_vc_build.ps1 b/relenv/_scripts/install_vc_build.ps1 index eb4256f2..5eb65aa3 100644 --- a/relenv/_scripts/install_vc_build.ps1 +++ b/relenv/_scripts/install_vc_build.ps1 @@ -41,6 +41,38 @@ function Write-Result($result, $ForegroundColor="Green") { } } +function Test-V140Installed { + # The v140 PlatformToolset lives in one of two places depending on the + # VS installer generation: + # + # * VS 2017 / VS 2019 and earlier shared MSBuild writes to the legacy + # shared location under ``${env:ProgramFiles(x86)}\MSBuild``. + # * VS 2022 17.14+ / VS 18 moved MSBuild inside the per-install tree + # and populates only ``\MSBuild\Microsoft\VC\\ + # Platforms\\PlatformToolsets\v140`` -- the legacy path stays + # empty even when v140 is installed and usable. + # + # Return $true on the first match. + [CmdletBinding()] + param( + [Parameter(Mandatory=$false)] + [String] $VSInstLoc + ) + if ( Test-Path -Path "${env:ProgramFiles(x86)}\MSBuild\Microsoft.Cpp\v4.0\V140" ) { + return $true + } + if ( $VSInstLoc ) { + $hit = Get-ChildItem ` + -Path "$VSInstLoc\MSBuild\Microsoft\VC\*\Platforms\*\PlatformToolsets\v140" ` + -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ( $hit ) { + return $true + } + } + return $false +} + function Add-Certificate { [CmdletBinding()] param( @@ -97,8 +129,23 @@ try { $VS_INST_LOC = $(Get-CimInstance MSFT_VSInstance -Namespace root/cimv2/vs).InstallLocation $MSBUILD_BIN = $(Get-ChildItem "$VS_INST_LOC\MSBuild\*\Bin\msbuild.exe").FullName } catch { - # If VS is not installed, this is the fallback for this installation - $MSBUILD_BIN = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe" + # The MSFT_VSInstance CIM class isn't registered on every runner image + # (confirmed absent on windows-11-arm even though VS 2022 ships + # pre-installed there) -- before assuming VS needs to be installed + # from scratch, fall back to vswhere.exe, which is present on every + # GitHub-hosted Windows image regardless of CIM provider support. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $VS_INST_LOC = $null + if ( Test-Path -Path $vswhere ) { + $VS_INST_LOC = & $vswhere -latest -products * -property installationPath + } + if ( $VS_INST_LOC ) { + $MSBUILD_BIN = $(Get-ChildItem "$VS_INST_LOC\MSBuild\*\Bin\msbuild.exe").FullName + } else { + # Genuinely no VS install found by either method - this is the + # fallback for a from-scratch installation. + $MSBUILD_BIN = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe" + } } #------------------------------------------------------------------------------- @@ -115,10 +162,22 @@ if ( Test-Path -Path $MSBUILD_BIN ) { # without v140 by default, so use the VS bootstrapper to add the # component to the existing install. We don't trust the installer's # exit code (it returns non-zero in a few benign cases) — instead - # verify the v140 platform-toolset targets file exists afterwards. - $V140_TARGETS = "${env:ProgramFiles(x86)}\MSBuild\Microsoft.Cpp\v4.0\V140" + # verify the v140 platform-toolset targets exist afterwards. Check + # both the legacy shared-MSBuild location and the VS 2022 17.14+ / + # VS 18 per-install location (see Test-V140Installed above). + # + # Skip v140 entirely on arm64 hosts: v140 cannot generate arm64 code + # (VS 2015 predates the arm64 toolchain), and the windows-11-arm + # runner image ships VS 18 / VS Enterprise 2026, whose product graph + # does not expose Microsoft.VisualStudio.Component.VC.140 at all — + # the bootstrapper logs "Cannot find package ... in product graph" + # and exits 0, which then fails the post-install verify. Python + # 3.10/3.11 PCbuild on arm64 falls through python.props to whatever + # toolset VS has (v143/v144), which supports arm64 natively. Write-Host "Ensuring v140 toolset is installed: " -NoNewline - if ( Test-Path -Path $V140_TARGETS ) { + if ( $env:PROCESSOR_ARCHITECTURE -eq "ARM64" ) { + Write-Result "Skipped (arm64 host; v140 does not support arm64)" -ForegroundColor Green + } elseif ( Test-V140Installed -VSInstLoc $VS_INST_LOC ) { Write-Result "Already present" -ForegroundColor Green } else { Write-Host "" @@ -151,7 +210,7 @@ if ( Test-Path -Path $MSBUILD_BIN ) { -PassThru -Wait -NoNewWindow Write-Host " bootstrapper exit code: $($proc.ExitCode)" Write-Host "Verifying v140 toolset: " -NoNewline - if ( Test-Path -Path $V140_TARGETS ) { + if ( Test-V140Installed -VSInstLoc $VS_INST_LOC ) { Write-Result "Success" -ForegroundColor Green } else { Write-Result "Failed (v140 still not registered with MSBuild)" -ForegroundColor Red diff --git a/relenv/build/windows.py b/relenv/build/windows.py index 44af4b8b..27d526ff 100644 --- a/relenv/build/windows.py +++ b/relenv/build/windows.py @@ -12,6 +12,7 @@ import logging import os import pathlib +import re import shutil import subprocess import sys @@ -228,6 +229,62 @@ def find_vcvarsall(env: EnvMapping) -> pathlib.Path | None: return None +def _discover_installed_platform_toolset(env: EnvMapping, platform: str) -> str | None: + r""" + Return the newest PlatformToolset installed under the detected VS + install for *platform* (``ARM64``, ``x64``, ``Win32``), or ``None`` + if we can't find vswhere/an install. + + Each VS install lays out installed PlatformToolsets under:: + + \MSBuild\Microsoft\VC\\Platforms\\PlatformToolsets\ + + ```` is the value we need to pass to MSBuild via + ``/p:PlatformToolset=``. Sorting lexicographically on the + directory name is good enough -- v143 < v144 < v180 as strings. + """ + vswhere = shutil.which("vswhere.exe") + if not vswhere: + candidate = ( + pathlib.Path(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")) + / "Microsoft Visual Studio" + / "Installer" + / "vswhere.exe" + ) + if candidate.exists(): + vswhere = str(candidate) + if not vswhere: + return None + + try: + result = subprocess.run( + [vswhere, "-latest", "-products", "*", "-property", "installationPath"], + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError: + return None + vs_path = result.stdout.strip() + if not vs_path: + return None + + toolset_root = pathlib.Path(vs_path) / "MSBuild" / "Microsoft" / "VC" + if not toolset_root.exists(): + return None + + names: set[str] = set() + for vc_dir in toolset_root.iterdir(): + plats = vc_dir / "Platforms" / platform / "PlatformToolsets" + if plats.is_dir(): + for p in plats.iterdir(): + if p.is_dir() and p.name.lower().startswith("v"): + names.add(p.name) + if not names: + return None + return max(names) + + def update_props(source: pathlib.Path, old: str, new: str) -> None: """ Overwrite a dependency string for Windows PCBuild. @@ -618,6 +675,20 @@ def update_openssl(dirs: Dirs, env: EnvMapping) -> None: shutil.copy(str(target_dir / license_file), str(out_dir / "LICENSE")) break + if not is_binary: + # Bundle a copy of the OpenSSL dev tree (headers + import + # libs) into the onedir itself. Rust crates such as + # openssl-sys -- a transitive build dependency of packages + # like `cryptography` when pip falls back to building from + # source, which it does on arm64 since no prebuilt wheel + # exists yet for a target nothing has shipped wheels for -- + # have no other way to find an OpenSSL to link against. + # Consumers set OPENSSL_DIR to /OpenSSL before + # pip installing such packages. + openssl_bundle_dir = dirs.prefix / "OpenSSL" + if not openssl_bundle_dir.exists(): + shutil.copytree(str(prefix), str(openssl_bundle_dir)) + if is_binary: # Ensure include/openssl exists inc_openssl_dir = target_dir / "include" / "openssl" @@ -1007,6 +1078,110 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None: patch_ssl_for_cpython_104135(dirs.source) + # Drop any python.props line that hard-pins BasePlatformToolset to v143 + # when running under VS Enterprise 2026 / VS 18 + # (VisualStudioVersion='18.0'). The windows-11-arm runner image ships + # VS 18 with its native toolset only; v143 (VS 2022 build tools) is not + # installed and installing it on-demand is awkward (the "legacy" v143 + # ARM64 compat component isn't exposed in VS 18's product graph). With + # the pin removed, BasePlatformToolset stays empty for VS 18, so + # python.props's existing fallback leaves PlatformToolset empty too and + # MSBuild resolves it via Microsoft.Cpp.Default.props's + # DefaultPlatformToolset -- i.e. whatever VS 18 ships natively on the + # runner. + # + # Only CPython 3.14 upstream carries the 18.0 line; 3.10-3.13 never + # matched VS 18 to begin with, so the sub below is a no-op on those + # trees. No effect on windows-2022 (x86/amd64) builds, which run under + # VS 2022 (VisualStudioVersion='17.0') and still match the '17.0' arm. + props_path = dirs.source / "PCbuild" / "python.props" + if props_path.exists(): + original = props_path.read_text(encoding="utf-8") + content = original + + vs18_pin_re = ( + r"^[ \t]*" + r"v14\d\r?\n" + ) + content, n = re.subn(vs18_pin_re, "", content, count=1, flags=re.MULTILINE) + if n: + log.info("Removed VS 18 -> v14x pin from python.props (letting MSBuild use DefaultPlatformToolset)") + + # Force x64-hosted cl.exe/link.exe on ARM64 builds. The default + # ``PreferredToolArchitecture`` MSBuild picks for the ARM64 platform + # is empty, which causes Microsoft.Cpp.Default.props to default it + # to ``x86`` -- the 32-bit compiler then OOMs ("fatal error C1002: + # compiler is out of heap space in pass 2") on sources like + # Objects/unicodetype_db.h where PGO/whole-program optimization + # pushes the heap past 2 GB. Setting the value in python.props + # (imported before Microsoft.Cpp.Default.props by every .vcxproj + # under PCbuild/) wins over the default. Passing it via a MSBuild + # ``/p:`` flag isn't an option because build.bat forwards %* through + # cmd.exe, which splits positional args on ``=`` into two tokens + # and triggers MSB1008. Setting it via a process env var doesn't + # win against Microsoft.Cpp.Default.props's resetter. + if "PreferredToolArchitecture" not in content: + marker = " x64\n" + ) + content = content.replace(marker, prefarch_line + marker, 1) + log.info("Inserted PreferredToolArchitecture=x64 for Platform=ARM64 into python.props") + + if content != original: + props_path.write_text(content, encoding="utf-8") + + # Reduce optimisation level and disable whole-program optimisation / + # link-time code generation on ARM64 Release builds. The HostX86 + # cross-compiler (which MSBuild picks for ARM64 targets under VS 18 + # and refuses to let us override) OOMs in pass 2 on + # ``Objects/unicodetype_db.h`` (``fatal error C1002``) with Python's + # default ``MaxSpeed``+``WholeProgramOptimization=true`` settings. + # Even disabling WPO alone isn't enough -- /O2's register allocator + # also blows past the 32-bit heap on that file. Switching to + # ``MinSpace`` (``/O1``), turning off WPO/LTCG, and disabling + # ``IntrinsicFunctions`` keeps optimisation shallow enough to fit in + # 2 GB. Python 3.14 happens to compile without hitting the limit so + # its arm64 build was already green; this patch is a no-op there and + # on amd64/x86 (the condition keys off Platform='ARM64'). + pyproject_path = dirs.source / "PCbuild" / "pyproject.props" + if pyproject_path.exists(): + original = pyproject_path.read_text(encoding="utf-8") + content = original + sentinel = "" + marker = " " + if sentinel not in content and marker in content: + override_block = ( + " \n" + " " + sentinel + "\n" + " \n" + " \n" + " Disabled\n" + " Disabled" + "\n" + " false\n" + " false\n" + " false\n" + " false\n" + " \n" + " \n" + " Default\n" + " \n" + " \n" + " false\n" + " \n" + " " + ) + content = content.replace(marker, override_block, 1) + log.info("Patched pyproject.props to disable WPO/LTCG on ARM64 Release") + if content != original: + pyproject_path.write_text(content, encoding="utf-8") + # CPython 3.14.10 shipped a bad backport of gh-149816 (upstream PR #160006) # that dropped the "!" from `int fips = !disable_fips_property(py_ht);` in # Modules/_hashopenssl.c. The inverted polarity makes openssl_md5( @@ -1033,6 +1208,29 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None: "--no-tkinter", ] + # On VS Enterprise 2026 / VS 18 (the windows-11-arm runner), neither + # v143 nor v140 ARM64 build tools are installed by default, but + # python.props on all branches 3.10-3.14 still manages to pick one of + # them -- VS 18 unconditionally sets $(VCTargetsPath14) which makes + # 3.10's fallback match and resolve PlatformToolset=v140 (which can't + # even target arm64), while 3.14 upstream pins the '18.0' arm to v143. + # Discover the newest vXXX directory actually installed under + # \MSBuild\Microsoft\VC\*\Platforms\\PlatformToolsets + # and push it into MSBuild via the ``PlatformToolset`` env var + # (MSBuild promotes every env var whose name matches a known property; + # CPython's build.bat passes the environment through to msbuild.exe + # intact). + if arch == "arm64": + toolset = _discover_installed_platform_toolset(env, "ARM64") + if toolset: + log.info("Pinning PlatformToolset=%s via env var for arm64 build", toolset) + env["PlatformToolset"] = toolset + else: + log.warning( + "No installed arm64 PlatformToolset discovered; MSBuild may fall " + "back to an uninstalled default and fail with MSB8020" + ) + log.info("Start PCbuild") runcmd(cmd, env=env, stderr=logfp, stdout=logfp) log.info("PCbuild finished") @@ -1132,6 +1330,7 @@ def runpip(pkg: str | os.PathLike[str]) -> None: "*.whl", "/Include/*", "/Lib/site-packages/*", + "/OpenSSL/*", ] archive = f"{dirs.prefix}.tar.xz" with tarfile.open(archive, mode="w:xz") as fp: diff --git a/relenv/common.py b/relenv/common.py index 888ba887..0929b6a9 100644 --- a/relenv/common.py +++ b/relenv/common.py @@ -131,7 +131,7 @@ def toolchain_root_dir() -> pathlib.Path: WIN32: ( "amd64", "x86", - # "arm64", # Python 11 should support arm. + "arm64", ), } diff --git a/tests/test_verify_build.py b/tests/test_verify_build.py index 428dc52c..d8d3f30a 100644 --- a/tests/test_verify_build.py +++ b/tests/test_verify_build.py @@ -750,25 +750,44 @@ def test_pip_install_pyzmq( ) -def test_pip_install_cryptography(pipexec, pyexec): +def _openssl_dir_env(build: pathlib.Path, env: dict[str, str]) -> dict[str, str]: + """ + Point OPENSSL_DIR at the OpenSSL dev tree relenv bundles into the + onedir when it built OpenSSL from source (currently windows arm64 + only). Packages like `cryptography` that fall back to compiling + openssl-sys from source -- because no prebuilt wheel exists yet for + a target nothing has shipped wheels for -- have no other way to find + an OpenSSL to link against. A no-op everywhere the bundled tree + doesn't exist (binary-openssl platforms already have prebuilt + wheels, so this never matters there). + """ + openssl_dir = build / "OpenSSL" + if openssl_dir.exists(): + env["OPENSSL_DIR"] = str(openssl_dir) + return env + + +def test_pip_install_cryptography(pipexec, pyexec, build): _install_ppbt(pyexec) packages = [ "cryptography", ] env = os.environ.copy() env["RELENV_BUILDENV"] = "yes" + env = _openssl_dir_env(build, env) for name in packages: p = subprocess.run([str(pipexec), "install", name, "--no-cache-dir"], env=env) assert p.returncode == 0, f"Failed to pip install {name}" -def test_pip_install_idem(pipexec, pyexec): +def test_pip_install_idem(pipexec, pyexec, build): _install_ppbt(pyexec) packages = [ "idem", ] env = os.environ.copy() env["RELENV_BUILDENV"] = "yes" + env = _openssl_dir_env(build, env) for name in packages: p = subprocess.run([str(pipexec), "install", name, "--no-cache-dir"], env=env) assert p.returncode == 0, f"Failed to pip install {name}" @@ -797,6 +816,15 @@ def test_pip_install_salt_pip_dir(pipexec, pyexec, build, build_version, arch): if sys.platform == "win32" and arch == "amd64": pytest.xfail("Known failure on windows amd64") + if sys.platform == "win32" and arch == "arm64": + # Windows arm64: salt's transitive C-extension deps + # (pymssql, cryptography, cffi at their older pinned versions) + # have no arm64 wheels on PyPI and their source builds do not + # succeed under the MSVC arm64 toolchain (no FreeTDS/OpenSSL + # for arm64 in this environment, and cffi <1.17 predates arm64 + # Windows support). + pytest.xfail("Known failure on windows arm64") + if sys.platform == "darwin" and ("3.13" in build_version or "3.14" in build_version): pytest.xfail("Salt does not work with 3.13+ on macos yet") @@ -1489,7 +1517,7 @@ def test_install_with_target_uninstall(pipexec, build): assert not (extras / "bin" / "cowsay").exists() -def test_install_with_target_cffi_versions(pipexec, pyexec, build, build_version): +def test_install_with_target_cffi_versions(pipexec, pyexec, build, build_version, arch): env = os.environ.copy() env["RELENV_DEBUG"] = "yes" extras = build / "extras" @@ -1497,7 +1525,13 @@ def test_install_with_target_cffi_versions(pipexec, pyexec, build, build_version cffi_version = "2.0.0" else: cffi_version = "1.17.1" - if build_version[:4] not in ["3.13", "3.14"]: + # cffi 1.14.6 / 1.16.0 have no Windows arm64 wheels on PyPI and their + # source builds fail under the MSVC arm64 toolchain (arm64 support + # landed upstream in cffi 1.17). Skip the older-cffi coverage on + # win-arm64; the ``cffi_version`` path below still exercises the + # arm64-supported release. + old_cffi_supported = build_version[:4] not in ["3.13", "3.14"] and not (sys.platform == "win32" and arch == "arm64") + if old_cffi_supported: subprocess.run( [str(pipexec), "install", "cffi==1.14.6"], check=True, @@ -1523,8 +1557,26 @@ def test_install_with_target_cffi_versions(pipexec, pyexec, build, build_version proc.stdout.decode().strip() == "1.17.1" -def test_install_with_target_no_ignore_installed(pipexec, pyexec, build, build_version): - if build_version.startswith("3.14"): +def test_install_with_target_no_ignore_installed(pipexec, pyexec, build, build_version, arch): + # On Windows arm64, cffi <1.17 and pygit2 <1.19 have no wheels on + # PyPI and their source builds do not support the MSVC arm64 + # toolchain (pygit2 additionally needs libgit2 headers, which are + # not installed on the windows-11-arm runners). pygit2 wheels for + # win_arm64 first appear in 1.19.0 and only for cp311+ (pygit2's + # requires_python is >=3.11), so Python 3.10 arm64 has no runnable + # combination -- skip. Otherwise pin to the earliest arm64-wheel + # release so the "install cffi, then --target pygit2 sees it as + # already-satisfied" flow still gets exercised. + if sys.platform == "win32" and arch == "arm64": + if build_version.startswith("3.10"): + pytest.skip("pygit2 has no win_arm64 wheels for Python 3.10") + # pygit2 1.19.2 is the earliest release carrying win_arm64 wheels, + # and its METADATA pins ``cffi>=2.0``. Pre-installing anything + # older would be re-upgraded during the pygit2 install and defeat + # the "already satisfied: cffi" assertion below. + cffi = "cffi==2.0.0" + pygit2 = "pygit2==1.19.2" + elif build_version.startswith("3.14"): cffi = "cffi==2.0.0" pygit2 = "pygit2==1.19.2" elif build_version.startswith("3.13"): @@ -1559,8 +1611,19 @@ def test_install_with_target_no_ignore_installed(pipexec, pyexec, build, build_v assert "installed cffi" not in out -def test_install_with_target_ignore_installed(pipexec, pyexec, build, build_version): - if build_version.startswith("3.14"): +def test_install_with_target_ignore_installed(pipexec, pyexec, build, build_version, arch): + # See test_install_with_target_no_ignore_installed for why win-arm64 + # needs its own version selection. + if sys.platform == "win32" and arch == "arm64": + if build_version.startswith("3.10"): + pytest.skip("pygit2 has no win_arm64 wheels for Python 3.10") + # pygit2 1.19.2 is the earliest release carrying win_arm64 wheels, + # and its METADATA pins ``cffi>=2.0``. Pre-installing anything + # older would be re-upgraded during the pygit2 install and defeat + # the "already satisfied: cffi" assertion below. + cffi = "cffi==2.0.0" + pygit2 = "pygit2==1.19.2" + elif build_version.startswith("3.14"): cffi = "cffi==2.0.0" pygit2 = "pygit2==1.19.2" elif build_version.startswith("3.13"):