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..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:
@@ -177,8 +180,15 @@ jobs:
- job: VerifyWindowsRequirements
displayName: 'Verify src/azure-cli/requirements.*.Windows.txt'
condition: succeeded()
+ strategy:
+ matrix:
+ x64:
+ PoolName: ${{ variables.windows_pool }}
+ ${{ if eq(parameters.enableWindowsArm64, true) }}:
+ arm64:
+ PoolName: ${{ variables.windows_arm64_pool }}
pool:
- name: ${{ variables.windows_pool }}
+ name: $(PoolName)
steps:
- task: UsePythonVersion@0
@@ -191,6 +201,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 +229,19 @@ jobs:
matrix:
x86:
Platform: x86
+ PoolName: ${{ variables.windows_pool }}
x64:
Platform: x64
+ PoolName: ${{ variables.windows_pool }}
+ ${{ if eq(parameters.enableWindowsArm64, true) }}:
+ 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 +282,16 @@ jobs:
matrix:
x64:
Platform: x64
+ PoolName: ${{ variables.windows_pool }}
+ ${{ if eq(parameters.enableWindowsArm64, true) }}:
+ 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 +332,19 @@ jobs:
matrix:
x86:
Platform: x86
+ PoolName: ${{ variables.windows_pool }}
x64:
Platform: x64
+ PoolName: ${{ variables.windows_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'))
pool:
- name: ${{ variables.windows_pool }}
+ name: $(PoolName)
steps:
- task: DownloadPipelineArtifact@1
displayName: 'Download Build Artifacts'
@@ -340,9 +370,14 @@ jobs:
matrix:
x64:
Platform: x64
+ PoolName: ${{ variables.windows_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: ${{ variables.windows_pool }}
+ name: $(PoolName)
steps:
- task: DownloadPipelineArtifact@1
displayName: 'Download Build Artifacts'
diff --git a/build_scripts/windows/Product.wxs b/build_scripts/windows/Product.wxs
index c4d7fe2ebeb..fa59822da32 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,22 @@
+
+
+
+
+
+
+ NOT WIX_ARM64_INSTALLED
+
+
+
+
+
+
+
+ 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.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/azure-cli.wixproj b/build_scripts/windows/azure-cli.wixproj
index e0497237fbd..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
@@ -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..eae9f896ab7 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,16 @@ 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 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
@@ -48,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.
@@ -85,9 +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 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/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..f599fec7d65
--- /dev/null
+++ b/build_scripts/windows/tests/test_arm64_packaging.py
@@ -0,0 +1,77 @@
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for license information.
+# --------------------------------------------------------------------------------------------
+
+import unittest
+from pathlib import Path
+
+
+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()
+ 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('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)
+
+ 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('