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('