From 464f2b91f8e235d2cac78315ecaff74bfe3c6281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 14:52:29 +0200 Subject: [PATCH 01/11] {Packaging} Prepare dependencies for Windows ARM64 Model the dependency versions needed for native Windows ARM64 packaging across the generated platform locks. Keeping the three lock files aligned avoids platform-specific resolution drift while the packaging work is developed. --- src/azure-cli/requirements.py3.Darwin.txt | 8 ++++---- src/azure-cli/requirements.py3.Linux.txt | 8 ++++---- src/azure-cli/requirements.py3.windows.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/azure-cli/requirements.py3.Darwin.txt b/src/azure-cli/requirements.py3.Darwin.txt index 3ade2330638..2a46a860339 100644 --- a/src/azure-cli/requirements.py3.Darwin.txt +++ b/src/azure-cli/requirements.py3.Darwin.txt @@ -96,12 +96,12 @@ azure-synapse-accesscontrol==0.5.0 azure-synapse-artifacts==0.22.0 azure-synapse-managedprivateendpoints==0.4.0 azure-synapse-spark==0.7.0 -bcrypt==3.2.0 +bcrypt==5.0.0 certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==48.0.1 +cryptography==51.0.0 fabric==3.2.2 humanfriendly==10.0 idna==3.15 @@ -120,12 +120,12 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==6.1.0 +psutil==7.2.2 pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 PyNaCl==1.6.2 -pyOpenSSL==26.2.0 +pyOpenSSL==26.5.0 PySocks==1.7.1 python-dateutil==2.8.0 requests-oauthlib==1.2.0 diff --git a/src/azure-cli/requirements.py3.Linux.txt b/src/azure-cli/requirements.py3.Linux.txt index b6f10bf027f..7012761ba57 100644 --- a/src/azure-cli/requirements.py3.Linux.txt +++ b/src/azure-cli/requirements.py3.Linux.txt @@ -96,12 +96,12 @@ azure-synapse-accesscontrol==0.5.0 azure-synapse-artifacts==0.22.0 azure-synapse-managedprivateendpoints==0.4.0 azure-synapse-spark==0.7.0 -bcrypt==3.2.0 +bcrypt==5.0.0 certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==48.0.1 +cryptography==51.0.0 distro==1.6.0 fabric==3.2.2 humanfriendly==10.0 @@ -121,12 +121,12 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==6.1.0 +psutil==7.2.2 pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 PyNaCl==1.6.2 -pyOpenSSL==26.2.0 +pyOpenSSL==26.5.0 PySocks==1.7.1 python-dateutil==2.8.0 requests-oauthlib==1.2.0 diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index d174e06c629..450db19f4ad 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -96,12 +96,12 @@ azure-synapse-accesscontrol==0.5.0 azure-synapse-artifacts==0.22.0 azure-synapse-managedprivateendpoints==0.4.0 azure-synapse-spark==0.7.0 -bcrypt==3.2.0 +bcrypt==5.0.0 certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==48.0.1 +cryptography==51.0.0 fabric==3.2.2 humanfriendly==10.0 idna==3.15 @@ -120,13 +120,13 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==6.1.0 +psutil==7.2.2 pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 pymsalruntime==0.20.6 PyNaCl==1.6.2 -pyOpenSSL==26.2.0 +pyOpenSSL==26.5.0 PySocks==1.7.1 python-dateutil==2.8.0 pywin32==311 From 3c70789f966103dce9cd3cb1d36a195c745340d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 14:53:21 +0200 Subject: [PATCH 02/11] {Packaging} Build native Windows ARM64 packages Teach the Windows builder to select the ARM64 CPython runtime and emit an ARM64 MSI with architecture-specific product metadata. Update install tests and documentation to use the native Program Files location. WiX 3.14 is required because the previous WiX 3.10 toolset cannot build ARM64 Windows Installer packages. --- build_scripts/windows/Product.wxs | 28 +++++++++++++- build_scripts/windows/README.md | 10 +++-- build_scripts/windows/azure-cli.wixproj | 12 ++++++ build_scripts/windows/scripts/build.cmd | 8 ++-- .../windows/scripts/setup_msi_test.ps1 | 19 +++++++--- .../windows/scripts/test_msi_installation.ps1 | 7 ++-- .../windows/scripts/test_msi_package.py | 3 +- .../windows/tests/test_arm64_packaging.py | 37 +++++++++++++++++++ 8 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 build_scripts/windows/tests/test_arm64_packaging.py diff --git a/build_scripts/windows/Product.wxs b/build_scripts/windows/Product.wxs index c4d7fe2ebeb..1c1ee11f52c 100644 --- a/build_scripts/windows/Product.wxs +++ b/build_scripts/windows/Product.wxs @@ -8,6 +8,8 @@ + + @@ -16,8 +18,20 @@ - + + + + + + + + + + + + + @@ -28,6 +42,7 @@ + @@ -40,7 +55,7 @@ Manufacturer="$(var.ProductAuthor)" UpgradeCode="$(var.UpgradeCode)"> - @@ -59,6 +74,15 @@ + + + + + + + + NOT (WIX_DOWNGRADE_DETECTED OR WIX_X86_DOWNGRADE_DETECTED OR WIX_X64_DOWNGRADE_DETECTED) + NOT (WIX_DOWNGRADE_DETECTED OR WIX_X86_DOWNGRADE_DETECTED) NOT WIX_DOWNGRADE_DETECTED diff --git a/build_scripts/windows/README.md b/build_scripts/windows/README.md index 6ceabff1f9c..e7e92de2fbc 100644 --- a/build_scripts/windows/README.md +++ b/build_scripts/windows/README.md @@ -8,7 +8,8 @@ Prerequisites 1. Turn on the '.NET Framework 3.5' Windows Feature (required for WIX Toolset). -2. Install 'WIX Toolset build tools' if not already installed. (e.g. WiX v3.10.3) +2. Install 'WIX Toolset build tools' if not already installed. WiX v3.14.1 + or later is required for ARM64 MSI packages. http://wixtoolset.org/releases/ 3. Get Git for Windows (it has several tools used for the build). @@ -28,6 +29,9 @@ Building 1. Set the `CLI_VERSION` environment variable. -2. Run `build_scripts\windows\scripts\build.cmd`. +2. Set `ARCH` to `x86`, `x64`, or `arm64`. ARM64 builds must run on a native + Windows ARM64 host because the build executes the embedded Python runtime. -3. The unsigned MSI will be in the `.\out` folder. +3. Run `build_scripts\windows\scripts\build.cmd`. + +4. The unsigned MSI will be in the `.\out` folder. diff --git a/build_scripts/windows/azure-cli.wixproj b/build_scripts/windows/azure-cli.wixproj index e0497237fbd..874b2a379bd 100644 --- a/build_scripts/windows/azure-cli.wixproj +++ b/build_scripts/windows/azure-cli.wixproj @@ -41,6 +41,18 @@ out\obj\$(Configuration)\ AzureCliSource=$(AzureCliSource) + + out\$(Configuration)\ + out\obj\$(Configuration)\ + Debug;AzureCliSource=$(AzureCliSource) + arm64 + + + out\ + out\obj\$(Configuration)\ + AzureCliSource=$(AzureCliSource) + arm64 + azure-cli.wxs diff --git a/build_scripts/windows/scripts/build.cmd b/build_scripts/windows/scripts/build.cmd index dd11ab9972f..19f36dcb265 100644 --- a/build_scripts/windows/scripts/build.cmd +++ b/build_scripts/windows/scripts/build.cmd @@ -13,7 +13,7 @@ if "%CLI_VERSION%"=="" ( echo Please set the CLI_VERSION environment variable, e.g. 2.0.13 goto ERROR ) -@REM ARCH can be x86 or x64 +@REM ARCH can be x86, x64, or arm64 if "%ARCH%"=="" ( set ARCH=x86 ) @@ -26,13 +26,15 @@ if "%ARCH%"=="x86" ( set PYTHON_ARCH=win32 ) else if "%ARCH%"=="x64" ( set PYTHON_ARCH=amd64 +) else if "%ARCH%"=="arm64" ( + set PYTHON_ARCH=arm64 ) else ( - echo Please set ARCH to "x86" or "x64" + echo Please set ARCH to "x86", "x64", or "arm64" goto ERROR ) set PYTHON_VERSION=3.14.8 -set WIX_DOWNLOAD_URL="https://azurecliprod.blob.core.windows.net/msi/wix310-binaries-mirror.zip" +set WIX_DOWNLOAD_URL="https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip" set PYTHON_DOWNLOAD_URL="https://www.python.org/ftp/python/%PYTHON_VERSION%/python-%PYTHON_VERSION%-embed-%PYTHON_ARCH%.zip" REM https://pip.pypa.io/en/stable/installation/#get-pip-py diff --git a/build_scripts/windows/scripts/setup_msi_test.ps1 b/build_scripts/windows/scripts/setup_msi_test.ps1 index 5ad7b16f277..d61f696a104 100644 --- a/build_scripts/windows/scripts/setup_msi_test.ps1 +++ b/build_scripts/windows/scripts/setup_msi_test.ps1 @@ -1,3 +1,8 @@ +param( + [ValidateSet('x86', 'x64', 'arm64')] + [string]$Platform = 'x86' +) + # Prerequisites: # 1. Install the MSI built with current branch # 2. Run bash azure-cli\scripts\ci\build.sh with Git Bash first to generate artifacts under azure-cli\artifacts\build so we can use the testsdk and fulltest wheels. @@ -9,13 +14,17 @@ If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdenti Start-Process powershell -Verb runAs -ArgumentList $arguments } -& 'C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\python.exe' -m pip install pytest -& 'C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\python.exe' -m pip install pytest-xdist +$program_files = if ($Platform -eq 'x86') { ${env:ProgramFiles(x86)} } else { $env:ProgramFiles } +$env:AZURE_CLI_INSTALL_DIR = Join-Path $program_files 'Microsoft SDKs\Azure\CLI2' +$python = Join-Path $env:AZURE_CLI_INSTALL_DIR 'python.exe' + +& $python -m pip install pytest +& $python -m pip install pytest-xdist $testsdk = Get-ChildItem -Path $PSScriptRoot\..\..\..\artifacts\build\azure_cli_testsdk*.whl | Select-Object Name -& 'C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\python.exe' -m pip install $PSScriptRoot\..\..\..\artifacts\build\$($testsdk.Name) +& $python -m pip install $PSScriptRoot\..\..\..\artifacts\build\$($testsdk.Name) $fulltest = Get-ChildItem -Path $PSScriptRoot\..\..\..\artifacts\build\azure_cli_fulltest*.whl | Select-Object Name -& 'C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\python.exe' -m pip install --no-deps $PSScriptRoot\..\..\..\artifacts\build\$($fulltest.Name) +& $python -m pip install --no-deps $PSScriptRoot\..\..\..\artifacts\build\$($fulltest.Name) -& 'C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\python.exe' $PSScriptRoot\test_msi_package.py \ No newline at end of file +& $python $PSScriptRoot\test_msi_package.py \ No newline at end of file diff --git a/build_scripts/windows/scripts/test_msi_installation.ps1 b/build_scripts/windows/scripts/test_msi_installation.ps1 index c65bb99676c..a8f1256ab9b 100644 --- a/build_scripts/windows/scripts/test_msi_installation.ps1 +++ b/build_scripts/windows/scripts/test_msi_installation.ps1 @@ -21,6 +21,7 @@ az --version # - Microsoft Azure CLI --deprecated # - Microsoft Azure CLI (32-bit) # - Microsoft Azure CLI (64-bit) +# - Microsoft Azure CLI (ARM64) $cli_package = Get-Package -Provider Programs -IncludeWindowsInstaller -Name "Microsoft Azure CLI*" $cli_package | Format-List Uninstall-Package -Name $cli_package.Name @@ -52,10 +53,10 @@ Get-Package -Provider Programs -IncludeWindowsInstaller -Name "Microsoft Azure C # We can't restart the current shell in CI to refresh PATH, so use absolute path. # If we can find a way to refresh PATH in the same shell session, we can directly call az. -if ($env:PLATFORM -eq 'x64') { - $az_full_path = "C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd" +if ($env:PLATFORM -eq 'x86') { + $az_full_path = "${env:ProgramFiles(x86)}\Microsoft SDKs\Azure\CLI2\wbin\az.cmd" } else { - $az_full_path = "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin\az.cmd" + $az_full_path = "$env:ProgramFiles\Microsoft SDKs\Azure\CLI2\wbin\az.cmd" } & $az_full_path --version diff --git a/build_scripts/windows/scripts/test_msi_package.py b/build_scripts/windows/scripts/test_msi_package.py index 37bfdc834d7..b3fb1485510 100644 --- a/build_scripts/windows/scripts/test_msi_package.py +++ b/build_scripts/windows/scripts/test_msi_package.py @@ -10,7 +10,8 @@ import sys import subprocess -base_dir = 'C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\CLI2\\Lib\\site-packages\\azure\\cli' +install_dir = os.environ.get('AZURE_CLI_INSTALL_DIR', 'C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\CLI2') +base_dir = '{}\\Lib\\site-packages\\azure\\cli'.format(install_dir) root_dir = '{}\\command_modules'.format(base_dir) mod_list = [mod for mod in sorted(os.listdir(root_dir)) if os.path.isdir(os.path.join(root_dir, mod)) and mod != '__pycache__'] diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py new file mode 100644 index 00000000000..7d9a626346d --- /dev/null +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[3] + + +class WindowsArm64PackagingTest(unittest.TestCase): + + def test_build_uses_arm64_python_and_wix(self): + build = (ROOT / 'build_scripts/windows/scripts/build.cmd').read_text() + self.assertIn('else if "%ARCH%"=="arm64"', build) + self.assertIn('set PYTHON_ARCH=arm64', build) + self.assertIn('wix314-binaries.zip', build) + + def test_wix_project_builds_arm64_packages(self): + project = (ROOT / 'build_scripts/windows/azure-cli.wixproj').read_text(encoding='utf-8-sig') + product = (ROOT / 'build_scripts/windows/Product.wxs').read_text() + self.assertIn("'Release|arm64'", project) + self.assertIn('arm64', project) + self.assertIn('$(var.Platform) = "arm64"', product) + self.assertIn('', product) + self.assertIn(' Date: Thu, 8 Oct 2026 14:53:52 +0200 Subject: [PATCH 03/11] {Packaging} Test Windows ARM64 packages in CI Add proposed native Windows ARM64 matrix entries for dependency validation, MSI and ZIP packaging, and installation tests. Keep existing x86 and x64 jobs on their current pool and preserve architecture-specific artifacts. Extend the packaging regression test so future pipeline edits cannot silently drop ARM64 coverage. --- .azure-pipelines/templates/variables.yml | 1 + azure-pipelines.yml | 37 ++++++++++++++++--- .../windows/tests/test_arm64_packaging.py | 9 +++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/templates/variables.yml b/.azure-pipelines/templates/variables.yml index 35b2c6cc3cb..4b0c1f52e42 100644 --- a/.azure-pipelines/templates/variables.yml +++ b/.azure-pipelines/templates/variables.yml @@ -2,6 +2,7 @@ variables: ubuntu_pool: 'pool-ubuntu-2204' ubuntu_multi_core_pool: 'pool-ubuntu-latest-multi-core' windows_pool: 'pool-windows-2022' + windows_arm64_pool: 'pool-windows-2022-arm64' ubuntu_arm64_pool: 'pool-ubuntu-latest-arm64' macos_pool: 'macOS-14' macos_intel_pool: 'macOS-15' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 280c045d7eb..970cd17a068 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -177,8 +177,14 @@ jobs: - job: VerifyWindowsRequirements displayName: 'Verify src/azure-cli/requirements.*.Windows.txt' condition: succeeded() + strategy: + matrix: + x64: + PoolName: ${{ variables.windows_pool }} + arm64: + PoolName: ${{ variables.windows_arm64_pool }} pool: - name: ${{ variables.windows_pool }} + name: $(PoolName) steps: - task: UsePythonVersion@0 @@ -191,6 +197,9 @@ jobs: filename: ./scripts/ci/dependency_check.bat displayName: 'Verify src/azure-cli/requirements.py3.Windows.txt' + - script: python build_scripts/windows/tests/test_arm64_packaging.py + displayName: 'Verify Windows packaging configuration' + - job: VerifyVersions displayName: Verify Command Module Versions condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/release'), eq(variables['System.PullRequest.TargetBranch'], 'release'))) @@ -216,13 +225,18 @@ jobs: matrix: x86: Platform: x86 + PoolName: ${{ variables.windows_pool }} x64: Platform: x64 + PoolName: ${{ variables.windows_pool }} + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} dependsOn: ExtractMetadata condition: succeeded() pool: - name: ${{ variables.windows_pool }} + name: $(PoolName) steps: - task: DownloadPipelineArtifact@1 displayName: 'Download Build Artifacts' @@ -263,11 +277,15 @@ jobs: matrix: x64: Platform: x64 + PoolName: ${{ variables.windows_pool }} + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} dependsOn: ExtractMetadata condition: succeeded() pool: - name: ${{ variables.windows_pool }} + name: $(PoolName) steps: - task: DownloadPipelineArtifact@1 displayName: 'Download Build Artifacts' @@ -308,13 +326,18 @@ jobs: matrix: x86: Platform: x86 + PoolName: ${{ variables.windows_pool }} x64: Platform: x64 + PoolName: ${{ variables.windows_pool }} + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} dependsOn: BuildWindowsMSI condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) pool: - name: ${{ variables.windows_pool }} + name: $(PoolName) steps: - task: DownloadPipelineArtifact@1 displayName: 'Download Build Artifacts' @@ -340,9 +363,13 @@ jobs: matrix: x64: Platform: x64 + PoolName: ${{ variables.windows_pool }} + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) pool: - name: ${{ variables.windows_pool }} + name: $(PoolName) steps: - task: DownloadPipelineArtifact@1 displayName: 'Download Build Artifacts' diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 7d9a626346d..33f40d6dd35 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -32,6 +32,15 @@ def test_installation_tests_use_native_program_files(self): self.assertIn("$env:PLATFORM -eq 'x86'", script) self.assertIn('$env:ProgramFiles\\Microsoft SDKs\\Azure\\CLI2', script) + def test_pipeline_builds_and_tests_arm64_artifacts(self): + pipeline = (ROOT / 'azure-pipelines.yml').read_text() + variables = (ROOT / '.azure-pipelines/templates/variables.yml').read_text() + self.assertIn("windows_arm64_pool: 'pool-windows-2022-arm64'", variables) + self.assertGreaterEqual(pipeline.count('Platform: arm64'), 4) + self.assertGreaterEqual(pipeline.count('PoolName: ${{ variables.windows_arm64_pool }}'), 5) + self.assertIn('ArtifactName: msi-$(Platform)', pipeline) + self.assertIn('ArtifactName: zip-$(Platform)', pipeline) + if __name__ == '__main__': unittest.main() From 63374767694164913c0a59f0bc616db4041e39da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 14:54:29 +0200 Subject: [PATCH 04/11] {Packaging} Add Windows ARM64 solution configurations Add Debug and Release ARM64 mappings to the Windows solution so local and IDE builds select the ARM64 WiX project configuration consistently with the command-line packaging flow. --- build_scripts/windows/azure-cli.sln | 6 ++++++ build_scripts/windows/tests/test_arm64_packaging.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/build_scripts/windows/azure-cli.sln b/build_scripts/windows/azure-cli.sln index fa338bf49a3..87d4e54e079 100644 --- a/build_scripts/windows/azure-cli.sln +++ b/build_scripts/windows/azure-cli.sln @@ -9,6 +9,8 @@ Global Release|x86 = Release|x86 Debug|x64 = Debug|x64 Release|x64 = Release|x64 + Debug|arm64 = Debug|arm64 + Release|arm64 = Release|arm64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Debug|x86.ActiveCfg = Debug|x86 @@ -19,6 +21,10 @@ Global {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Debug|x64.Build.0 = Debug|x64 {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Release|x64.ActiveCfg = Release|x64 {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Release|x64.Build.0 = Release|x64 + {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Debug|arm64.ActiveCfg = Debug|arm64 + {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Debug|arm64.Build.0 = Debug|arm64 + {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Release|arm64.ActiveCfg = Release|arm64 + {9A0DC66D-3186-4EE4-B471-6C0F1DD6E159}.Release|arm64.Build.0 = Release|arm64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 33f40d6dd35..3ed045710d6 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -20,9 +20,11 @@ def test_build_uses_arm64_python_and_wix(self): def test_wix_project_builds_arm64_packages(self): project = (ROOT / 'build_scripts/windows/azure-cli.wixproj').read_text(encoding='utf-8-sig') + solution = (ROOT / 'build_scripts/windows/azure-cli.sln').read_text(encoding='utf-8-sig') product = (ROOT / 'build_scripts/windows/Product.wxs').read_text() self.assertIn("'Release|arm64'", project) self.assertIn('arm64', project) + self.assertIn('Release|arm64 = Release|arm64', solution) self.assertIn('$(var.Platform) = "arm64"', product) self.assertIn('', product) self.assertIn(' Date: Thu, 8 Oct 2026 14:55:28 +0200 Subject: [PATCH 05/11] {Packaging} Verify WiX 3.14 build tools Pin and verify the SHA-256 digest of the WiX 3.14.1 archive before extraction. The build downloads executable packaging tools, so a corrupted or replaced archive must fail rather than enter the release toolchain. --- build_scripts/windows/scripts/build.cmd | 3 +++ build_scripts/windows/tests/test_arm64_packaging.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/build_scripts/windows/scripts/build.cmd b/build_scripts/windows/scripts/build.cmd index 19f36dcb265..bf2bcb82093 100644 --- a/build_scripts/windows/scripts/build.cmd +++ b/build_scripts/windows/scripts/build.cmd @@ -35,6 +35,7 @@ if "%ARCH%"=="x86" ( set PYTHON_VERSION=3.14.8 set WIX_DOWNLOAD_URL="https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip" +set WIX_DOWNLOAD_SHA256=6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31 set PYTHON_DOWNLOAD_URL="https://www.python.org/ftp/python/%PYTHON_VERSION%/python-%PYTHON_VERSION%-embed-%PYTHON_ARCH%.zip" REM https://pip.pypa.io/en/stable/installation/#get-pip-py @@ -88,6 +89,8 @@ if "%TARGET%" == "msi" ( pushd %WIX_DIR% echo Downloading Wix. curl --output wix-archive.zip %WIX_DOWNLOAD_URL% + powershell.exe -NoProfile -Command "if ((Get-FileHash -Algorithm SHA256 wix-archive.zip).Hash -ne '%WIX_DOWNLOAD_SHA256%') { throw 'WiX archive SHA-256 mismatch' }" + if %errorlevel% neq 0 goto ERROR unzip wix-archive.zip if %errorlevel% neq 0 goto ERROR del wix-archive.zip diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 3ed045710d6..99f98ee0802 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -17,6 +17,8 @@ def test_build_uses_arm64_python_and_wix(self): self.assertIn('else if "%ARCH%"=="arm64"', build) self.assertIn('set PYTHON_ARCH=arm64', build) self.assertIn('wix314-binaries.zip', build) + self.assertIn('WIX_DOWNLOAD_SHA256=6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31', build) + self.assertIn('Get-FileHash -Algorithm SHA256 wix-archive.zip', build) def test_wix_project_builds_arm64_packages(self): project = (ROOT / 'build_scripts/windows/azure-cli.wixproj').read_text(encoding='utf-8-sig') From 11ae7d8e595d1ffb9634e59c8fc8f1facb43a1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 14:56:30 +0200 Subject: [PATCH 06/11] {Packaging} Preserve psutil support for Windows x86 Use psutil 7.2.2 only on Windows ARM64, where it provides a native wheel. Keep Windows x86 on 6.1.0 because newer psutil releases no longer publish a win32 wheel and the existing x86 MSI must remain buildable. --- build_scripts/windows/tests/test_arm64_packaging.py | 5 +++++ src/azure-cli/requirements.py3.windows.txt | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 99f98ee0802..b1694a4e471 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -45,6 +45,11 @@ def test_pipeline_builds_and_tests_arm64_artifacts(self): self.assertIn('ArtifactName: msi-$(Platform)', pipeline) self.assertIn('ArtifactName: zip-$(Platform)', pipeline) + def test_windows_requirements_preserve_x86_psutil(self): + requirements = (ROOT / 'src/azure-cli/requirements.py3.windows.txt').read_text() + self.assertIn('psutil==6.1.0; platform_machine != "ARM64"', requirements) + self.assertIn('psutil==7.2.2; platform_machine == "ARM64"', requirements) + if __name__ == '__main__': unittest.main() diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index 450db19f4ad..c7386d65163 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -120,7 +120,8 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==7.2.2 +psutil==6.1.0; platform_machine != "ARM64" +psutil==7.2.2; platform_machine == "ARM64" pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 From 8a431dd5d13ed2bf33bbf30566147e8e2d3c59d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 14:56:58 +0200 Subject: [PATCH 07/11] {Packaging} Preserve cryptography support for Windows x86 Select the future cryptography and pyOpenSSL pair only on Windows ARM64. Keep the current pair for other Windows architectures because cryptography 50 and later no longer publish win32 wheels. This preserves the existing x86 MSI while preparing ARM64 for the first cryptography release that publishes native wheels. --- build_scripts/windows/tests/test_arm64_packaging.py | 6 +++++- src/azure-cli/requirements.py3.windows.txt | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index b1694a4e471..0e71e18ad80 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -45,10 +45,14 @@ def test_pipeline_builds_and_tests_arm64_artifacts(self): self.assertIn('ArtifactName: msi-$(Platform)', pipeline) self.assertIn('ArtifactName: zip-$(Platform)', pipeline) - def test_windows_requirements_preserve_x86_psutil(self): + def test_windows_requirements_preserve_x86_native_wheels(self): requirements = (ROOT / 'src/azure-cli/requirements.py3.windows.txt').read_text() + self.assertIn('cryptography==48.0.1; platform_machine != "ARM64"', requirements) + self.assertIn('cryptography==51.0.0; platform_machine == "ARM64"', requirements) self.assertIn('psutil==6.1.0; platform_machine != "ARM64"', requirements) self.assertIn('psutil==7.2.2; platform_machine == "ARM64"', requirements) + self.assertIn('pyOpenSSL==26.2.0; platform_machine != "ARM64"', requirements) + self.assertIn('pyOpenSSL==26.5.0; platform_machine == "ARM64"', requirements) if __name__ == '__main__': diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index c7386d65163..f675f3db7d9 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -101,7 +101,8 @@ certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==51.0.0 +cryptography==48.0.1; platform_machine != "ARM64" +cryptography==51.0.0; platform_machine == "ARM64" fabric==3.2.2 humanfriendly==10.0 idna==3.15 @@ -127,7 +128,8 @@ PyGithub==1.55 PyJWT==2.15.0 pymsalruntime==0.20.6 PyNaCl==1.6.2 -pyOpenSSL==26.5.0 +pyOpenSSL==26.2.0; platform_machine != "ARM64" +pyOpenSSL==26.5.0; platform_machine == "ARM64" PySocks==1.7.1 python-dateutil==2.8.0 pywin32==311 From 2fa7e7317a5c2634f54db8ff1c2e0a588a1c8cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 15:06:01 +0200 Subject: [PATCH 08/11] {Packaging} Gate unavailable Windows ARM64 dependencies Restore the released dependency locks and compile-time gate ARM64 pipeline entries behind a disabled parameter. Cryptography 51, its compatible pyOpenSSL release, and the native Windows ARM64 pool are not available yet. The gate keeps normal development and CI environments resolvable while retaining the packaging and test wiring for later activation. --- azure-pipelines.yml | 36 +++++++++++-------- .../windows/tests/test_arm64_packaging.py | 15 ++++---- src/azure-cli/requirements.py3.Darwin.txt | 8 ++--- src/azure-cli/requirements.py3.Linux.txt | 8 ++--- src/azure-cli/requirements.py3.windows.txt | 11 +++--- 5 files changed, 42 insertions(+), 36 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 970cd17a068..b808b863f4b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,6 +29,9 @@ variables: value: '3.14' parameters: +- name: enableWindowsArm64 + type: boolean + default: false - name: architectures type: object default: @@ -181,8 +184,9 @@ jobs: matrix: x64: PoolName: ${{ variables.windows_pool }} - arm64: - PoolName: ${{ variables.windows_arm64_pool }} + ${{ if eq(parameters.enableWindowsArm64, true) }}: + arm64: + PoolName: ${{ variables.windows_arm64_pool }} pool: name: $(PoolName) @@ -229,9 +233,10 @@ jobs: x64: Platform: x64 PoolName: ${{ variables.windows_pool }} - arm64: - Platform: arm64 - PoolName: ${{ variables.windows_arm64_pool }} + ${{ if eq(parameters.enableWindowsArm64, true) }}: + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} dependsOn: ExtractMetadata condition: succeeded() @@ -278,9 +283,10 @@ jobs: x64: Platform: x64 PoolName: ${{ variables.windows_pool }} - arm64: - Platform: arm64 - PoolName: ${{ variables.windows_arm64_pool }} + ${{ if eq(parameters.enableWindowsArm64, true) }}: + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} dependsOn: ExtractMetadata condition: succeeded() @@ -330,9 +336,10 @@ jobs: x64: Platform: x64 PoolName: ${{ variables.windows_pool }} - arm64: - Platform: arm64 - PoolName: ${{ variables.windows_arm64_pool }} + ${{ if eq(parameters.enableWindowsArm64, true) }}: + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} dependsOn: BuildWindowsMSI condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) @@ -364,9 +371,10 @@ jobs: x64: Platform: x64 PoolName: ${{ variables.windows_pool }} - arm64: - Platform: arm64 - PoolName: ${{ variables.windows_arm64_pool }} + ${{ if eq(parameters.enableWindowsArm64, true) }}: + arm64: + Platform: arm64 + PoolName: ${{ variables.windows_arm64_pool }} condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) pool: name: $(PoolName) diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 0e71e18ad80..b599e230c84 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -40,19 +40,20 @@ def test_pipeline_builds_and_tests_arm64_artifacts(self): pipeline = (ROOT / 'azure-pipelines.yml').read_text() variables = (ROOT / '.azure-pipelines/templates/variables.yml').read_text() self.assertIn("windows_arm64_pool: 'pool-windows-2022-arm64'", variables) + self.assertIn('- name: enableWindowsArm64\n type: boolean\n default: false', pipeline) self.assertGreaterEqual(pipeline.count('Platform: arm64'), 4) self.assertGreaterEqual(pipeline.count('PoolName: ${{ variables.windows_arm64_pool }}'), 5) + self.assertGreaterEqual(pipeline.count('${{ if eq(parameters.enableWindowsArm64, true) }}:'), 5) self.assertIn('ArtifactName: msi-$(Platform)', pipeline) self.assertIn('ArtifactName: zip-$(Platform)', pipeline) - def test_windows_requirements_preserve_x86_native_wheels(self): + def test_unreleased_arm64_dependencies_are_not_pinned(self): requirements = (ROOT / 'src/azure-cli/requirements.py3.windows.txt').read_text() - self.assertIn('cryptography==48.0.1; platform_machine != "ARM64"', requirements) - self.assertIn('cryptography==51.0.0; platform_machine == "ARM64"', requirements) - self.assertIn('psutil==6.1.0; platform_machine != "ARM64"', requirements) - self.assertIn('psutil==7.2.2; platform_machine == "ARM64"', requirements) - self.assertIn('pyOpenSSL==26.2.0; platform_machine != "ARM64"', requirements) - self.assertIn('pyOpenSSL==26.5.0; platform_machine == "ARM64"', requirements) + self.assertIn('cryptography==48.0.1', requirements) + self.assertIn('psutil==6.1.0', requirements) + self.assertIn('pyOpenSSL==26.2.0', requirements) + self.assertNotIn('cryptography==51', requirements) + self.assertNotIn('pyOpenSSL==26.5', requirements) if __name__ == '__main__': diff --git a/src/azure-cli/requirements.py3.Darwin.txt b/src/azure-cli/requirements.py3.Darwin.txt index 2a46a860339..3ade2330638 100644 --- a/src/azure-cli/requirements.py3.Darwin.txt +++ b/src/azure-cli/requirements.py3.Darwin.txt @@ -96,12 +96,12 @@ azure-synapse-accesscontrol==0.5.0 azure-synapse-artifacts==0.22.0 azure-synapse-managedprivateendpoints==0.4.0 azure-synapse-spark==0.7.0 -bcrypt==5.0.0 +bcrypt==3.2.0 certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==51.0.0 +cryptography==48.0.1 fabric==3.2.2 humanfriendly==10.0 idna==3.15 @@ -120,12 +120,12 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==7.2.2 +psutil==6.1.0 pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 PyNaCl==1.6.2 -pyOpenSSL==26.5.0 +pyOpenSSL==26.2.0 PySocks==1.7.1 python-dateutil==2.8.0 requests-oauthlib==1.2.0 diff --git a/src/azure-cli/requirements.py3.Linux.txt b/src/azure-cli/requirements.py3.Linux.txt index 7012761ba57..b6f10bf027f 100644 --- a/src/azure-cli/requirements.py3.Linux.txt +++ b/src/azure-cli/requirements.py3.Linux.txt @@ -96,12 +96,12 @@ azure-synapse-accesscontrol==0.5.0 azure-synapse-artifacts==0.22.0 azure-synapse-managedprivateendpoints==0.4.0 azure-synapse-spark==0.7.0 -bcrypt==5.0.0 +bcrypt==3.2.0 certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==51.0.0 +cryptography==48.0.1 distro==1.6.0 fabric==3.2.2 humanfriendly==10.0 @@ -121,12 +121,12 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==7.2.2 +psutil==6.1.0 pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 PyNaCl==1.6.2 -pyOpenSSL==26.5.0 +pyOpenSSL==26.2.0 PySocks==1.7.1 python-dateutil==2.8.0 requests-oauthlib==1.2.0 diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index f675f3db7d9..d174e06c629 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -96,13 +96,12 @@ azure-synapse-accesscontrol==0.5.0 azure-synapse-artifacts==0.22.0 azure-synapse-managedprivateendpoints==0.4.0 azure-synapse-spark==0.7.0 -bcrypt==5.0.0 +bcrypt==3.2.0 certifi==2024.7.4 cffi==2.0.0 chardet==5.2.0 colorama==0.4.6 -cryptography==48.0.1; platform_machine != "ARM64" -cryptography==51.0.0; platform_machine == "ARM64" +cryptography==48.0.1 fabric==3.2.2 humanfriendly==10.0 idna==3.15 @@ -121,15 +120,13 @@ paramiko==5.0.0 pbr==7.0.3 pkginfo==1.12.1.2 portalocker==3.2.0 -psutil==6.1.0; platform_machine != "ARM64" -psutil==7.2.2; platform_machine == "ARM64" +psutil==6.1.0 pycomposefile==0.0.34 PyGithub==1.55 PyJWT==2.15.0 pymsalruntime==0.20.6 PyNaCl==1.6.2 -pyOpenSSL==26.2.0; platform_machine != "ARM64" -pyOpenSSL==26.5.0; platform_machine == "ARM64" +pyOpenSSL==26.2.0 PySocks==1.7.1 python-dateutil==2.8.0 pywin32==311 From 7944fd30d6df1c0e2148a00b8e722c2e0bbb13f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 15:22:33 +0200 Subject: [PATCH 09/11] {Packaging} Isolate the WiX 3.14 tool cache Version the local WiX cache directory. The builder reuses an existing cache, so sharing the old path could silently run WiX 3.10 and fail to produce a valid ARM64 package. --- build_scripts/windows/azure-cli.wixproj | 2 +- build_scripts/windows/scripts/build.cmd | 2 +- .../windows/tests/test_arm64_packaging.py | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/build_scripts/windows/azure-cli.wixproj b/build_scripts/windows/azure-cli.wixproj index 874b2a379bd..4ef7eec7116 100644 --- a/build_scripts/windows/azure-cli.wixproj +++ b/build_scripts/windows/azure-cli.wixproj @@ -14,7 +14,7 @@ - artifacts\wix + artifacts\wix-3.14.1 $(MSBuildThisFileDirectory)$(LocalWixRoot) $(WixToolPath)\Wix.targets $(WixToolPath)\wixtasks.dll diff --git a/build_scripts/windows/scripts/build.cmd b/build_scripts/windows/scripts/build.cmd index bf2bcb82093..92bc6228e72 100644 --- a/build_scripts/windows/scripts/build.cmd +++ b/build_scripts/windows/scripts/build.cmd @@ -51,7 +51,7 @@ set ARTIFACTS_DIR=%~dp0..\artifacts mkdir %ARTIFACTS_DIR% set TEMP_SCRATCH_FOLDER=%ARTIFACTS_DIR%\cli_scratch set BUILDING_DIR=%ARTIFACTS_DIR%\cli -set WIX_DIR=%ARTIFACTS_DIR%\wix +set WIX_DIR=%ARTIFACTS_DIR%\wix-3.14.1 set PYTHON_DIR=%ARTIFACTS_DIR%\Python REM Get the absolute directory since we pushd into different levels of subdirectories. diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index b599e230c84..09855582242 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -3,8 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from pathlib import Path import unittest +from pathlib import Path ROOT = Path(__file__).resolve().parents[3] @@ -14,11 +14,17 @@ class WindowsArm64PackagingTest(unittest.TestCase): def test_build_uses_arm64_python_and_wix(self): build = (ROOT / 'build_scripts/windows/scripts/build.cmd').read_text() + project = (ROOT / 'build_scripts/windows/azure-cli.wixproj').read_text(encoding='utf-8-sig') self.assertIn('else if "%ARCH%"=="arm64"', build) self.assertIn('set PYTHON_ARCH=arm64', build) self.assertIn('wix314-binaries.zip', build) - self.assertIn('WIX_DOWNLOAD_SHA256=6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31', build) + self.assertIn( + 'WIX_DOWNLOAD_SHA256=6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31', + build, + ) self.assertIn('Get-FileHash -Algorithm SHA256 wix-archive.zip', build) + self.assertIn('set WIX_DIR=%ARTIFACTS_DIR%\\wix-3.14.1', build) + self.assertIn('artifacts\\wix-3.14.1', project) def test_wix_project_builds_arm64_packages(self): project = (ROOT / 'build_scripts/windows/azure-cli.wixproj').read_text(encoding='utf-8-sig') @@ -42,8 +48,12 @@ def test_pipeline_builds_and_tests_arm64_artifacts(self): self.assertIn("windows_arm64_pool: 'pool-windows-2022-arm64'", variables) self.assertIn('- name: enableWindowsArm64\n type: boolean\n default: false', pipeline) self.assertGreaterEqual(pipeline.count('Platform: arm64'), 4) - self.assertGreaterEqual(pipeline.count('PoolName: ${{ variables.windows_arm64_pool }}'), 5) - self.assertGreaterEqual(pipeline.count('${{ if eq(parameters.enableWindowsArm64, true) }}:'), 5) + self.assertGreaterEqual( + pipeline.count('PoolName: ${{ variables.windows_arm64_pool }}'), 5 + ) + self.assertGreaterEqual( + pipeline.count('${{ if eq(parameters.enableWindowsArm64, true) }}:'), 5 + ) self.assertIn('ArtifactName: msi-$(Platform)', pipeline) self.assertIn('ArtifactName: zip-$(Platform)', pipeline) From b714cd6eb05bb76a77f46b5c8fad6c1e229c9be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 8 Oct 2026 15:22:41 +0200 Subject: [PATCH 10/11] {Packaging} Prevent x64 installs over ARM64 Detect an installed ARM64 product from the x64 MSI and require it to be removed first. Windows ARM64 can run the x64 package under emulation, and both architectures use the native Program Files path. Because the products have separate upgrade codes, allowing both would let the x64 installer overwrite files without a coordinated MSI upgrade. --- build_scripts/windows/Product.wxs | 7 +++++++ build_scripts/windows/tests/test_arm64_packaging.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/build_scripts/windows/Product.wxs b/build_scripts/windows/Product.wxs index 1c1ee11f52c..fa59822da32 100644 --- a/build_scripts/windows/Product.wxs +++ b/build_scripts/windows/Product.wxs @@ -76,6 +76,13 @@ + + + + + NOT WIX_ARM64_INSTALLED + + diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 09855582242..66e51683133 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -36,6 +36,8 @@ def test_wix_project_builds_arm64_packages(self): self.assertIn('$(var.Platform) = "arm64"', product) self.assertIn('', product) self.assertIn(' Date: Tue, 20 Oct 2026 09:17:20 +0200 Subject: [PATCH 11/11] {Packaging} Follow WiX release redirects Make the WiX download follow GitHub release redirects and fail on HTTP errors before verifying or extracting the archive. In cmd.exe, `if errorlevel 1` means the exit code is greater than or equal to 1, so it catches values such as 1 and 3. It is also evaluated when the statement executes. By contrast, `%errorlevel%` inside a parenthesized block is expanded when the entire block is parsed. The previous checks could therefore read a stale value, ignore checksum and unzip failures, and fail later when MSBuild could not find Heat.exe. --- build_scripts/windows/scripts/build.cmd | 7 ++++--- build_scripts/windows/tests/test_arm64_packaging.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/build_scripts/windows/scripts/build.cmd b/build_scripts/windows/scripts/build.cmd index 92bc6228e72..eae9f896ab7 100644 --- a/build_scripts/windows/scripts/build.cmd +++ b/build_scripts/windows/scripts/build.cmd @@ -88,11 +88,12 @@ if "%TARGET%" == "msi" ( mkdir %WIX_DIR% pushd %WIX_DIR% echo Downloading Wix. - curl --output wix-archive.zip %WIX_DOWNLOAD_URL% + curl --fail --location --output wix-archive.zip %WIX_DOWNLOAD_URL% + if errorlevel 1 goto ERROR powershell.exe -NoProfile -Command "if ((Get-FileHash -Algorithm SHA256 wix-archive.zip).Hash -ne '%WIX_DOWNLOAD_SHA256%') { throw 'WiX archive SHA-256 mismatch' }" - if %errorlevel% neq 0 goto ERROR + if errorlevel 1 goto ERROR unzip wix-archive.zip - if %errorlevel% neq 0 goto ERROR + if errorlevel 1 goto ERROR del wix-archive.zip echo Wix downloaded and extracted successfully. popd diff --git a/build_scripts/windows/tests/test_arm64_packaging.py b/build_scripts/windows/tests/test_arm64_packaging.py index 66e51683133..f599fec7d65 100644 --- a/build_scripts/windows/tests/test_arm64_packaging.py +++ b/build_scripts/windows/tests/test_arm64_packaging.py @@ -23,6 +23,11 @@ def test_build_uses_arm64_python_and_wix(self): build, ) self.assertIn('Get-FileHash -Algorithm SHA256 wix-archive.zip', build) + self.assertIn( + 'curl --fail --location --output wix-archive.zip %WIX_DOWNLOAD_URL%', + build, + ) + self.assertNotIn('if %errorlevel% neq 0 goto ERROR\n unzip wix-archive.zip', build) self.assertIn('set WIX_DIR=%ARTIFACTS_DIR%\\wix-3.14.1', build) self.assertIn('artifacts\\wix-3.14.1', project)