[llvm] [llvm][release] Modernize the Windows release script (PR #228528)

Alexandre Ganea via llvm-commits llvm-commits at lists.llvm.org
Sat Oct 3 11:13:52 PDT 2026


https://github.com/aganea updated https://github.com/llvm/llvm-project/pull/228528

>From 562129b376c21505c36f097c8c8ea2b80f0056b2 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Fri, 2 Oct 2026 11:59:08 -0400
Subject: [PATCH 1/8] [llvm][release] Replace the Windows release .bat with a
 PowerShell script

Replace llvm/utils/release/build_llvm_release.bat with
build_llvm_release.ps1 and update the release-binaries workflows to
call it. See the RFC for the motivation and overall plan:
https://discourse.llvm.org/t/rfc-modernize-llvms-windows-release-build-and-packaging/91673

The script requires PowerShell 7 and relaunches itself under it (offering
to install it via winget) when started from Windows PowerShell 5.1.

Build flow:
  * Explicit, resumable steps: libxml2, stage1, pgo, stage2, package,
    tarball. -StartAt <step> resumes from a step; every step that runs
    starts from a clean directory, so stale artifacts (including an old
    PGO profile or tarball cache settings) cannot leak into later steps.
  * Stage 0 uses the official LLVM release (clang-cl + lld-link) unless
    -ForceMSVC is given. Stage 2 is built with ThinLTO, whole-program
    vtables and optional PGO.
  * -FastBuild builds only the stage 1 tools stage 2 needs and skips
    stage 1 tests, PGO and ThinLTO.
  * CMake options go through initial-cache files (-C) instead of the
    command line, avoiding command-line length limits.
  * Downloaded dependencies (libxml2, zlib, zstd) are SHA256-verified.
  * The MSI is verified after packaging: the permanent UpgradeCode, and
    the DuplicateFile entries for clang-cl.exe / lld-link.exe.
  * -DownloadSource builds from a release tarball instead of the local
    tree; the .bat's x86 variant is dropped.

Prerequisites:
  * -InstallPrerequisites installs everything needed on a fresh machine
    (NetFx3, Visual Studio Build Tools, official LLVM, CMake, Ninja,
    Python + psutil, 7-Zip, WiX 3.14, Git, SWIG, Perl, GNU Make) and
    elevates once when needed. -Unattended never prompts.
  * ARM64 uses a matching ARM64 Python; a native ARM64 PATH Python is
    recognized and no longer triggers a fallback warning.

Workflow changes:
  * release-binaries.yml runs the script under pwsh, selects the
    architecture from RUNNER_ARCH and passes -FastBuild -Unattended.
  * Drop the Windows -DLLVM_RELEASE_ENABLE_LTO=OFF override, which only
    affected the CMake-cache based Linux/macOS configure step.
  * release-binaries-all.yml now triggers on the new script's path.

Co-Authored-By: Claude Sonnet 5.5 <noreply at anthropic.com>
---
 .github/workflows/release-binaries-all.yml |    2 +-
 .github/workflows/release-binaries.yml     |   19 +-
 llvm/utils/release/build_llvm_release.bat  |  645 ------
 llvm/utils/release/build_llvm_release.ps1  | 2342 ++++++++++++++++++++
 4 files changed, 2353 insertions(+), 655 deletions(-)
 delete mode 100644 llvm/utils/release/build_llvm_release.bat
 create mode 100644 llvm/utils/release/build_llvm_release.ps1

diff --git a/.github/workflows/release-binaries-all.yml b/.github/workflows/release-binaries-all.yml
index cc10623f68f54d..38137b31619567 100644
--- a/.github/workflows/release-binaries-all.yml
+++ b/.github/workflows/release-binaries-all.yml
@@ -53,7 +53,7 @@ on:
       - '.github/workflows/release-binaries-setup-stage/*'
       - '.github/workflows/release-binaries-save-stage/*'
       - 'clang/cmake/caches/Release.cmake'
-      - 'llvm/utils/release/build_llvm_release.bat'
+      - 'llvm/utils/release/build_llvm_release.ps1'
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.event.pull_request.number || 'dispatch' }}
diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index 299ae35dfb6298..db64958052ebf9 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -159,11 +159,6 @@ jobs:
           target_cmake_flags="$target_cmake_flags -DBOOTSTRAP_BOOTSTRAP_DARWIN_osx_ARCHS=$arches -DBOOTSTRAP_BOOTSTRAP_DARWIN_osx_BUILTIN_ARCHS=$arches"
         fi
 
-        if [ "$RUNNER_OS" = "Windows" ]; then
-          # The build times out on Windows, so we need to disable LTO.
-          target_cmake_flags="$target_cmake_flags -DLLVM_RELEASE_ENABLE_LTO=OFF"
-        fi
-
         case "$INPUTS_RUNS_ON" in
           ubuntu-22.04* | windows-2022)
             build_runs_on="depot-$INPUTS_RUNS_ON-16"
@@ -342,14 +337,20 @@ jobs:
     - name: Build Windows
       id: build-windows
       if: runner.os == 'Windows'
+      shell: pwsh
       env:
         INPUTS_RUNS_ON: ${{ inputs.runs-on }}
         LLVM_VERSION: ${{ needs.prepare.outputs.release-version }}
-        EXTRA_ARGS: ${{ case(needs.prepare.outputs.build-runs-on == 'windows-11-arm', '--fast-build', '--fast-build') }}
       run: |
-        subst S: ${{ github.workspace }}
+        subst S: "${{ github.workspace }}"
         cd S:\llvm\utils\release\
-        .\build_llvm_release.bat "--$($env:RUNNER_ARCH.ToLower())" --version $env:LLVM_VERSION --local-python --skip-checkout $env:EXTRA_ARGS
+        $buildArgs = @{ Version = $env:LLVM_VERSION; FastBuild = $true; Unattended = $true }
+        switch ($env:RUNNER_ARCH) {
+          'X64'   { $buildArgs['x64'] = $true }
+          'ARM64' { $buildArgs['arm64'] = $true }
+          default { throw "Unsupported Windows runner architecture: $env:RUNNER_ARCH" }
+        }
+        & .\build_llvm_release.ps1 @buildArgs
         if ($env:INPUTS_RUNS_ON -eq "windows-11-arm") {
           $zstd = (Get-ChildItem -Recurse -Filter "zstd.exe" | Select-Object -First 1).fullName
           mv $zstd $env:GITHUB_WORKSPACE
@@ -367,7 +368,7 @@ jobs:
         LLVM_VERSION: ${{ needs.prepare.outputs.release-version }}
         BUILD_DIR_SUFFIX: ${{ case(runner.arch == 'ARM64', 'arm64', 'amd64') }}
       run: |
-        cat S:/llvm/utils/release/llvm_package_$($env:LLVM_VERSION)/build_$($env:BUILD_DIR_SUFFIX)/_CPack_Packages/win64/WIX/wix.log
+        cat S:/llvm/utils/release/llvm_package_$($env:LLVM_VERSION)/build_$($env:BUILD_DIR_SUFFIX)_stage2/_CPack_Packages/win64/WIX/wix.log
 
     - name: Create zstd compressed tarball
       shell: bash
diff --git a/llvm/utils/release/build_llvm_release.bat b/llvm/utils/release/build_llvm_release.bat
deleted file mode 100644
index 131de80bfe4077..00000000000000
--- a/llvm/utils/release/build_llvm_release.bat
+++ /dev/null
@@ -1,645 +0,0 @@
- at echo off
-
-REM Filter out tests that are known to fail.
-set "LIT_FILTER_OUT=gh110231.cpp|crt_initializers.cpp|init-order-atexit.cpp|use_after_return_linkage.cpp|initialization-bug.cpp|initialization-bug-no-global.cpp|trace-malloc-unbalanced.test|trace-malloc-2.test|TraceMallocTest|TestLockFileExclusive"
-
-setlocal enabledelayedexpansion
-goto begin
-
-:usage
-echo Script for building the LLVM installer on Windows,
-echo used for the releases at https://github.com/llvm/llvm-project/releases
-echo.
-echo Usage: build_llvm_release.bat --version ^<version^> [--x86,--x64, --arm64] [--skip-checkout] [--local-python] [--force-msvc] [--fast-build]
-echo.
-echo Options:
-echo --version: [required] version to build
-echo --help: display this help
-echo --x86: build and test x86 variant
-echo --x64: build and test x64 variant
-echo --arm64: build and test arm64 variant
-echo --skip-checkout: use local git checkout instead of downloading src.zip
-echo --local-python: use installed Python and does not try to use a specific version (3.11)
-echo --force-msvc: use MSVC compiler for stage0, even if clang-cl is present
-echo.
-echo Note: At least one variant to build is required.
-echo.
-echo Example: build_llvm_release.bat --version 15.0.0 --x86 --x64
-exit /b 1
-
-
-:begin
-
-::==============================================================================
-:: parse args
-set version=
-set help=
-set x86=
-set x64=
-set arm64=
-set skip-checkout=
-set local-python=
-set force-msvc=
-set fast-build=
-call :parse_args %*
-
-if "%help%" NEQ "" goto usage
-
-if "%version%" == "" (
-    echo --version option is required
-    echo =============================
-    goto usage
-)
-
-if "%arm64%" == "" if "%x64%" == "" if "%x86%" == "" (
-    echo nothing to build!
-    echo choose one or several variants from: --x86 --x64 --arm64
-    exit /b 1
-)
-
-::==============================================================================
-:: check prerequisites
-REM Note:
-REM   7zip versions 21.x and higher will try to extract the symlinks in
-REM   llvm's git archive, which requires running as administrator.
-
-REM Check 7-zip version and/or administrator permissions.
-for /f "delims=" %%i in ('7z.exe ^| findstr /r "2[1-9].[0-9][0-9]"') do set version_7z=%%i
-if not "%version_7z%"=="" (
-  REM Unique temporary filename to use by the 'mklink' command.
-  set "link_name=%temp%\%username%_%random%_%random%.tmp"
-
-  REM As the 'mklink' requires elevated permissions, the symbolic link
-  REM creation will fail if the script is not running as administrator.
-  mklink /d "!link_name!" . 1>nul 2>nul
-  if errorlevel 1 (
-    echo.
-    echo Script requires administrator permissions, or a 7-zip version 20.x or older.
-    echo Current version is "%version_7z%"
-    exit /b 1
-  ) else (
-    REM Remove the temporary symbolic link.
-    rd "!link_name!"
-  )
-)
-
-REM Prerequisites:
-REM
-REM   Visual Studio 2019, CMake, Ninja, GNUWin32, SWIG, Python 3,
-REM   Perl (for the OpenMP run-time).
-REM
-REM
-REM   For LLDB, SWIG version 4.1.1 should be used.
-REM
-
-:: Detect Visual Studio
-set vsinstall=
-set vswhere=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe
-
-if "%VSINSTALLDIR%" NEQ "" (
-  echo using enabled Visual Studio installation
-  set "vsinstall=%VSINSTALLDIR%"
-) else (
-  echo using vswhere to detect Visual Studio installation
-  FOR /F "delims=" %%r IN ('^""%vswhere%" -nologo -latest -products "*" -all -property installationPath^"') DO set vsinstall=%%r
-)
-set "vsdevcmd=%vsinstall%\Common7\Tools\VsDevCmd.bat"
-
-if not exist "%vsdevcmd%" (
-  echo Can't find any installation of Visual Studio
-  exit /b 1
-)
-echo Using VS devcmd: %vsdevcmd%
-
-::==============================================================================
-:: start echoing what we do
- at echo on
-
-set python32_dir=C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python311-32
-set python64_dir=C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python311
-set pythonarm64_dir=C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python311-arm64
-
-set revision=llvmorg-%version%
-set package_version=%version%
-set build_dir=%cd%\llvm_package_%package_version%
-
-echo Revision: %revision%
-echo Package version: %package_version%
-echo Build dir: %build_dir%
-echo.
-
-if exist %build_dir% (
-  echo Build directory already exists: %build_dir%
-  exit /b 1
-)
-mkdir %build_dir%
-cd %build_dir% || exit /b 1
-
-if "%skip-checkout%" == "true" (
-  echo Using local source
-  set llvm_src=%~dp0..\..\..
-) else (
-  echo Checking out %revision%
-  curl -L https://github.com/llvm/llvm-project/archive/%revision%.zip -o src.zip || exit /b 1
-  7z x src.zip || exit /b 1
-  mv llvm-project-* llvm-project || exit /b 1
-  set llvm_src=%build_dir%\llvm-project
-)
-
-set libxml_version=2.15.3
-curl -O https://gitlab.gnome.org/GNOME/libxml2/-/archive/v%libxml_version%/libxml2-v%libxml_version%.tar.gz || exit /b 1
-call :verify_checksum libxml2-v%libxml_version%.tar.gz sha256 0da50c1415f4ec0364569d2119b1436ba837b31df44af28569d234272c23cf1f || exit /b 1
-REM 'test' directory excluded because of symlinks.
-tar zxf libxml2-v%libxml_version%.tar.gz --exclude "test/*" || exit /b 1
-
-REM FIXME: It would be preferrable to use zlib-ng here since it is better
-REM        maintained and performs better than zlib, but lld tests currently
-REM        assume the original zlib is used. They need to be fixed first:
-REM        https://github.com/llvm/llvm-project/pull/186630#discussion_r2939953952
-set zlib_version=1.3.2
-curl -LO https://github.com/madler/zlib/releases/download/v%zlib_version%/zlib-%zlib_version%.tar.gz || exit /b 1
-call :verify_checksum zlib-%zlib_version%.tar.gz sha256 bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 || exit /b 1
-tar zxf zlib-%zlib_version%.tar.gz || exit /b 1
-
-set zstd_version=1.5.7
-curl -LO https://github.com/facebook/zstd/releases/download/v%zstd_version%/zstd-%zstd_version%.tar.gz || exit /b 1
-call :verify_checksum zstd-%zstd_version%.tar.gz sha256 eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3 || exit /b 1
-REM 'tests' directory excluded because of symlinks.
-tar zxf zstd-%zstd_version%.tar.gz --exclude "tests/*" || exit /b 1
-
-REM Setting CMAKE_CL_SHOWINCLUDES_PREFIX to work around PR27226.
-REM Have to add bcrypt.lib to the linker flags because of libxml2.
-REM Common flags for all builds.
-set common_compiler_flags=-DLIBXML_STATIC
-set common_cmake_flags=^
-  -DCMAKE_BUILD_TYPE=Release ^
-  -DLLVM_ENABLE_ASSERTIONS=OFF ^
-  -DLLVM_INSTALL_TOOLCHAIN_ONLY=ON ^
-  -DLLVM_TARGETS_TO_BUILD="AArch64;ARM;X86;BPF;WebAssembly;RISCV;NVPTX" ^
-  -DLLVM_BUILD_LLVM_C_DYLIB=ON ^
-  -DPython3_FIND_REGISTRY=NEVER ^
-  -DPACKAGE_VERSION=%package_version% ^
-  -DCMAKE_CL_SHOWINCLUDES_PREFIX="Note: including file: " ^
-  -DLLVM_ENABLE_LIBXML2=FORCE_ON ^
-  -DCLANG_ENABLE_LIBXML2=OFF ^
-  -DLLVM_ENABLE_ZLIB=FORCE_ON ^
-  -DLLVM_ENABLE_ZSTD=FORCE_ON ^
-  -DCMAKE_C_FLAGS="%common_compiler_flags%" ^
-  -DCMAKE_CXX_FLAGS="%common_compiler_flags%" ^
-  -DCMAKE_EXE_LINKER_FLAGS=bcrypt.lib ^
-  -DCMAKE_SHARED_LINKER_FLAGS=bcrypt.lib ^
-  -DLLVM_ENABLE_RPMALLOC=ON ^
-  -DLLVM_ENABLE_PROJECTS="clang;lld" ^
-  -DLLVM_ENABLE_RUNTIMES="compiler-rt" ^
-  -DCPACK_GENERATOR="WIX" ^
-  -DCOMPILER_RT_BUILD_ORC=OFF
-
-if "%force-msvc%" == "" (
-  clang-cl --version
-  if !errorlevel! EQU 0 (
-    lld-link --version
-    if !errorlevel! EQU 0 (
-      set common_compiler_flags=%common_compiler_flags% -fuse-ld=lld
-      set common_cmake_flags=%common_cmake_flags%^
-        -DCMAKE_C_COMPILER=clang-cl.exe ^
-        -DCMAKE_CXX_COMPILER=clang-cl.exe ^
-        -DCMAKE_LINKER=lld-link.exe ^
-        -DLLVM_ENABLE_LLD=ON ^
-        -DCMAKE_C_FLAGS="%common_compiler_flags%" ^
-        -DCMAKE_CXX_FLAGS="%common_compiler_flags%"
-    )
-  )
-)
-
-set common_lldb_flags=^
-  -DLLDB_RELOCATABLE_PYTHON=1 ^
-  -DLLDB_EMBED_PYTHON_HOME=OFF
-
-set cmake_profile_flags=""
-
-REM Preserve original path
-set OLDPATH=%PATH%
-
-REM Build the 32-bits and/or 64-bits binaries.
-if "%x86%" == "true" call :do_build_32 || exit /b 1
-if "%x64%" == "true" call :do_build_64_common amd64 %python64_dir% || exit /b 1
-if "%arm64%" == "true" call :do_build_64_common arm64 %pythonarm64_dir% || exit /b 1
-exit /b 0
-
-::==============================================================================
-:: Build 32-bits binaries.
-::==============================================================================
-:do_build_32
-call :set_environment %python32_dir% || exit /b 1
-call "%vsdevcmd%" -arch=x86 || exit /b 1
- at echo on
-mkdir build32_stage0
-cd build32_stage0
-call :do_build_libxml || exit /b 1
-call :do_build_zlib || exit /b 1
-call :do_build_zstd || exit /b 1
-
-REM Stage0 binaries directory; used in stage1.
-set "stage0_bin_dir=%build_dir%/build32_stage0/bin"
-set cmake_flags=^
-  %common_cmake_flags% ^
-  -DLLVM_ENABLE_RPMALLOC=OFF ^
-  -DPython3_ROOT_DIR=%PYTHONHOME% ^
-  -DLIBXML2_INCLUDE_DIR=%libxmldir%/include/libxml2 ^
-  -DLIBXML2_LIBRARIES=%libxmldir%/lib/libxml2s.lib ^
-  -DZLIB_INCLUDE_DIR=%zlibdir%/include ^
-  -DZLIB_LIBRARY=%zlibdir%/lib/zs.lib ^
-  -DZLIB_LIBRARY_RELEASE=%zlibdir%/lib/zs.lib ^
-  -Dzstd_INCLUDE_DIR=%zstddir%/include ^
-  -Dzstd_LIBRARY=%zstddir%/lib/zstd_static.lib
-
-cmake -GNinja %cmake_flags% %llvm_src%\llvm || exit /b 1
-ninja || exit /b 1
-REM ninja check-llvm || exit /b 1
-REM ninja check-clang || exit /b 1
-ninja check-lld || exit /b 1
-REM ninja check-runtimes || exit /b 1
-cd..
-
-REM CMake expects the paths that specifies the compiler and linker to be
-REM with forward slash.
-set all_cmake_flags=^
-  %cmake_flags% ^
-  -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld;lldb;" ^
-  -DLLVM_ENABLE_RUNTIMES="compiler-rt;openmp" ^
-  %common_lldb_flags% ^
-  -DPYTHON_HOME=%PYTHONHOME% ^
-  -DCMAKE_C_COMPILER=%stage0_bin_dir%/clang-cl.exe ^
-  -DCMAKE_CXX_COMPILER=%stage0_bin_dir%/clang-cl.exe ^
-  -DCMAKE_LINKER=%stage0_bin_dir%/lld-link.exe ^
-  -DCMAKE_AR=%stage0_bin_dir%/llvm-lib.exe ^
-  -DCMAKE_RC=%stage0_bin_dir%/llvm-windres.exe
-set cmake_flags=%all_cmake_flags:\=/%
-
-mkdir build32
-cd build32
-cmake -GNinja %cmake_flags% %llvm_src%\llvm || exit /b 1
-ninja || exit /b 1
-REM ninja check-llvm || exit /b 1
-REM ninja check-clang || exit /b 1
-ninja check-lld || exit /b 1
-REM ninja check-runtimes || exit /b 1
-REM ninja check-clang-tools || exit /b 1
-ninja package || exit /b 1
-call :verify_msi_upgrade_code || exit /b 1
-cd ..
-
-exit /b 0
-::==============================================================================
-
-::==============================================================================
-:: Build 64-bits binaries (common function for both x64 and arm64)
-::==============================================================================
-:do_build_64_common
-set arch=%1
-set python_dir=%2
-
-call :set_environment %python_dir% || exit /b 1
-call "%vsdevcmd%" -arch=%arch% || exit /b 1
- at echo on
-mkdir build_%arch%_stage0
-cd build_%arch%_stage0
-call :do_build_libxml || exit /b 1
-call :do_build_zlib || exit /b 1
-call :do_build_zstd || exit /b 1
-
-REM Stage0 binaries directory; used in stage1.
-set "stage0_bin_dir=%build_dir%/build_%arch%_stage0/bin"
-set cmake_flags=^
-  %common_cmake_flags% ^
-  -DPython3_ROOT_DIR=%PYTHONHOME% ^
-  -DLIBXML2_INCLUDE_DIR=%libxmldir%/include/libxml2 ^
-  -DLIBXML2_LIBRARIES=%libxmldir%/lib/libxml2s.lib ^
-  -DZLIB_INCLUDE_DIR=%zlibdir%/include ^
-  -DZLIB_LIBRARY=%zlibdir%/lib/zs.lib ^
-  -DZLIB_LIBRARY_RELEASE=%zlibdir%/lib/zs.lib ^
-  -Dzstd_INCLUDE_DIR=%zstddir%/include ^
-  -Dzstd_LIBRARY=%zstddir%/lib/zstd_static.lib ^
-  -DCLANG_DEFAULT_LINKER=lld
-if "%arch%"=="arm64" (
-  set cmake_flags=%cmake_flags% ^
-    -DCOMPILER_RT_BUILD_SANITIZERS=OFF
-)
-
-cmake -GNinja %cmake_flags% ^
-  -DLLVM_TARGETS_TO_BUILD=Native ^
-  %llvm_src%\llvm || exit /b 1
-ninja clang lld llvm-lib llvm-windres runtimes || exit /b 1
-if "%fast-build%" neq "true" (
-ninja || exit /b 1
-ninja check-llvm || exit /b 1
-ninja check-clang || exit /b 1
-ninja check-lld || exit /b 1
-if "%arch%"=="amd64" (
-  ninja check-runtimes || exit /b 1
-)
-)
-cd..
-
-REM CMake expects the paths that specifies the compiler and linker to be
-REM with forward slash.
-set all_cmake_flags=^
-  %cmake_flags% ^
-  -DCMAKE_C_COMPILER=%stage0_bin_dir%/clang-cl.exe ^
-  -DCMAKE_CXX_COMPILER=%stage0_bin_dir%/clang-cl.exe ^
-  -DCMAKE_LINKER=%stage0_bin_dir%/lld-link.exe ^
-  -DCMAKE_AR=%stage0_bin_dir%/llvm-lib.exe ^
-  -DCMAKE_RC=%stage0_bin_dir%/llvm-windres.exe
-if "%arch%"=="arm64" (
-  set all_cmake_flags=%all_cmake_flags% ^
-    -DCPACK_SYSTEM_NAME=woa64
-)
-set cmake_flags=%all_cmake_flags:\=/%
-
-mkdir build_%arch%
-cd build_%arch%
-if "%fast-build%" neq "true" (
-  call :do_generate_profile || exit /b 1
-)
-cmake -GNinja %cmake_flags% ^
-  -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld;lldb;flang;mlir" ^
-  -DLLVM_ENABLE_RUNTIMES="compiler-rt;openmp" ^
-  %common_lldb_flags% ^
-  -DPYTHON_HOME=%PYTHONHOME% ^
-  %cmake_profile_flags% %llvm_src%\llvm || exit /b 1
-ninja || exit /b 1
-ninja check-llvm || exit /b 1
-ninja check-clang || exit /b 1
-ninja check-lld || exit /b 1
-if "%arch%"=="amd64" (
-  ninja check-runtimes || exit /b 1
-)
-ninja check-clang-tools || exit /b 1
-ninja check-clangd || exit /b 1
-REM ninja check-flang || exit /b 1
-REM ninja check-mlir || exit /b 1
-REM ninja check-lldb || exit /b 1
-ninja package || exit /b 1
-call :verify_msi_upgrade_code || exit /b 1
-
-:: generate tarball with install toolchain only off
-if "%arch%"=="amd64" (
-  set filename=clang+llvm-%version%-x86_64-pc-windows-msvc
-) else (
-  set filename=clang+llvm-%version%-aarch64-pc-windows-msvc
-)
-cmake -GNinja %cmake_flags% %cmake_profile_flags% -DLLVM_INSTALL_TOOLCHAIN_ONLY=OFF ^
-  -DCMAKE_INSTALL_PREFIX=%build_dir%/%filename% %llvm_src%\llvm || exit /b 1
-ninja install || exit /b 1
-:: check llvm_config is present & returns something
-%build_dir%/%filename%/bin/llvm-config.exe --bindir || exit /b 1
-cd ..
-7z a -ttar -so %filename%.tar %filename% | 7z a -txz -si %filename%.tar.xz
-
-exit /b 0
-
-::==============================================================================
-:: Set PATH and some environment variables.
-::==============================================================================
-:set_environment
-REM Restore original path
-set "PATH=%OLDPATH%"
-
-set "python_dir=%1"
-
-REM Set Python environment
-if "%local-python%" == "true" (
-  set "python_dir="
-  set "python_exe="
-  FOR /F "delims=" %%i IN ('where python.exe') DO (
-    if not defined python_exe (
-      set "python_exe=%%i"
-      rem Python Manager puts only the executable and a __target__ file into its bin directory.
-      rem winget installs a reparse point into WindowsApps.
-      rem But: CMake needs a full Python installation directory or find_package() will fail.
-      FOR /F "delims=" %%h IN ('!python_exe! -c "import sys; print(sys.exec_prefix)"') DO (
-        set "python_dir=%%h"
-      )
-    )
-  )
-)
-if not defined python_dir exit /b 1
-"%python_dir%\python.exe" --version || exit /b 1
-set "PYTHONHOME=%python_dir%"
-set "PATH=%PYTHONHOME%;%PATH%"
-
-set "VSCMD_START_DIR=%build_dir%"
-
-exit /b 0
-
-::=============================================================================
-
-::==============================================================================
-:: Verify that the generated MSI has LLVM's permanent UpgradeCode.
-::==============================================================================
-:verify_msi_upgrade_code
-:: This code has to match the value in llvm/CMakeLists.txt
-set "expected_upgrade_code=B08613CD-8BD0-4FB6-8937-621936604DE3"
-set "msi_path="
-for %%f in (*.msi) do (
-  if defined msi_path (
-    echo Found more than one MSI in %cd%; cannot determine which one to verify.
-    exit /b 1
-  )
-  set "msi_path=%%~ff"
-)
-if not defined msi_path (
-  echo No MSI found in %cd%.
-  exit /b 1
-)
-
-:: Query the MSI's UpgradeCode through the Windows Installer COM API. WiX's
-:: dark.exe would do this too, but it is deprecated and removed in newer WiX.
-set "msi_query=SELECT Value FROM Property WHERE Property='UpgradeCode'"
-set "ps_cmd=$i = New-Object -ComObject WindowsInstaller.Installer;"
-set "ps_cmd=%ps_cmd% $db = $i.OpenDatabase($env:msi_path, 0);"
-set "ps_cmd=%ps_cmd% $v = $db.OpenView($env:msi_query);"
-set "ps_cmd=%ps_cmd% $v.Execute();"
-set "ps_cmd=%ps_cmd% $v.Fetch().StringData(1).Trim('{', '}')"
-
-set "actual_upgrade_code="
-for /f %%i in ('powershell -NoProfile -Command "%ps_cmd%"') do (
-  set "actual_upgrade_code=%%i"
-)
-if not defined actual_upgrade_code (
-  echo Failed to read the UpgradeCode from "%msi_path%".
-  exit /b 1
-)
-if /i not "%actual_upgrade_code%"=="%expected_upgrade_code%" (
-  echo Unexpected UpgradeCode %actual_upgrade_code% in "%msi_path%".
-  echo Expected %expected_upgrade_code%.
-  exit /b 1
-)
-echo Verified MSI UpgradeCode: %expected_upgrade_code%
-exit /b 0
-
-::=============================================================================
-
-::==============================================================================
-:: Verify checksum.
-::==============================================================================
-:verify_checksum
-cmake -E %2sum %1 > %1.%2sum
-echo %3  %1> %1.%2sum.orig
-cmake -E compare_files --ignore-eol %1.%2sum %1.%2sum.orig
-if %ERRORLEVEL% NEQ 0 (
-  echo verify_checksum failed for %1
-  exit /b 1
-)
-exit /b 0
-
-::==============================================================================
-:: Build libxml.
-::==============================================================================
-:do_build_libxml
-mkdir libxmlbuild
-cd libxmlbuild
-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install ^
-  -DBUILD_SHARED_LIBS=OFF -DLIBXML2_WITH_C14N=OFF -DLIBXML2_WITH_CATALOG=OFF ^
-  -DLIBXML2_WITH_DEBUG=OFF -DLIBXML2_WITH_DOCB=OFF -DLIBXML2_WITH_FTP=OFF ^
-  -DLIBXML2_WITH_HTML=OFF -DLIBXML2_WITH_HTTP=OFF -DLIBXML2_WITH_ICONV=OFF ^
-  -DLIBXML2_WITH_ICU=OFF -DLIBXML2_WITH_ISO8859X=OFF -DLIBXML2_WITH_LEGACY=OFF ^
-  -DLIBXML2_WITH_LZMA=OFF -DLIBXML2_WITH_MEM_DEBUG=OFF -DLIBXML2_WITH_MODULES=OFF ^
-  -DLIBXML2_WITH_OUTPUT=ON -DLIBXML2_WITH_PATTERN=OFF -DLIBXML2_WITH_PROGRAMS=OFF ^
-  -DLIBXML2_WITH_PUSH=OFF -DLIBXML2_WITH_PYTHON=OFF -DLIBXML2_WITH_READER=OFF ^
-  -DLIBXML2_WITH_REGEXPS=OFF -DLIBXML2_WITH_RUN_DEBUG=OFF -DLIBXML2_WITH_SAX1=ON ^
-  -DLIBXML2_WITH_SCHEMAS=OFF -DLIBXML2_WITH_SCHEMATRON=OFF -DLIBXML2_WITH_TESTS=OFF ^
-  -DLIBXML2_WITH_THREADS=ON -DLIBXML2_WITH_THREAD_ALLOC=OFF -DLIBXML2_WITH_TREE=ON ^
-  -DLIBXML2_WITH_VALID=OFF -DLIBXML2_WITH_WRITER=OFF -DLIBXML2_WITH_XINCLUDE=OFF ^
-  -DLIBXML2_WITH_XPATH=OFF -DLIBXML2_WITH_XPTR=OFF -DLIBXML2_WITH_ZLIB=OFF ^
-  -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ^
-  ../../libxml2-v%libxml_version% || exit /b 1
-ninja install || exit /b 1
-set libxmldir=%cd%\install
-set "libxmldir=%libxmldir:\=/%"
-cd ..
-exit /b 0
-
-::==============================================================================
-:: Build zlib.
-::==============================================================================
-:do_build_zlib
-mkdir zlibbuild
-cd zlibbuild
-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install ^
-  -DZLIB_BUILD_TESTING=OFF -DZLIB_BUILD_SHARED=OFF -DZLIB_BUILD_STATIC=ON ^
-  -DZLIB_INSTALL=ON -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ^
-  ../../zlib-%zlib_version% || exit /b 1
-ninja install || exit /b 1
-set zlibdir=%cd%\install
-set "zlibdir=%zlibdir:\=/%"
-cd ..
-exit /b 0
-
-::==============================================================================
-:: Build zstd.
-::==============================================================================
-:do_build_zstd
-mkdir zstdbuild
-cd zstdbuild
-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install ^
-  -DZSTD_BUILD_PROGRAMS=ON -DZSTD_BUILD_TESTS=OFF -DZSTD_BUILD_STATIC=ON ^
-  -DZSTD_BUILD_SHARED=OFF -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded ^
-  ../../zstd-%zstd_version%/build/cmake || exit /b 1
-ninja install || exit /b 1
-set zstddir=%cd%\install
-set "zstddir=%zstddir:\=/%"
-cd ..
-exit /b 0
-
-::==============================================================================
-:: Generate a PGO profile.
-::==============================================================================
-:do_generate_profile
-REM Build Clang with instrumentation.
-mkdir instrument
-cd instrument
-cmake -GNinja %cmake_flags% -DLLVM_TARGETS_TO_BUILD=Native ^
-  -DLLVM_BUILD_INSTRUMENTED=IR %llvm_src%\llvm || exit /b 1
-ninja clang || exit /b 1
-set instrumented_clang=%cd:\=/%/bin/clang-cl.exe
-cd ..
-REM Use that to build part of llvm to generate a profile.
-mkdir train
-cd train
-cmake -GNinja %cmake_flags% ^
-  -DCMAKE_C_COMPILER=%instrumented_clang% ^
-  -DCMAKE_CXX_COMPILER=%instrumented_clang% ^
-  -DLLVM_ENABLE_PROJECTS=clang ^
-  -DLLVM_TARGETS_TO_BUILD=Native ^
-  %llvm_src%\llvm || exit /b 1
-REM Drop profiles generated from running cmake; those are not representative.
-del ..\instrument\profiles\*.profraw
-ninja tools/clang/lib/Sema/CMakeFiles/obj.clangSema.dir/Sema.cpp.obj
-cd ..
-set profile=%cd:\=/%/profile.profdata
-%stage0_bin_dir%\llvm-profdata merge -output=%profile% instrument\profiles\*.profraw || exit /b 1
-set common_compiler_flags=%common_compiler_flags% -Wno-backend-plugin
-set cmake_profile_flags=-DLLVM_PROFDATA_FILE=%profile% ^
-  -DCMAKE_C_FLAGS="%common_compiler_flags%" ^
-  -DCMAKE_CXX_FLAGS="%common_compiler_flags%"
-exit /b 0
-
-::=============================================================================
-:: Parse command line arguments.
-:: The format for the arguments is:
-::   Boolean: --option
-::   Value:   --option<separator>value
-::     with <separator> being: space, colon, semicolon or equal sign
-::
-:: Command line usage example:
-::   my-batch-file.bat --build --type=release --version 123
-:: It will create 3 variables:
-::   'build' with the value 'true'
-::   'type' with the value 'release'
-::   'version' with the value '123'
-::
-:: Usage:
-::   set "build="
-::   set "type="
-::   set "version="
-::
-::   REM Parse arguments.
-::   call :parse_args %*
-::
-::   if defined build (
-::     ...
-::   )
-::   if %type%=='release' (
-::     ...
-::   )
-::   if %version%=='123' (
-::     ...
-::   )
-::=============================================================================
-:parse_args
-  set "arg_name="
-  :parse_args_start
-  if "%1" == "" (
-    :: Set a seen boolean argument.
-    if "%arg_name%" neq "" (
-      set "%arg_name%=true"
-    )
-    goto :parse_args_done
-  )
-  set aux=%1
-  if "%aux:~0,2%" == "--" (
-    :: Set a seen boolean argument.
-    if "%arg_name%" neq "" (
-      set "%arg_name%=true"
-    )
-    set "arg_name=%aux:~2,250%"
-  ) else (
-    set "%arg_name%=%1"
-    set "arg_name="
-  )
-  shift
-  goto :parse_args_start
-
-:parse_args_done
-exit /b 0
diff --git a/llvm/utils/release/build_llvm_release.ps1 b/llvm/utils/release/build_llvm_release.ps1
new file mode 100644
index 00000000000000..92fc701fff6b8a
--- /dev/null
+++ b/llvm/utils/release/build_llvm_release.ps1
@@ -0,0 +1,2342 @@
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+    Script for building the LLVM installer on Windows,
+    used for the releases at https://github.com/llvm/llvm-project/releases
+
+.DESCRIPTION
+    Builds LLVM release packages for Windows (x64, arm64).
+    Performs a 2-stage build with a ThinLTO final stage and optional PGO.
+    Can bootstrap a fresh VM by installing all prerequisites.
+
+    By default, builds x64 using the local source tree and auto-detected
+    Python.  Use -DownloadSource to download a tagged release tarball
+    instead.
+
+    Build steps (in order):
+      1. libxml2   - Build libxml2, zlib, and zstd (inside stage 1 directory)
+      2. stage1    - Build the stage 1 bootstrap compiler with the host compiler
+      3. pgo       - PGO instrumented build + training + profile merge
+      4. stage2    - Build the stage 2 self-hosted compiler with stage 1 clang
+      5. package   - Create WiX MSI installer
+      6. tarball   - Generate full install tarball
+
+.PARAMETER Version
+    LLVM version string (e.g. "19.1.0").  If omitted, auto-detected from
+    the source tree.
+
+.PARAMETER x64
+    Build for x64 (64-bit).  This is the default if no architecture is specified.
+
+.PARAMETER arm64
+    Build for ARM64 (AArch64).
+
+.PARAMETER DownloadSource
+    Download and extract the tagged release source tarball from GitHub
+    instead of using the local source tree.  Requires -Version.
+
+.PARAMETER ForceMSVC
+    Force using MSVC (cl.exe) as the stage 0 host compiler instead of the
+    official LLVM release. Skips the LLVM release prerequisite.
+
+.PARAMETER FastBuild
+    Build only the stage 1 tools needed by stage 2 and skip the remaining
+    stage 1 targets, stage 1 tests, and PGO. Stage 2 tests still run.
+
+.PARAMETER InstallPrerequisites
+    Install build prerequisites (NetFx3, Visual Studio, official LLVM release,
+    CMake, Python and its psutil module, etc.)
+    before building.  When used with -arm64, also installs ARM64 Python for
+    LLDB.  Requests administrator elevation once for the entire prerequisite
+    installation when the current shell is not elevated.
+
+.PARAMETER Unattended
+    Never prompt for input.  Missing prerequisites cause the build to fail
+    unless -InstallPrerequisites is also specified.  An existing build
+    directory must be resumed with -StartAt or removed beforehand.  Run
+    from an elevated shell when using -InstallPrerequisites.
+
+.PARAMETER StartAt
+    Resume a previous build from a specific step.  Artifacts from earlier
+    steps must already exist on disk.  The step being restarted gets a
+    clean directory; earlier steps are skipped entirely.
+
+    Valid steps: libxml2, stage1, pgo, stage2, package, tarball
+
+    Pass an empty string (-StartAt "") or "?" (-StartAt "?") to display
+    the list of available steps and exit.
+
+.PARAMETER Help
+    Display this help message and exit.
+
+.EXAMPLE
+    .\build_llvm_release.ps1
+    Full x64 build using the local source tree.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -x64 -arm64
+    Build for both x64 and ARM64.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -Version 19.1.0 -DownloadSource
+    Download version 19.1.0 sources and build x64.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -InstallPrerequisites -x64
+    Install prerequisites, then do a full x64 build.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -InstallPrerequisites -x64 -arm64
+    Install prerequisites (including ARM64 Python), then build both architectures.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -Unattended -InstallPrerequisites
+    From an elevated shell, install prerequisites and build without prompts.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -x64 -StartAt stage2
+    Resume from stage 2 (reuses the stage 1 bootstrap compiler and PGO profile).
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -x64 -StartAt package
+    Resume at WiX MSI packaging, then regenerate the portable archive.
+
+.EXAMPLE
+    .\build_llvm_release.ps1 -StartAt ""
+    Display the list of available build steps and exit.
+
+.NOTES
+    Python is auto-detected from PATH for x64 builds.  For ARM64 builds,
+    the script probes standard install locations for ARM64 Python.  Use
+    -InstallPrerequisites to install it automatically.
+
+    Environment variables:
+      LLVM_NINJA_OVERRIDE  - Override the ninja binary and optionally provide
+                             extra flags.  The first token is the executable,
+                             remaining tokens are prepended to every ninja
+                             invocation.
+                             Example: LLVM_NINJA_OVERRIDE="myninja.exe --flag1 --flag2"
+#>
+
+param(
+    [string]$Version,
+    [switch]$x64,
+    [switch]$arm64,
+    [switch]$DownloadSource,
+    [switch]$ForceMSVC,
+    [switch]$FastBuild,
+    [switch]$InstallPrerequisites,
+    [switch]$Unattended,
+    [switch]$PrerequisitesOnly,  # Internal mode for the elevated installer.
+    [string]$StartAt,
+    [switch]$Help
+)
+
+#===============================================================================
+# PowerShell 7 self-relaunch
+#
+# This script requires PowerShell 7+ features (ternary operator, null-
+# coalescing, improved error handling, etc.).  If we detect we are running
+# under Windows PowerShell 5.x, we attempt to find or install PowerShell 7
+# and re-launch ourselves under it, forwarding all original arguments.
+#===============================================================================
+if ($PSVersionTable.PSVersion.Major -lt 7) {
+    # Try to find pwsh in PATH first.
+    $pwsh = Get-Command pwsh -ErrorAction SilentlyContinue
+    if (-not $pwsh) {
+        # Check the default install location.
+        $defaultPath = "$env:ProgramFiles\PowerShell\7\pwsh.exe"
+        if (Test-Path $defaultPath) {
+            $pwsh = Get-Item $defaultPath
+        }
+    }
+
+    if (-not $pwsh) {
+        if ($Unattended) {
+            Write-Error "PowerShell 7 is required. Install it before running in unattended mode."
+            exit 1
+        }
+        Write-Host "PowerShell 7 is required but not found." -ForegroundColor Yellow
+        Write-Host "Attempting to install via winget..." -ForegroundColor Yellow
+        $winget = Get-Command winget -ErrorAction SilentlyContinue
+        if (-not $winget) {
+            Write-Error "Cannot install PowerShell 7: winget is not available.`nPlease install PowerShell 7 manually from https://aka.ms/powershell-release?tag=stable"
+            exit 1
+        }
+        winget install --id Microsoft.PowerShell --source winget --accept-source-agreements --accept-package-agreements
+        if ($LASTEXITCODE -ne 0) {
+            Write-Error "Failed to install PowerShell 7 via winget (exit code $LASTEXITCODE)."
+            exit 1
+        }
+        # Refresh PATH so we can find the newly installed pwsh.
+        $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
+                     [System.Environment]::GetEnvironmentVariable("Path", "User")
+        $pwsh = Get-Command pwsh -ErrorAction SilentlyContinue
+        if (-not $pwsh) {
+            $defaultPath = "$env:ProgramFiles\PowerShell\7\pwsh.exe"
+            if (Test-Path $defaultPath) {
+                $pwsh = Get-Item $defaultPath
+            }
+        }
+        if (-not $pwsh) {
+            Write-Error "PowerShell 7 was installed but pwsh.exe could not be found in PATH."
+            exit 1
+        }
+        Write-Host "PowerShell 7 installed successfully." -ForegroundColor Green
+    }
+
+    # Re-launch this script under PowerShell 7, forwarding all arguments.
+    # Note: avoid PS7-only syntax (??  ternary, etc.) in this block since
+    # it executes under PS5 (PowerShell 5).
+    $pwshPath = if ($pwsh.Source) { $pwsh.Source } else { $pwsh.Path }
+    Write-Host "Re-launching under PowerShell 7 ($pwshPath)..." -ForegroundColor Cyan
+    $forwardArgs = @()
+    foreach ($entry in $PSBoundParameters.GetEnumerator()) {
+        if ($entry.Value -is [System.Management.Automation.SwitchParameter]) {
+            if ($entry.Value.IsPresent) { $forwardArgs += "-$($entry.Key)" }
+        } else {
+            $forwardArgs += "-$($entry.Key)"
+            # Windows PowerShell 5.1 drops empty-string arguments when calling
+            # native programs; a literal pair of quotes survives as "".
+            $stringValue = [string]$entry.Value
+            $forwardArgs += if ($stringValue -eq '') { '""' } else { $stringValue }
+        }
+    }
+    & $pwshPath -NoProfile -ExecutionPolicy Bypass -File $PSCommandPath @forwardArgs
+    exit $LASTEXITCODE
+}
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$script:ReleaseScriptPath = $PSCommandPath
+
+# Save the console mode so we can restore it at exit.  Child processes
+# (cmake, ninja, link.exe, etc.) sometimes disable virtual terminal
+# processing and don't restore it, which breaks arrow keys and ESC in
+# the parent shell after the script finishes.
+$script:SavedConsoleMode = $null
+try {
+    Add-Type -TypeDefinition @'
+using System;
+using System.Runtime.InteropServices;
+public static class ConsoleMode {
+    [DllImport("kernel32.dll", SetLastError = true)]
+    static extern IntPtr GetStdHandle(int nStdHandle);
+    [DllImport("kernel32.dll", SetLastError = true)]
+    static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
+    [DllImport("kernel32.dll", SetLastError = true)]
+    static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
+    const int STD_INPUT_HANDLE = -10;
+    public static uint Get() {
+        uint mode;
+        GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), out mode);
+        return mode;
+    }
+    public static void Set(uint mode) {
+        SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode);
+    }
+}
+'@ -ErrorAction SilentlyContinue
+    $script:SavedConsoleMode = [ConsoleMode]::Get()
+} catch {
+    # Non-fatal; we just won't be able to restore the console mode.
+}
+
+# Default to x64 if no architecture specified
+if (-not $x64 -and -not $arm64) {
+    $x64 = $true
+}
+
+#===============================================================================
+# Step-resume support
+#===============================================================================
+
+$script:StepOrder   = @('libxml2','stage1','pgo','stage2','package','tarball')
+
+if ($PSBoundParameters.ContainsKey('StartAt')) {
+    if (-not $StartAt -or $StartAt -eq '?') {
+        Write-Host "Available -StartAt steps (in order):" -ForegroundColor Cyan
+        for ($i = 0; $i -lt $script:StepOrder.Count; $i++) {
+            Write-Host "  $($i + 1). $($script:StepOrder[$i])"
+        }
+        exit 0
+    }
+    if ($StartAt -notin $script:StepOrder) {
+        Write-Error "Invalid -StartAt value: '$StartAt'. Valid steps: $($script:StepOrder -join ', ')"
+        exit 1
+    }
+}
+
+$script:StartAtStep = if ($StartAt) { $StartAt } else { $null }
+
+function Test-ShouldRun {
+    <#
+    .SYNOPSIS
+        Returns $true if the given step should execute (i.e. it is at or after
+        the -StartAt step).  When -StartAt is not set, always returns $true.
+    #>
+    param([Parameter(Mandatory)][string]$Step)
+    if (-not $script:StartAtStep) { return $true }
+    return $script:StepOrder.IndexOf($Step) -ge $script:StepOrder.IndexOf($script:StartAtStep)
+}
+
+function Test-IsStartStep {
+    <#
+    .SYNOPSIS
+        Returns $true if the given step is exactly the -StartAt step (the step
+        whose directory should be cleaned before a fresh rebuild).
+    #>
+    param([Parameter(Mandatory)][string]$Step)
+    return ($script:StartAtStep -and $script:StartAtStep -eq $Step)
+}
+
+function Assert-PathExists {
+    <#
+    .SYNOPSIS
+        Validates that a path exists on disk.  Used when skipping steps to
+        ensure the artifacts from a prior run are still present.
+    #>
+    param(
+        [Parameter(Mandatory)][string]$Path,
+        [string]$Description = $Path
+    )
+    if (-not (Test-Path $Path)) {
+        Write-Error "Required artifact missing: $Description`n  Path: $Path`n  Hint: run the earlier build steps first, or use a different -StartAt value."
+        exit 1
+    }
+}
+
+function Remove-StepDirectory {
+    <#
+    .SYNOPSIS
+        Removes a directory (or file) if it exists, printing a message.
+        Used to clean a step's build directory before restarting it.
+    #>
+    param([Parameter(Mandatory)][string]$Path)
+    if (Test-Path $Path) {
+        Write-SubStep "Cleaning: $Path"
+        Remove-Item -Recurse -Force $Path
+    }
+}
+
+# Ninja override: allow using a custom ninja binary with extra flags.
+# Usage: $env:LLVM_NINJA_OVERRIDE = "myninja.exe --flag1 --flag2"
+if ($env:LLVM_NINJA_OVERRIDE) {
+    $tokens = @($env:LLVM_NINJA_OVERRIDE.Trim() -split '\s+')
+    $script:NinjaCommand  = $tokens[0]
+    [string[]]$script:NinjaExtraArgs = if ($tokens.Count -gt 1) { $tokens[1..($tokens.Count - 1)] } else { @() }
+    Write-Host "Using custom ninja: $($script:NinjaCommand)" -ForegroundColor Cyan
+    if ($script:NinjaExtraArgs.Count -gt 0) {
+        Write-Host "  Extra ninja args: $($script:NinjaExtraArgs -join ' ')" -ForegroundColor Cyan
+    }
+} else {
+    $script:NinjaCommand  = 'ninja'
+    $script:NinjaExtraArgs = @()
+}
+
+# Filter out tests that are known to fail.
+$env:LIT_FILTER_OUT = "gh110231.cpp|crt_initializers.cpp|init-order-atexit.cpp|use_after_return_linkage.cpp|initialization-bug.cpp|initialization-bug-no-global.cpp|trace-malloc-unbalanced.test|trace-malloc-2.test|TraceMallocTest|TestLockFileExclusive"
+
+#===============================================================================
+# Utility functions
+#===============================================================================
+
+function Write-Step {
+    param([string]$Message)
+    Write-Host "`n==== $Message ====`n" -ForegroundColor Cyan
+}
+
+function Write-SubStep {
+    param([string]$Message)
+    Write-Host "  -- $Message" -ForegroundColor DarkCyan
+}
+
+function Write-CMakeCacheFile {
+    <#
+    .SYNOPSIS
+        Writes CMake cache assignments to an initial cache script file to avoid
+        exceeding the Windows command-line length limit.
+    .DESCRIPTION
+        Accepts VAR[:TYPE]=VALUE entries and writes each as a
+        set(VAR VALUE CACHE <type> "") line in a temporary .cmake file.
+        Returns a hashtable with:
+          CacheFile  - the path to the generated file
+          OtherFlags - any non-assignment arguments in the input array
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string[]]$Flags,
+        [string]$FileName = 'cache_flags.cmake'
+    )
+    $cacheLines = @()
+    $otherFlags = @()
+    foreach ($f in $Flags) {
+        if ($f -match '^([A-Za-z_][A-Za-z0-9_]*)(?::([^=]*))?=(.*)$') {
+            $varName  = $Matches[1]
+            $varType  = if ($Matches[2]) { $Matches[2] } else { 'STRING' }
+            $varValue = ($Matches[3] -replace '"', '').Replace('\', '/')   # strip quotes, use forward slashes
+            $cacheLines += "set($varName `"$varValue`" CACHE $varType `"`" FORCE)"
+        } else {
+            $otherFlags += $f
+        }
+    }
+    $cachePath = (Join-Path $PWD $FileName).Replace('\', '/')
+    $cacheLines -join "`n" | Set-Content -Path $cachePath -Encoding UTF8
+    return @{
+        CacheFile  = $cachePath
+        OtherFlags = $otherFlags
+    }
+}
+
+function Invoke-NativeCommand {
+    <#
+    .SYNOPSIS
+        Runs a native command and throws on non-zero exit code.
+    .NOTES
+        This is intentionally a simple function (no [Parameter()] attributes
+        and no [CmdletBinding()]) so that PowerShell does NOT inject common
+        parameters (-Confirm, -OutVariable, -ErrorAction, etc.).  Those
+        common parameters collide with native flags like -C, -O, -E, etc.
+    #>
+    $Command = $args[0]
+    $Arguments = @()
+    if ($args.Count -gt 1) {
+        $Arguments = @($args[1..($args.Count - 1)])
+    }
+    Write-Host "+ $Command $($Arguments -join ' ')" -ForegroundColor DarkGray
+    & $Command @Arguments
+    if ($LASTEXITCODE -ne 0) {
+        throw "Command failed with exit code ${LASTEXITCODE}: $Command $($Arguments -join ' ')"
+    }
+}
+
+function Test-ExecutableVersion {
+    param([string]$Path)
+
+    try {
+        & $Path --version *> $null
+        return $LASTEXITCODE -eq 0
+    } catch {
+        return $false
+    }
+}
+
+function Get-ForwardSlashPath {
+    <#
+    .SYNOPSIS
+        Convert backslashes to forward slashes (CMake expects this for compiler paths).
+    #>
+    param([string]$Path)
+    return $Path.Replace('\', '/')
+}
+
+function Test-FileChecksum {
+    <#
+    .SYNOPSIS
+        Verifies a downloaded file's hash and throws if it doesn't match.
+    #>
+    param(
+        [Parameter(Mandatory)][string]$Path,
+        [Parameter(Mandatory)][string]$Algorithm,
+        [Parameter(Mandatory)][string]$ExpectedHash
+    )
+    $actual = (Get-FileHash -Path $Path -Algorithm $Algorithm).Hash
+    if ($actual -ne $ExpectedHash.ToUpperInvariant()) {
+        throw "Checksum mismatch for ${Path}:`n  expected $ExpectedHash`n  actual   $actual"
+    }
+    Write-SubStep "Checksum OK ($Algorithm): $Path"
+}
+
+#===============================================================================
+# Version detection
+#===============================================================================
+
+function Get-LLVMVersionFromSource {
+    <#
+    .SYNOPSIS
+        Extracts LLVM version from cmake/Modules/LLVMVersion.cmake.
+    #>
+    param([string]$SourceDir)
+    $versionFile = Join-Path $SourceDir 'cmake' 'Modules' 'LLVMVersion.cmake'
+    if (-not (Test-Path $versionFile)) {
+        throw "Cannot find LLVMVersion.cmake at: $versionFile"
+    }
+    $content = Get-Content $versionFile -Raw
+    $major = if ($content -match 'LLVM_VERSION_MAJOR\s+(\d+)') { $Matches[1] } else { throw "Cannot parse LLVM_VERSION_MAJOR" }
+    $minor = if ($content -match 'LLVM_VERSION_MINOR\s+(\d+)') { $Matches[1] } else { throw "Cannot parse LLVM_VERSION_MINOR" }
+    $patch = if ($content -match 'LLVM_VERSION_PATCH\s+(\d+)') { $Matches[1] } else { throw "Cannot parse LLVM_VERSION_PATCH" }
+    $suffix = if ($content -match 'LLVM_VERSION_SUFFIX\s+(\S+)\)') { $Matches[1] } else { '' }
+    # Don't include the "git" development suffix in the release version
+    if ($suffix -eq 'git') { $suffix = '' }
+    return @{
+        Major = $major
+        Minor = $minor
+        Patch = $patch
+        Suffix = $suffix
+        Full = "${major}.${minor}.${patch}${suffix}"
+    }
+}
+
+#===============================================================================
+# Prerequisite validation
+#===============================================================================
+
+function Find-Wix314Bin {
+    $binDirs = @(
+        "${env:ProgramFiles(x86)}\WiX Toolset v3.14\bin"
+        "$env:ProgramFiles\WiX Toolset v3.14\bin"
+    )
+    $binDirs += @(Get-Command candle -All -CommandType Application -ErrorAction SilentlyContinue |
+        ForEach-Object { Split-Path -Parent $_.Source })
+
+    foreach ($binDir in ($binDirs | Select-Object -Unique)) {
+        $candlePath = Join-Path $binDir 'candle.exe'
+        $lightPath = Join-Path $binDir 'light.exe'
+        if (-not (Test-Path -LiteralPath $candlePath -PathType Leaf) -or
+            -not (Test-Path -LiteralPath $lightPath -PathType Leaf)) { continue }
+        if ((Get-Item -LiteralPath $candlePath).VersionInfo.FileVersion -notlike '3.14.*' -or
+            (Get-Item -LiteralPath $lightPath).VersionInfo.FileVersion -notlike '3.14.*') {
+            continue
+        }
+        try {
+            & $candlePath -? *> $null
+            if ($LASTEXITCODE -ne 0) { continue }
+            & $lightPath -? *> $null
+            if ($LASTEXITCODE -eq 0) { return $binDir }
+        } catch {
+            # Try the next WiX installation location.
+        }
+    }
+    return $null
+}
+
+function Find-SevenZipExecutable {
+    $candidates = @()
+    $command = Get-Command 7z -CommandType Application -ErrorAction SilentlyContinue
+    if ($command) { $candidates += $command.Source }
+    foreach ($programFiles in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
+        if ($programFiles) {
+            $candidates += Join-Path $programFiles '7-Zip\7z.exe'
+        }
+    }
+
+    foreach ($candidate in ($candidates | Select-Object -Unique)) {
+        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
+        try {
+            & $candidate -? *> $null
+            if ($LASTEXITCODE -ne 0) { continue }
+        } catch {
+            continue
+        }
+
+        # An existing installation may not have added 7z.exe to PATH.
+        if (-not $command -or $command.Source -ne $candidate) {
+            $binDir = Split-Path -Parent $candidate
+            $otherPaths = @($env:PATH -split ';' | Where-Object { $_ -and $_ -ne $binDir })
+            $env:PATH = (@($binDir) + $otherPaths) -join ';'
+            Write-SubStep "Using 7-Zip from: $binDir"
+        }
+        return $candidate
+    }
+    return $null
+}
+
+function Find-MakeExecutable {
+    $candidates = @()
+    $command = Get-Command make -CommandType Application -ErrorAction SilentlyContinue
+    if ($command) { $candidates += $command.Source }
+    foreach ($programFiles in @(${env:ProgramFiles(x86)}, $env:ProgramFiles)) {
+        if ($programFiles) {
+            $candidates += Join-Path $programFiles 'GnuWin32\bin\make.exe'
+        }
+    }
+
+    foreach ($candidate in ($candidates | Select-Object -Unique)) {
+        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf) -or
+            -not (Test-ExecutableVersion -Path $candidate)) { continue }
+
+        if (-not $command -or $command.Source -ne $candidate) {
+            $binDir = Split-Path -Parent $candidate
+            $otherPaths = @($env:PATH -split ';' | Where-Object { $_ -and $_ -ne $binDir })
+            $env:PATH = (@($binDir) + $otherPaths) -join ';'
+            Write-SubStep "Using GNU Make from: $binDir"
+        }
+        return $candidate
+    }
+    return $null
+}
+
+function Find-OfficialLlvmBin {
+    $binDir = Join-Path $env:ProgramFiles 'LLVM\bin'
+    $clangCl = Join-Path $binDir 'clang-cl.exe'
+    $lldLink = Join-Path $binDir 'lld-link.exe'
+    if ((Test-Path -LiteralPath $clangCl -PathType Leaf) -and
+        (Test-Path -LiteralPath $lldLink -PathType Leaf) -and
+        (Test-ExecutableVersion -Path $clangCl) -and
+        (Test-ExecutableVersion -Path $lldLink)) {
+        return $binDir
+    }
+    return $null
+}
+
+function Get-MissingPrerequisites {
+    <#
+    .SYNOPSIS
+        Applies PATH fixups, checks required build tools, and returns the
+        names of any that are missing.
+    #>
+    Write-Step "Validating prerequisites"
+
+    # Fix up PATH for tools that install to well-known non-PATH locations.
+    $pathFixups = @(
+        "$env:ProgramFiles\Git\usr\bin"                     # sh, bash, sed, grep (needed by tests)
+        "${env:ProgramFiles(x86)}\GnuWin32\bin"             # make (GnuWin32 default)
+        "$env:ProgramFiles\GnuWin32\bin"                    # make (alt location)
+        "$env:ProgramFiles\CMake\bin"                       # cmake
+    )
+    foreach ($dir in $pathFixups) {
+        if ((Test-Path $dir) -and ($env:PATH -notlike "*$dir*")) {
+            $env:PATH = "$env:PATH;$dir"
+            Write-SubStep "Added to PATH: $dir"
+        }
+    }
+
+    # CPack finds WiX through PATH. An older candle/light pair can appear
+    # before the installed 3.14 tools (for example, in an Unreal SDK).
+    $wixBin = Find-Wix314Bin
+    if ($wixBin) {
+        $otherPaths = @($env:PATH -split ';' | Where-Object { $_ -and $_ -ne $wixBin })
+        $env:PATH = (@($wixBin) + $otherPaths) -join ';'
+        Write-SubStep "Using WiX 3.14 from: $wixBin"
+    }
+
+    # Check all required tools.
+    $required = @(
+        @{ Name = "CMake";      Cmd = "cmake" }
+        @{ Name = "Ninja";      Cmd = "ninja" }
+        @{ Name = "Python";     Cmd = "python" }
+        @{ Name = "7-Zip";      Cmd = "7z" }
+        @{ Name = "WiX candle"; Cmd = "candle" }
+        @{ Name = "WiX light";  Cmd = "light" }
+        @{ Name = "Git";        Cmd = "git" }
+        @{ Name = "SWIG";       Cmd = "swig" }
+        @{ Name = "Perl";       Cmd = "perl" }
+        @{ Name = "Make";       Cmd = "make" }
+    )
+
+    $missing = @()
+    foreach ($tool in $required) {
+        $cmd = Get-Command $tool.Cmd -ErrorAction SilentlyContinue
+        $toolPath = if ($cmd) { $cmd.Source } else { $null }
+        # Windows may expose a python.exe App Execution Alias that is not an
+        # installed interpreter.
+        if ($cmd -and $tool.Cmd -eq 'python' -and
+            -not (Test-ExecutableVersion -Path $cmd.Source)) {
+            $toolPath = $null
+        }
+        if ($tool.Cmd -eq '7z') {
+            $toolPath = Find-SevenZipExecutable
+        }
+        if ($tool.Cmd -eq 'make') {
+            $toolPath = Find-MakeExecutable
+        }
+        if ($tool.Cmd -in @('candle', 'light') -and -not $wixBin) {
+            $toolPath = $null
+        }
+        if ($toolPath) {
+            Write-SubStep "$($tool.Name): $toolPath"
+        } else {
+            $missing += $tool.Name
+        }
+    }
+
+    if (-not $ForceMSVC) {
+        $llvmBin = Find-OfficialLlvmBin
+        if ($llvmBin) {
+            Write-SubStep "LLVM release (clang-cl and lld-link): $llvmBin"
+        } else {
+            $missing += 'LLVM release (clang-cl and lld-link)'
+        }
+    }
+
+    # Lit disables the Python user site when running tests. Check the Python
+    # installation itself so timeouts and tests requiring them stay available.
+    $hostPython = Get-PythonExecutableForArch -Arch 'amd64'
+    if ($hostPython) {
+        if (Test-PythonPsutil -PythonExecutable $hostPython) {
+            Write-SubStep "Python psutil: $hostPython"
+        } else {
+            $missing += 'Python psutil'
+        }
+    }
+    if ($arm64) {
+        $arm64Python = Get-PythonExecutableForArch -Arch 'arm64'
+        if ($arm64Python -and $arm64Python -ne $hostPython) {
+            if (Test-PythonPsutil -PythonExecutable $arm64Python) {
+                Write-SubStep "Python psutil (ARM64): $arm64Python"
+            } else {
+                $missing += 'Python psutil (ARM64)'
+            }
+        }
+    }
+
+    if (Test-NetFx3Installed) {
+        Write-SubStep 'NetFx3 (.NET Framework 3.5): installed'
+    } else {
+        $missing += 'NetFx3 (.NET Framework 3.5)'
+    }
+
+    return $missing
+}
+
+function Assert-SevenZipSymlinkSupport {
+    # 7-Zip 21+ extracts symlinks from LLVM's source archive. Windows must
+    # allow symlink creation when -DownloadSource is selected.
+    $banner = (& 7z 2>&1 | Select-Object -First 2) -join ' '
+    if ($banner -notmatch '7-Zip\s+(\d+)\.' -or [int]$Matches[1] -lt 21) {
+        return
+    }
+
+    $linkPath = Join-Path ([IO.Path]::GetTempPath()) ("llvm-release-" + [guid]::NewGuid())
+    try {
+        New-Item -ItemType SymbolicLink -Path $linkPath -Target $PWD.Path -ErrorAction Stop | Out-Null
+    } catch {
+        throw "7-Zip 21+ needs permission to create symlinks when extracting the source archive. Run elevated, enable Developer Mode, or use 7-Zip 20.x."
+    } finally {
+        Remove-Item -LiteralPath $linkPath -Force -ErrorAction SilentlyContinue
+    }
+}
+
+#===============================================================================
+# Prerequisite installation (for fresh VMs)
+#===============================================================================
+
+function Test-IsElevated {
+    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
+    $principal = [Security.Principal.WindowsPrincipal]::new($identity)
+    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+function Test-NetFx3Installed {
+    # The .NET Framework 3.5 installer records this value on both Windows
+    # Server and desktop Windows. WiX 3.14 needs the feature to run.
+    $key = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5'
+    $framework = Get-ItemProperty -LiteralPath $key -ErrorAction SilentlyContinue
+    return $null -ne $framework -and $framework.Install -eq 1
+}
+
+function Install-NetFx3 {
+    if (Test-NetFx3Installed) {
+        Write-SubStep 'NetFx3 (.NET Framework 3.5) is already installed.'
+        return
+    }
+
+    Write-SubStep 'Installing NetFx3 (.NET Framework 3.5)...'
+    $installationType = (Get-ItemProperty `
+        -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').InstallationType
+    if ($installationType -like 'Server*') {
+        # Windows Server exposes NetFx3 as the NET-Framework-Core server role.
+        # Use the built-in Windows PowerShell to load ServerManager reliably,
+        # even when this release script is running under PowerShell 7.
+        $windowsPowerShell = Join-Path $env:SystemRoot `
+            'System32\WindowsPowerShell\v1.0\powershell.exe'
+        $serverCommand = @'
+$ErrorActionPreference = 'Stop'
+Import-Module ServerManager
+$feature = Get-WindowsFeature -Name NET-Framework-Core
+if (-not $feature) { throw 'NET-Framework-Core is not available on this server.' }
+if (-not $feature.Installed) {
+    $result = Install-WindowsFeature -Name NET-Framework-Core
+    if (-not $result.Success) {
+        throw "Install-WindowsFeature failed: $($result.ExitCode)"
+    }
+    if ($result.RestartNeeded -eq 'Yes') { exit 3010 }
+}
+'@
+        $encodedCommand = [Convert]::ToBase64String(
+            [Text.Encoding]::Unicode.GetBytes($serverCommand))
+        & $windowsPowerShell -NoProfile -NonInteractive -EncodedCommand $encodedCommand
+        if ($LASTEXITCODE -eq 3010) {
+            throw 'NetFx3 installation requires a restart. Restart Windows and rerun the release script.'
+        }
+        if ($LASTEXITCODE -ne 0) {
+            throw "NetFx3 installation failed (exit code $LASTEXITCODE). If Windows cannot find the feature files, install NET-Framework-Core from matching Windows Server media and rerun the release script."
+        }
+    } else {
+        $result = Enable-WindowsOptionalFeature -Online -FeatureName NetFx3 `
+            -All -NoRestart -ErrorAction Stop
+        if ($result.RestartNeeded) {
+            throw 'NetFx3 installation requires a restart. Restart Windows and rerun the release script.'
+        }
+    }
+
+    if (-not (Test-NetFx3Installed)) {
+        throw 'NetFx3 installation completed, but .NET Framework 3.5 is not registered as installed.'
+    }
+    Write-SubStep 'NetFx3 installed.'
+}
+
+function Update-PathAfterPrerequisites {
+    # The elevated installer runs in a separate process, so refresh the
+    # caller's PATH from the machine and user environment after it exits.
+    $machinePath = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
+    $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
+    $env:PATH = "$machinePath;$userPath"
+}
+
+function Invoke-ElevatedPrerequisiteInstallation {
+    $scriptPath = $script:ReleaseScriptPath.Replace("'", "''")
+    $invocation = "& '$scriptPath' -InstallPrerequisites -PrerequisitesOnly"
+    if ($arm64) { $invocation += ' -arm64' }
+    if ($ForceMSVC) { $invocation += ' -ForceMSVC' }
+    # Keep the elevated window open so the user can review the result.
+    $workerLines = @(
+        '$exitCode = 0'
+        'try {'
+        "    $invocation"
+        '} catch {'
+        '    Write-Host "Prerequisite installation failed:" -ForegroundColor Red'
+        '    Write-Host ($_ | Out-String) -ForegroundColor Red'
+        '    $exitCode = 1'
+        '}'
+    )
+    if (-not $Unattended) {
+        $workerLines += "Read-Host 'Press Enter to close this window' | Out-Null"
+    }
+    $workerLines += 'exit $exitCode'
+    $workerCommand = $workerLines -join "`n"
+    $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($workerCommand))
+
+    Write-SubStep 'Requesting administrator access to install prerequisites...'
+    try {
+        $process = Start-Process -FilePath (Join-Path $PSHOME 'pwsh.exe') `
+            -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encodedCommand) `
+            -Verb RunAs -Wait -PassThru
+    } catch {
+        throw "Could not start elevated prerequisite installation: $_"
+    }
+    if ($process.ExitCode -ne 0) {
+        throw "Elevated prerequisite installation failed (exit code $($process.ExitCode))."
+    }
+}
+
+function Find-WingetExecutable {
+    $candidates = @()
+    $winget = Get-Command winget -CommandType Application -ErrorAction SilentlyContinue
+    if ($winget) { $candidates += $winget.Source }
+    if ($env:LOCALAPPDATA) {
+        $candidates += Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\winget.exe'
+    }
+
+    # On fresh Windows Server images the App Installer alias may be absent
+    # from the elevated PATH. Discover the installed package directly too.
+    try {
+        $packages = @(Get-AppxPackage -AllUsers -Name Microsoft.DesktopAppInstaller -ErrorAction Stop)
+        foreach ($package in ($packages | Sort-Object Version -Descending)) {
+            if ($package.InstallLocation) {
+                $candidates += Join-Path $package.InstallLocation 'winget.exe'
+            }
+        }
+    } catch {
+        # Appx discovery is not available on every Windows image.
+    }
+
+    foreach ($candidate in ($candidates | Select-Object -Unique)) {
+        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
+        try {
+            & $candidate --version *> $null
+            if ($LASTEXITCODE -eq 0) { return $candidate }
+        } catch {
+            # Try the next location.
+        }
+    }
+    throw "winget cannot run in the elevated shell. Checked: $($candidates -join ', '). Ensure App Installer is available to the account used by UAC."
+}
+
+function Assert-WixToolsUsable {
+    $binDir = Find-Wix314Bin
+    if (-not $binDir) {
+        throw 'WiX 3.14 was installed, but candle.exe and light.exe could not run.'
+    }
+    Write-SubStep "WiX tools verified: $binDir"
+}
+
+function Install-Prerequisites {
+    if (-not (Test-IsElevated)) {
+        if ($Unattended) {
+            throw "Unattended prerequisite installation requires administrator privileges. Start an elevated shell before using -Unattended -InstallPrerequisites."
+        }
+        Invoke-ElevatedPrerequisiteInstallation
+        Update-PathAfterPrerequisites
+        Install-PythonPsutilForRequestedArchitectures
+        return
+    }
+
+    Write-Step "Installing prerequisites"
+
+    Install-NetFx3
+    $wingetPath = Find-WingetExecutable
+
+    # Version requirements for the release build:
+    #   - NetFx3 (.NET Framework 3.5) for WiX Toolset 3.14
+    #   - Python 3.13 for all architectures
+    #   - SWIG 4 or newer for LLDB (install the latest available WinGet version)
+    #   - WiX Toolset 3.14 for MSI packaging
+    #   - Perl is needed for the OpenMP run-time
+    #   - GNU Make is needed by LLDB API tests
+    #   - 7-Zip 21.x+ needs permission to create source archive symlinks
+    #   - LLVM.LLVM supplies the stage 0 clang-cl and lld-link tools
+    $tools = @(
+        @{ Name = "cmake";      WingetId = "Kitware.CMake";                 Version = "";            Verify = "cmake" }
+        @{ Name = "ninja";      WingetId = "Ninja-build.Ninja";             Version = "";            Verify = "ninja" }
+        @{ Name = "python3.13"; WingetId = "Python.Python.3.13";            Version = "";            Verify = "python" }
+        @{ Name = "7z";         WingetId = "7zip.7zip";                     Version = "";            Verify = "7z" }
+        @{ Name = "wix";        WingetId = "WiXToolset.WiXToolset";         Version = "3.14.1.8722"; Verify = "candle" }
+        @{ Name = "git";        WingetId = "Git.Git";                       Version = "";            Verify = "git" }
+        @{ Name = "swig";       WingetId = "SWIG.SWIG";                     Version = "";            Verify = "swig" }
+        @{ Name = "perl";       WingetId = "StrawberryPerl.StrawberryPerl"; Version = "";            Verify = "perl" }
+        @{ Name = "make";       WingetId = "GnuWin32.Make";                 Version = "";            Verify = "make" }
+    )
+
+    foreach ($tool in $tools) {
+        $cmd = Get-Command $tool.Verify -ErrorAction SilentlyContinue
+        if ($cmd -and $tool.Verify -eq 'python' -and
+            -not (Test-ExecutableVersion -Path $cmd.Source)) {
+            $cmd = $null
+        }
+        $existingPath = if ($tool.Name -eq 'wix') { Find-Wix314Bin }
+            elseif ($tool.Name -eq '7z') { Find-SevenZipExecutable }
+            elseif ($tool.Name -eq 'make') { Find-MakeExecutable }
+            elseif ($cmd) { $cmd.Source }
+        if ($existingPath) {
+            Write-SubStep "$($tool.Name) is already installed: $existingPath"
+        } else {
+            $versionArg = if ($tool.Version) { @("--version", $tool.Version) } else { @() }
+            $versionLabel = if ($tool.Version) { " $($tool.Version)" } else { '' }
+            Write-SubStep "Installing $($tool.Name) ($($tool.WingetId)$versionLabel)..."
+            $installArgs = @('install', '--id', $tool.WingetId) + $versionArg +
+                @('--accept-package-agreements', '--accept-source-agreements', '--silent')
+            & $wingetPath @installArgs
+            $installExitCode = $LASTEXITCODE
+            if ($tool.Name -eq 'wix' -and $installExitCode -eq -1978334960) {
+                # winget can fail while checking its NetFx3 dependency on
+                # Server 2025 even after the feature is installed. The feature
+                # was installed and verified above; bypass only this check.
+                Write-Warning 'winget could not verify the installed NetFx3 feature. Retrying WiX with --force.'
+                & $wingetPath @installArgs --force
+                $installExitCode = $LASTEXITCODE
+            }
+            if ($tool.Name -eq '7z') {
+                $sevenZipPath = Find-SevenZipExecutable
+                if ($sevenZipPath) {
+                    if ($installExitCode -ne 0) {
+                        Write-Warning "winget exited with code $installExitCode, but 7-Zip is usable at $sevenZipPath. Continuing."
+                    }
+                    Write-SubStep "7-Zip verified: $sevenZipPath"
+                    continue
+                }
+            }
+            if ($tool.Name -eq 'make') {
+                $makePath = Find-MakeExecutable
+                if ($makePath) {
+                    if ($installExitCode -ne 0) {
+                        Write-Warning "winget exited with code $installExitCode, but GNU Make is usable at $makePath. Continuing."
+                    }
+                    Write-SubStep "GNU Make verified: $makePath"
+                    continue
+                }
+            }
+            if ($installExitCode -ne 0) {
+                if ($tool.Name -eq 'wix') {
+                    throw "Failed to install WiX via winget (exit code $installExitCode). NetFx3 is installed; inspect winget's error above."
+                }
+                throw "Failed to install $($tool.Name) via winget (exit code $installExitCode)."
+            }
+            if ($tool.Name -eq '7z') {
+                throw '7-Zip installation finished, but 7z.exe could not be found or run.'
+            }
+            if ($tool.Name -eq 'make') {
+                throw 'GNU Make installation finished, but make.exe could not be found or run.'
+            }
+            if ($tool.Name -eq 'wix') { Assert-WixToolsUsable }
+        }
+    }
+
+    if (-not $ForceMSVC) {
+        $llvmBin = Find-OfficialLlvmBin
+        if ($llvmBin) {
+            Write-SubStep "LLVM release is already installed: $llvmBin"
+        } else {
+            Write-SubStep 'Installing LLVM release (LLVM.LLVM)...'
+            & $wingetPath install --id LLVM.LLVM --exact --source winget `
+                --accept-package-agreements --accept-source-agreements --silent
+            $installExitCode = $LASTEXITCODE
+            $llvmBin = Find-OfficialLlvmBin
+            if (-not $llvmBin) {
+                throw "LLVM installation did not provide usable clang-cl.exe and lld-link.exe in $env:ProgramFiles\LLVM\bin (winget exit code $installExitCode)."
+            }
+            if ($installExitCode -ne 0) {
+                Write-Warning "winget exited with code $installExitCode, but LLVM tools are usable at $llvmBin. Continuing."
+            }
+            Write-SubStep "LLVM release verified: $llvmBin"
+        }
+    }
+
+    # Install per-architecture Python for cross-arch builds (LLDB needs matching Python).
+    # The main Python install above covers the host architecture (typically x64).
+    $archPython = @()
+    if ($arm64) { $archPython += @{ Arch = 'arm64'; Suffix = '-arm64' } }
+    foreach ($ap in $archPython) {
+        $basePath = "$env:LOCALAPPDATA\Programs\Python"
+        $existing = if (Test-Path $basePath) {
+            Get-ChildItem -Path $basePath -Directory -Filter "Python3*$($ap.Suffix)" |
+                Where-Object { Test-Path (Join-Path $_.FullName 'python.exe') }
+        } else { $null }
+        if ($existing) {
+            Write-SubStep "Python for $($ap.Arch) already installed: $($existing[0].FullName)"
+        } else {
+            Write-SubStep "Installing Python for $($ap.Arch) (Python.Python.3.13 --architecture $($ap.Arch))..."
+            & $wingetPath install --id Python.Python.3.13 --architecture $($ap.Arch) --accept-package-agreements --accept-source-agreements --silent
+            if ($LASTEXITCODE -ne 0) {
+                Write-Warning "Failed to install Python for $($ap.Arch). LLDB may not work for this architecture."
+            }
+        }
+    }
+
+    # Visual Studio Build Tools - special handling
+    Install-VisualStudio -WingetPath $wingetPath
+
+    # Get-MissingPrerequisites will add non-standard paths such as GnuWin32.
+    Update-PathAfterPrerequisites
+    Install-PythonPsutilForRequestedArchitectures
+}
+
+function Install-VisualStudio {
+    param([Parameter(Mandatory)][string]$WingetPath)
+
+    # Components required by LLVM:
+    #   - VCTools workload: core C++ compiler, linker, libs
+    #   - ATL: required by llvm/include/llvm/DebugInfo/PDB/DIA/DIASupport.h (<atlbase.h>)
+    #   - MFC: commonly needed alongside ATL
+    #   - DIA SDK: PDB debug info reader, checked by cmake/config-ix.cmake (LLVM_ENABLE_DIA_SDK)
+    #     The DIA SDK is part of the "Visual Studio C++ core features" component.
+    #
+    # Component IDs reference:
+    #   https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools
+
+    $requiredComponents = @(
+        "Microsoft.VisualStudio.Workload.VCTools"
+        "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
+        "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
+        "Microsoft.VisualStudio.Component.VC.ATL"
+        "Microsoft.VisualStudio.Component.VC.ATL.ARM64"
+        "Microsoft.VisualStudio.Component.VC.ATLMFC"
+        "Microsoft.VisualStudio.Component.VC.ATLMFC.ARM64"
+        "Microsoft.VisualStudio.Component.VC.DiagnosticTools"  # includes DIA SDK
+        # The latest Windows SDK is included automatically via --includeRecommended
+    )
+
+    $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
+    if (Test-Path $vswhere) {
+        $vs = & $vswhere -nologo -latest -products '*' -format json | ConvertFrom-Json
+        if ($vs) {
+            Write-SubStep "Visual Studio already installed: $($vs[0].installationPath)"
+            Write-SubStep "Ensuring required components are installed..."
+            # setup.exe modify does not accept --wait; wait for the process
+            # from PowerShell instead.
+            $addArgs = @()
+            foreach ($component in $requiredComponents) {
+                $addArgs += '--add'
+                $addArgs += $component
+            }
+            $installer = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe"
+            if (-not (Test-Path -LiteralPath $installer -PathType Leaf)) {
+                throw "Visual Studio Installer not found: $installer"
+            }
+            $installPath = $vs[0].installationPath
+            $installArgs = @('modify', '--installPath', "`"$installPath`"") +
+                $addArgs + @('--includeRecommended', '--passive', '--norestart')
+            $process = Start-Process -FilePath $installer -ArgumentList $installArgs -Wait -PassThru
+            if ($process.ExitCode -eq 3010) {
+                throw 'Visual Studio components were installed, but Windows must be restarted before building.'
+            }
+            if ($process.ExitCode -ne 0) {
+                throw "Failed to modify Visual Studio installation (exit code $($process.ExitCode))."
+            }
+            return
+        }
+    }
+
+    Write-SubStep "Installing Visual Studio Build Tools 2022..."
+    $addArgs = ($requiredComponents | ForEach-Object { "--add $_" }) -join ' '
+    & $WingetPath install Microsoft.VisualStudio.2022.BuildTools `
+        --accept-package-agreements --accept-source-agreements --silent `
+        --override "$addArgs --includeRecommended --passive --wait"
+    if ($LASTEXITCODE -ne 0) {
+        throw "Failed to install Visual Studio Build Tools."
+    }
+}
+
+#===============================================================================
+# Visual Studio detection
+#===============================================================================
+
+function Find-VisualStudio {
+    <#
+    .SYNOPSIS
+        Finds the Visual Studio installation and returns the path to VsDevCmd.bat.
+    #>
+
+    if ($env:VSINSTALLDIR) {
+        Write-SubStep "Using enabled Visual Studio installation: $env:VSINSTALLDIR"
+        $vsInstall = $env:VSINSTALLDIR
+    } else {
+        $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
+        if (-not (Test-Path $vswhere)) {
+            throw "Cannot find vswhere.exe. Is Visual Studio installed?"
+        }
+        $vsInstall = & $vswhere -nologo -latest -products '*' -all -property installationPath
+        if (-not $vsInstall) {
+            throw "Cannot find any Visual Studio installation."
+        }
+        Write-SubStep "Detected Visual Studio: $vsInstall"
+    }
+
+    $vsDevCmd = Join-Path $vsInstall 'Common7' 'Tools' 'VsDevCmd.bat'
+    if (-not (Test-Path $vsDevCmd)) {
+        throw "Cannot find VsDevCmd.bat at: $vsDevCmd"
+    }
+    return $vsDevCmd
+}
+
+function Enter-VsDevEnvironment {
+    <#
+    .SYNOPSIS
+        Sources VsDevCmd.bat and imports the resulting environment variables into PowerShell.
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string]$VsDevCmd,
+        [Parameter(Mandatory)]
+        [string]$Arch
+    )
+    Write-SubStep "Setting up VS developer environment for $Arch..."
+    # Run VsDevCmd.bat in a cmd subprocess and capture the resulting environment
+    $envBlock = cmd /c "`"$VsDevCmd`" -arch=$Arch -no_logo && set" 2>&1
+    if ($LASTEXITCODE -ne 0) {
+        throw "VsDevCmd.bat failed for $Arch (exit code $LASTEXITCODE)."
+    }
+    # Reimport the environment variables from the previous command into the current shell.
+    foreach ($line in $envBlock) {
+        if ($line -match '^([^=]+)=(.*)$') {
+            [Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], "Process")
+        }
+    }
+}
+
+function Use-DiaSdkRuntime {
+    param([Parameter(Mandatory)][string]$Arch)
+
+    if (-not $env:VSINSTALLDIR) {
+        throw 'VsDevCmd did not set VSINSTALLDIR; cannot locate the DIA SDK runtime.'
+    }
+    # LLVM loads msdia140.dll by name when COM registration is unavailable.
+    # Put the target architecture's DLL before other copies on PATH so an x86
+    # DLL cannot be loaded by the x64 or arm64 test tools.
+    $diaBin = Join-Path $env:VSINSTALLDIR "DIA SDK\bin\$Arch"
+    $diaDll = Join-Path $diaBin 'msdia140.dll'
+    if (-not (Test-Path -LiteralPath $diaDll -PathType Leaf)) {
+        throw "DIA runtime not found: $diaDll. Install the Visual Studio Diagnostic Tools component."
+    }
+    $otherPaths = @($env:PATH -split ';' | Where-Object { $_ -and $_ -ne $diaBin })
+    $env:PATH = (@($diaBin) + $otherPaths) -join ';'
+    Write-SubStep "Using DIA runtime: $diaDll"
+}
+
+#===============================================================================
+# Python setup
+#===============================================================================
+
+function Test-PythonPsutil {
+    param([Parameter(Mandatory)][string]$PythonExecutable)
+
+    try {
+        # Lit clears PYTHONPATH and disables the user site. -I performs the
+        # same checks and also avoids accidentally importing from the source tree.
+        & $PythonExecutable -I -c 'import psutil' *> $null
+        return $LASTEXITCODE -eq 0
+    } catch {
+        return $false
+    }
+}
+
+function Install-PythonPsutil {
+    param([Parameter(Mandatory)][string]$PythonExecutable)
+
+    if (Test-PythonPsutil -PythonExecutable $PythonExecutable) {
+        Write-SubStep "Python psutil is already installed: $PythonExecutable"
+        return
+    }
+
+    Write-SubStep "Installing Python psutil into $PythonExecutable..."
+    & $PythonExecutable -I -m pip --version *> $null
+    if ($LASTEXITCODE -ne 0) {
+        & $PythonExecutable -I -m ensurepip --upgrade
+        if ($LASTEXITCODE -ne 0) {
+            throw "Could not bootstrap pip for $PythonExecutable."
+        }
+    }
+    & $PythonExecutable -I -m pip install --disable-pip-version-check --no-input psutil
+    if ($LASTEXITCODE -ne 0 -or -not (Test-PythonPsutil -PythonExecutable $PythonExecutable)) {
+        throw "Could not install psutil into $PythonExecutable. Lit requires it for test timeouts."
+    }
+    Write-SubStep "Python psutil verified: $PythonExecutable"
+}
+
+function Install-PythonPsutilForRequestedArchitectures {
+    $hostPython = Get-PythonExecutableForArch -Arch 'amd64'
+    if (-not $hostPython) {
+        throw 'Python was installed, but its executable could not be found to install psutil.'
+    }
+    $pythonExecutables = @($hostPython)
+    if ($arm64) {
+        $arm64Python = Get-PythonExecutableForArch -Arch 'arm64'
+        if ($arm64Python) { $pythonExecutables += $arm64Python }
+    }
+    foreach ($pythonExecutable in ($pythonExecutables | Select-Object -Unique)) {
+        Install-PythonPsutil -PythonExecutable $pythonExecutable
+    }
+}
+
+function Get-PythonInstallDir {
+    param([Parameter(Mandatory)][string]$PythonCommand)
+
+    # Python Manager and winget can put only a shim in PATH. CMake needs the
+    # actual installation directory, which sys.exec_prefix reports.
+    $prefix = & $PythonCommand -c 'import sys; print(sys.exec_prefix)'
+    if ($LASTEXITCODE -ne 0 -or -not $prefix) {
+        throw "Could not find the Python installation behind $PythonCommand"
+    }
+    $installDir = ([string]$prefix).Trim()
+    if (-not (Test-Path (Join-Path $installDir 'python.exe'))) {
+        throw "Python reported $installDir, but python.exe is missing there."
+    }
+    return $installDir
+}
+
+function Test-PythonIsArm64 {
+    param([Parameter(Mandatory)][string]$PythonExecutable)
+
+    try {
+        $machine = & $PythonExecutable -c 'import platform; print(platform.machine())'
+        return $LASTEXITCODE -eq 0 -and ([string]$machine).Trim() -eq 'ARM64'
+    } catch {
+        return $false
+    }
+}
+
+function Get-PythonExecutableForArch {
+    param([Parameter(Mandatory)][ValidateSet('amd64', 'arm64')][string]$Arch)
+
+    if ($Arch -eq 'amd64') {
+        $python = Get-Command python -ErrorAction SilentlyContinue
+        if (-not $python -or -not (Test-ExecutableVersion -Path $python.Source)) {
+            return $null
+        }
+        try {
+            $pythonHome = Get-PythonInstallDir -PythonCommand $python.Source
+            return Join-Path $pythonHome 'python.exe'
+        } catch {
+            return $null
+        }
+    }
+
+    $basePath = "$env:LOCALAPPDATA\Programs\Python"
+    if (Test-Path $basePath) {
+        $candidates = @(Get-ChildItem -Path $basePath -Directory -Filter 'Python3*-arm64' |
+            Where-Object { Test-Path (Join-Path $_.FullName 'python.exe') } |
+            Sort-Object Name -Descending)
+        if ($candidates.Count -gt 0) {
+            return Join-Path $candidates[0].FullName 'python.exe'
+        }
+    }
+    return $null
+}
+
+function Find-Python {
+    <#
+    .SYNOPSIS
+        Locates a Python installation for the given target architecture.
+
+        For x64 (amd64) builds, uses the system Python from PATH.
+        For ARM64 builds, probes standard install locations for any ARM64
+        Python 3.x.  Falls back to the PATH Python with a warning if no
+        ARM64 install is found.
+    #>
+    param(
+        [Parameter(Mandatory)][string]$Arch
+    )
+
+    $pythonHome = $null
+
+    if ($Arch -eq 'amd64') {
+        # Host-arch build: use system Python from PATH.
+        $pythonExecutable = Get-PythonExecutableForArch -Arch 'amd64'
+        if (-not $pythonExecutable) {
+            throw "Cannot find python in PATH. Run with -InstallPrerequisites or install Python manually."
+        }
+        $pythonHome = Split-Path -Parent $pythonExecutable
+    } else {
+        # Cross-arch build: probe standard per-arch install locations.
+        # Python installs to %LOCALAPPDATA%\Programs\Python\Python3XX-arm64
+        $suffix = '-arm64'
+        $basePath = "$env:LOCALAPPDATA\Programs\Python"
+        $pythonExecutable = Get-PythonExecutableForArch -Arch 'arm64'
+        if ($pythonExecutable) { $pythonHome = Split-Path -Parent $pythonExecutable }
+
+        if (-not $pythonHome) {
+            # Fall back to PATH Python with a warning.
+            $pythonExecutable = Get-PythonExecutableForArch -Arch 'amd64'
+            if ($pythonExecutable) {
+                $pythonHome = Split-Path -Parent $pythonExecutable
+                # On a native ARM64 machine (e.g. CI) the PATH Python is
+                # already the right one.
+                if (Test-PythonIsArm64 -PythonExecutable $pythonExecutable) {
+                    Write-SubStep "PATH Python is already ARM64: $pythonHome"
+                } else {
+                    Write-Warning ("No per-arch Python found for $Arch (looked in $basePath\Python3*$suffix).`n" +
+                        "  Falling back to PATH Python: $pythonHome`n" +
+                        "  LLDB may not work correctly for the $Arch target.`n" +
+                        "  Run with -InstallPrerequisites -$Arch to install the correct Python.")
+                }
+            } else {
+                throw ("Cannot find Python for $Arch. No per-arch install in $basePath and no python in PATH.`n" +
+                    "Run with -InstallPrerequisites to install Python.")
+            }
+        }
+    }
+
+    # Display the version without returning it alongside the Python directory.
+    Invoke-NativeCommand (Join-Path $pythonHome 'python.exe') --version | Out-Host
+    $env:PYTHONHOME = $pythonHome
+    $env:PATH = "$pythonHome;$env:PATH"
+    Write-SubStep "Using Python from: $pythonHome"
+    return $pythonHome
+}
+
+#===============================================================================
+# SWIG setup
+#===============================================================================
+
+function Find-Swig {
+    <#
+    .SYNOPSIS
+        Locates SWIG's real executable and library directory.
+    #>
+    $command = Get-Command swig.exe -CommandType Application -ErrorAction Stop
+    $executable = $command.Source
+    $item = Get-Item -LiteralPath $executable -ErrorAction Stop
+
+    # WinGet can put a symlink to swig.exe in PATH. SWIG may look for its
+    # library beside that link rather than beside the installed executable.
+    $linkType = $item.PSObject.Properties['LinkType']
+    if ($linkType -and $linkType.Value -eq 'SymbolicLink') {
+        $targetProperty = $item.PSObject.Properties['Target']
+        $target = if ($targetProperty) { $targetProperty.Value } else { $null }
+        if ($target -is [array]) { $target = $target[0] }
+        if (-not $target) {
+            throw "Cannot resolve SWIG symlink: $executable"
+        }
+        if (-not [IO.Path]::IsPathRooted($target)) {
+            $target = Join-Path $item.DirectoryName $target
+        }
+        $executable = (Resolve-Path -LiteralPath $target -ErrorAction Stop).Path
+    }
+
+    $reportedLibDir = Invoke-NativeCommand $executable -swiglib
+    if ($reportedLibDir -is [array]) { $reportedLibDir = $reportedLibDir[-1] }
+    $libDir = ([string]$reportedLibDir).Trim()
+    if (-not $libDir -or -not (Test-Path -LiteralPath (Join-Path $libDir 'swig.swg') -PathType Leaf)) {
+        $libDir = Join-Path (Split-Path -Parent $executable) 'Lib'
+    }
+    if (-not (Test-Path -LiteralPath (Join-Path $libDir 'swig.swg') -PathType Leaf)) {
+        throw "SWIG library swig.swg was not found for $executable. Check the SWIG installation."
+    }
+
+    Write-SubStep "Using SWIG: $executable (library: $libDir)"
+    return @{ Executable = $executable; LibraryDirectory = $libDir }
+}
+
+#===============================================================================
+# Build helpers
+#===============================================================================
+
+# Suppress warnings in the downloaded dependencies with -w; clang, clang-cl,
+# and cl all accept it.
+function Build-LibXml2 {
+    <#
+    .SYNOPSIS
+        Builds libxml2 and sets $script:LibXmlInstallDir to the install path.
+    .NOTES
+        Uses a script-scoped variable instead of return to avoid PowerShell
+        capturing native command stdout as part of the function's return value,
+        which would break Ninja's \r progress display and corrupt the path.
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string]$SourceDir,
+        [Parameter(Mandatory)]
+        [string[]]$ToolchainFlags
+    )
+    Write-SubStep "Building libxml2..."
+    $libxmlBuild = 'libxmlbuild'
+    New-Item -ItemType Directory -Path $libxmlBuild -Force | Out-Null
+    Push-Location $libxmlBuild
+    try {
+        $libxmlFlags = $ToolchainFlags + @(
+            "CMAKE_BUILD_TYPE=Release"
+            "CMAKE_C_FLAGS=-w"
+            "CMAKE_INSTALL_PREFIX=$(Join-Path $PWD 'install')"
+            "BUILD_SHARED_LIBS=OFF"
+            "LIBXML2_WITH_C14N=OFF"
+            "LIBXML2_WITH_CATALOG=OFF"
+            "LIBXML2_WITH_DEBUG=OFF"
+            "LIBXML2_WITH_DOCB=OFF"
+            "LIBXML2_WITH_FTP=OFF"
+            "LIBXML2_WITH_HTML=OFF"
+            "LIBXML2_WITH_HTTP=OFF"
+            "LIBXML2_WITH_ICONV=OFF"
+            "LIBXML2_WITH_ICU=OFF"
+            "LIBXML2_WITH_ISO8859X=OFF"
+            "LIBXML2_WITH_LEGACY=OFF"
+            "LIBXML2_WITH_LZMA=OFF"
+            "LIBXML2_WITH_MEM_DEBUG=OFF"
+            "LIBXML2_WITH_MODULES=OFF"
+            "LIBXML2_WITH_OUTPUT=ON"
+            "LIBXML2_WITH_PATTERN=OFF"
+            "LIBXML2_WITH_PROGRAMS=OFF"
+            "LIBXML2_WITH_PUSH=OFF"
+            "LIBXML2_WITH_PYTHON=OFF"
+            "LIBXML2_WITH_READER=OFF"
+            "LIBXML2_WITH_REGEXPS=OFF"
+            "LIBXML2_WITH_RUN_DEBUG=OFF"
+            "LIBXML2_WITH_SAX1=ON"
+            "LIBXML2_WITH_SCHEMAS=OFF"
+            "LIBXML2_WITH_SCHEMATRON=OFF"
+            "LIBXML2_WITH_TESTS=OFF"
+            "LIBXML2_WITH_THREADS=ON"
+            "LIBXML2_WITH_THREAD_ALLOC=OFF"
+            "LIBXML2_WITH_TREE=ON"
+            "LIBXML2_WITH_VALID=OFF"
+            "LIBXML2_WITH_WRITER=OFF"
+            "LIBXML2_WITH_XINCLUDE=OFF"
+            "LIBXML2_WITH_XPATH=OFF"
+            "LIBXML2_WITH_XPTR=OFF"
+            "LIBXML2_WITH_ZLIB=OFF"
+            "CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded"
+        )
+        $cache = Write-CMakeCacheFile -Flags $libxmlFlags -FileName 'libxml2_cache.cmake'
+        $otherFlags = $cache.OtherFlags
+        Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags $SourceDir
+        Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs install
+        $script:LibXmlInstallDir = Get-ForwardSlashPath (Join-Path $PWD 'install')
+    } finally {
+        Pop-Location
+    }
+}
+
+function Build-Zlib {
+    <#
+    .SYNOPSIS
+        Builds zlib and sets $script:ZlibInstallDir to the install path.
+    .NOTES
+        See Build-LibXml2 for why a script-scoped variable is used instead of return.
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string]$SourceDir,
+        [Parameter(Mandatory)]
+        [string[]]$ToolchainFlags
+    )
+    Write-SubStep "Building zlib..."
+    $zlibBuild = 'zlibbuild'
+    New-Item -ItemType Directory -Path $zlibBuild -Force | Out-Null
+    Push-Location $zlibBuild
+    try {
+        $zlibFlags = $ToolchainFlags + @(
+            "CMAKE_BUILD_TYPE=Release"
+            "CMAKE_C_FLAGS=-w"
+            "CMAKE_INSTALL_PREFIX=$(Join-Path $PWD 'install')"
+            "ZLIB_BUILD_TESTING=OFF"
+            "ZLIB_BUILD_SHARED=OFF"
+            "ZLIB_BUILD_STATIC=ON"
+            "ZLIB_INSTALL=ON"
+            "CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded"
+        )
+        $cache = Write-CMakeCacheFile -Flags $zlibFlags -FileName 'zlib_cache.cmake'
+        $otherFlags = $cache.OtherFlags
+        Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags $SourceDir
+        Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs install
+        $script:ZlibInstallDir = Get-ForwardSlashPath (Join-Path $PWD 'install')
+    } finally {
+        Pop-Location
+    }
+}
+
+function Build-Zstd {
+    <#
+    .SYNOPSIS
+        Builds zstd and sets $script:ZstdInstallDir to the install path.
+    .NOTES
+        See Build-LibXml2 for why a script-scoped variable is used instead of return.
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string]$SourceDir,
+        [Parameter(Mandatory)]
+        [string[]]$ToolchainFlags
+    )
+    Write-SubStep "Building zstd..."
+    $zstdBuild = 'zstdbuild'
+    New-Item -ItemType Directory -Path $zstdBuild -Force | Out-Null
+    Push-Location $zstdBuild
+    try {
+        $zstdFlags = $ToolchainFlags + @(
+            "CMAKE_BUILD_TYPE=Release"
+            "CMAKE_C_FLAGS=-w"
+            "CMAKE_CXX_FLAGS=-w"
+            "CMAKE_INSTALL_PREFIX=$(Join-Path $PWD 'install')"
+            "ZSTD_BUILD_PROGRAMS=ON"
+            "ZSTD_BUILD_TESTS=OFF"
+            "ZSTD_BUILD_STATIC=ON"
+            "ZSTD_BUILD_SHARED=OFF"
+            # zstd's own CMakeLists.txt declares cmake_minimum_required(VERSION 3.10),
+            # below the 3.15 that introduced CMP0091 -- so it defaults to OLD, and
+            # CMAKE_MSVC_RUNTIME_LIBRARY below is silently ignored, leaving zstd on
+            # CMake's traditional default (/MD, dynamic CRT). That mismatches the
+            # rest of this build, which LLVM_ENABLE_RPMALLOC forces to /MT (see
+            # LLVM_ENABLE_RPMALLOC's handling in llvm/CMakeLists.txt) -- producing
+            # LNK4217 warnings ("locally defined symbol imported") for malloc/
+            # calloc/free/etc. when zstd_static.lib links into llvm.exe. Forcing the
+            # policy default (independent of zstd's own cmake_minimum_required)
+            # makes CMAKE_MSVC_RUNTIME_LIBRARY actually take effect.
+            "CMAKE_POLICY_DEFAULT_CMP0091=NEW"
+            "CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded"
+        )
+        $cache = Write-CMakeCacheFile -Flags $zstdFlags -FileName 'zstd_cache.cmake'
+        $otherFlags = $cache.OtherFlags
+        Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags "$SourceDir/build/cmake"
+        Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs install
+        $script:ZstdInstallDir = Get-ForwardSlashPath (Join-Path $PWD 'install')
+    } finally {
+        Pop-Location
+    }
+}
+
+function New-PGOProfile {
+    <#
+    .SYNOPSIS
+        Builds and trains an instrumented stage 2 clang and sets the profile path.
+    .NOTES
+        Uses a script-scoped variable instead of return to avoid PowerShell
+        capturing native command stdout as part of the function's return value,
+        which would break Ninja's \r progress display and corrupt the path.
+
+        PGO artifacts are created as top-level siblings of the stage 2 build
+        directory so that each step owns an independent directory tree:
+          $InstrumentDir/   - instrumented stage 2 clang build
+          $TrainDir/        - training build
+          $ProfilePath      - merged profile data
+    #>
+    param(
+        [string[]]$CMakeFlags,
+        [string]$Stage1BinDir,
+        [string]$LlvmSrc,
+        [Parameter(Mandatory)][string]$InstrumentDir,
+        [Parameter(Mandatory)][string]$TrainDir,
+        [Parameter(Mandatory)][string]$ProfilePath
+    )
+    Write-Step "Generating PGO profile for stage 2 self-hosted compiler"
+
+    # Build instrumented stage 2 Clang
+    Write-SubStep "Building instrumented stage 2 Clang..."
+    New-Item -ItemType Directory -Path $InstrumentDir -Force | Out-Null
+    Push-Location $InstrumentDir
+    try {
+        # The instrumented build only needs to produce an instrumented clang;
+        # strip runtimes/projects that are unnecessary and would themselves be
+        # compiled with -fprofile-generate (requiring the profile runtime to link).
+        $instrumentFlags = $CMakeFlags + @(
+            "LLVM_TARGETS_TO_BUILD=Native"
+            "LLVM_BUILD_INSTRUMENTED=IR"
+            'LLVM_ENABLE_RUNTIMES=""'
+            'LLVM_ENABLE_PROJECTS="clang;lld"'
+        )
+        $cache = Write-CMakeCacheFile -Flags $instrumentFlags -FileName 'instrument_cache.cmake'
+        $otherFlags = $cache.OtherFlags
+        Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags "$LlvmSrc/llvm"
+        Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs clang
+        $instrumentedClang = Get-ForwardSlashPath (Join-Path $PWD 'bin' 'clang-cl.exe')
+    } finally {
+        Pop-Location
+    }
+
+    # Train: build part of LLVM with the instrumented stage 2 compiler
+    Write-SubStep "Training with instrumented stage 2 Clang..."
+    New-Item -ItemType Directory -Path $TrainDir -Force | Out-Null
+    Push-Location $TrainDir
+    try {
+        $trainFlags = $CMakeFlags + @(
+            "CMAKE_C_COMPILER=$instrumentedClang"
+            "CMAKE_CXX_COMPILER=$instrumentedClang"
+            "LLVM_ENABLE_PROJECTS=clang"
+            'LLVM_ENABLE_RUNTIMES=""'
+            "LLVM_TARGETS_TO_BUILD=Native"
+        )
+        $cache = Write-CMakeCacheFile -Flags $trainFlags -FileName 'train_cache.cmake'
+        $otherFlags = $cache.OtherFlags
+        Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags "$LlvmSrc/llvm"
+
+        # Drop profiles generated from running cmake; those are not representative.
+        Remove-Item -Path "$InstrumentDir/profiles/*.profraw" -Force -ErrorAction SilentlyContinue
+        Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs 'tools/clang/lib/Sema/CMakeFiles/obj.clangSema.dir/Sema.cpp.obj'
+    } finally {
+        Pop-Location
+    }
+
+    # Merge profiles
+    $resolvedProfilePath = Get-ForwardSlashPath $ProfilePath
+    Invoke-NativeCommand "$Stage1BinDir/llvm-profdata" merge `
+        -output="$resolvedProfilePath" "$InstrumentDir/profiles/*.profraw"
+
+    Write-SubStep "PGO profile generated: $resolvedProfilePath"
+    $script:PGOProfilePath = $resolvedProfilePath
+}
+
+function Invoke-Build {
+    param([string[]]$Targets = @())
+    Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs @Targets
+}
+
+function Invoke-Tests {
+    <#
+    .SYNOPSIS
+        Runs a list of test targets once each, skipping as appropriate.
+        Honors $script:NinjaCommand and $script:NinjaExtraArgs (set via
+        LLVM_NINJA_OVERRIDE).
+    #>
+    param(
+        [string[]]$Targets,
+        [string]$Arch = ''
+    )
+    foreach ($target in $Targets) {
+        # Skip runtime checks on non-amd64
+        if ($target -eq 'check-runtimes' -and $Arch -ne 'amd64') {
+            Write-SubStep "Skipping $target on $Arch"
+            continue
+        }
+        Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs $target
+    }
+}
+
+function Assert-MsiUpgradeCode {
+    param([Parameter(Mandatory)][string]$BuildDirectory)
+
+    # This permanent UpgradeCode must match llvm/CMakeLists.txt.
+    $expected = 'B08613CD-8BD0-4FB6-8937-621936604DE3'
+    $msiFiles = @(Get-ChildItem -LiteralPath $BuildDirectory -Filter '*.msi' -File)
+    if ($msiFiles.Count -ne 1) {
+        throw "Expected one MSI in $BuildDirectory, found $($msiFiles.Count)."
+    }
+
+    # Query Windows Installer directly; dark.exe is absent from newer WiX.
+    $installer = New-Object -ComObject WindowsInstaller.Installer
+    $database = $installer.OpenDatabase($msiFiles[0].FullName, 0)
+    $view = $database.OpenView("SELECT Value FROM Property WHERE Property='UpgradeCode'")
+    $view.Execute()
+    $record = $view.Fetch()
+    if (-not $record) {
+        throw "Could not read the UpgradeCode from $($msiFiles[0].FullName)."
+    }
+    $actual = $record.StringData(1).Trim([char[]]'{}')
+    if ($actual -ine $expected) {
+        throw "Unexpected MSI UpgradeCode $actual in $($msiFiles[0].FullName); expected $expected."
+    }
+    Write-SubStep "Verified MSI UpgradeCode: $expected"
+}
+
+function Assert-MsiFileDeduplication {
+    param([Parameter(Mandatory)][string]$BuildDirectory)
+
+    $msiFiles = @(Get-ChildItem -LiteralPath $BuildDirectory -Filter '*.msi' -File)
+    if ($msiFiles.Count -ne 1) {
+        throw "Expected one MSI in $BuildDirectory, found $($msiFiles.Count)."
+    }
+
+    $installer = New-Object -ComObject WindowsInstaller.Installer
+    $database = $installer.OpenDatabase($msiFiles[0].FullName, 0)
+    $view = $database.OpenView('SELECT DestName FROM DuplicateFile')
+    $view.Execute()
+    $copies = @()
+    while ($record = $view.Fetch()) {
+        $copies += $record.StringData(1)
+    }
+    $view.Close()
+
+    # Clang and LLD create these aliases through llvm_install_symlink. The
+    # release MSI must create them from one payload instead of storing copies.
+    foreach ($alias in @('clang-cl.exe', 'lld-link.exe')) {
+        if ($alias -notin $copies) {
+            throw "MSI is missing the DuplicateFile entry for $alias."
+        }
+    }
+    Write-SubStep "Verified MSI file deduplication: $($copies.Count) copied filenames"
+}
+
+#===============================================================================
+# Main build stages
+#===============================================================================
+
+function Build-Stage {
+    <#
+    .SYNOPSIS
+        Builds the stage 1 bootstrap or stage 2 self-hosted compiler.
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string]$Name,
+        [Parameter(Mandatory)]
+        [string]$Description,
+        [Parameter(Mandatory)]
+        [string[]]$CMakeFlags,
+        [Parameter(Mandatory)]
+        [string]$LlvmSrc,
+        [string[]]$ExtraCMakeFlags = @(),
+        [string[]]$InitialBuildTargets = @(),
+        [switch]$SkipFullBuild,
+        [string[]]$TestTargets = @(),
+        [string]$Arch = ''
+    )
+
+    Write-Step "Building $Description ($Name)"
+    New-Item -ItemType Directory -Path $Name -Force | Out-Null
+    Push-Location $Name
+    try {
+        $allFlags = $CMakeFlags + $ExtraCMakeFlags
+        $cache = Write-CMakeCacheFile -Flags $allFlags
+        $otherFlags = $cache.OtherFlags
+        Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags "$LlvmSrc/llvm"
+        if ($InitialBuildTargets.Count -gt 0) {
+            Invoke-Build -Targets $InitialBuildTargets
+        }
+        if (-not $SkipFullBuild) {
+            Invoke-Build
+            if ($TestTargets.Count -gt 0) {
+                Invoke-Tests -Targets $TestTargets -Arch $Arch
+            }
+        }
+    } finally {
+        Pop-Location
+    }
+}
+
+function Build-Architecture {
+    <#
+    .SYNOPSIS
+        Selects the stage 0 host toolchain, then builds the stage 1 bootstrap
+        and stage 2 self-hosted compilers for an architecture.
+    #>
+    param(
+        [Parameter(Mandatory)]
+        [string]$Arch,
+        [Parameter(Mandatory)]
+        [string]$VsDevCmd,
+        [Parameter(Mandatory)]
+        [string]$LlvmSrc,
+        [Parameter(Mandatory)]
+        [string]$BuildDir,
+        [Parameter(Mandatory)]
+        [string]$ThirdPartyDir,
+        [Parameter(Mandatory)]
+        [string]$PackageVersion,
+        [Parameter(Mandatory)]
+        [string[]]$CommonCMakeFlags,
+        [Parameter(Mandatory)]
+        [string]$CommonCompilerFlags,
+        [Parameter(Mandatory)]
+        [string[]]$Stage2CMakeFlags,
+        [string[]]$CommonLLDBFlags = @(),
+        [switch]$UseFastBuild
+    )
+
+    Write-Step "Building for architecture: $Arch"
+
+    # Restore clean PATH and setup environment
+    $env:PATH = $script:OriginalPath
+    $pythonHome = Find-Python -Arch $Arch
+    $pythonExecutable = Join-Path $pythonHome 'python.exe'
+    if (-not (Test-PythonPsutil -PythonExecutable $pythonExecutable)) {
+        throw "Python psutil is missing from $pythonExecutable. Run with -InstallPrerequisites to enable lit test timeouts."
+    }
+    Enter-VsDevEnvironment -VsDevCmd $VsDevCmd -Arch $Arch
+    Use-DiaSdkRuntime -Arch $Arch
+
+    # Stage 0 uses the official LLVM release unless MSVC was requested.
+    $hostClangCl = $null
+    $hostLldLink = $null
+    if (-not $ForceMSVC) {
+        $llvmBin = Find-OfficialLlvmBin
+        if (-not $llvmBin) {
+            throw 'Official LLVM clang-cl.exe and lld-link.exe are missing. Run with -InstallPrerequisites, or use -ForceMSVC.'
+        }
+        $env:PATH = "$llvmBin;$env:PATH"
+        $hostClangCl = Join-Path $llvmBin 'clang-cl.exe'
+        $hostLldLink = Join-Path $llvmBin 'lld-link.exe'
+    }
+
+    if ($hostClangCl) {
+        $hostCompiler = $hostClangCl
+        $hostLinker = $hostLldLink
+        $CommonCompilerFlags += ' -fuse-ld=lld'
+        $CommonCMakeFlags += @(
+            "LLVM_ENABLE_LLD=ON"
+            "CMAKE_C_FLAGS=`"$CommonCompilerFlags`""
+            "CMAKE_CXX_FLAGS=`"$CommonCompilerFlags`""
+        )
+    } else {
+        # Pin the MSVC driver so bcrypt.lib is interpreted as a library.
+        $hostCompiler = 'cl.exe'
+        $hostLinker = 'link.exe'
+    }
+    Write-SubStep "Stage 0 host compiler: $hostCompiler (linker: $hostLinker)"
+
+    $hostToolchainFlags = @(
+        "CMAKE_C_COMPILER=$hostCompiler"
+        "CMAKE_CXX_COMPILER=$hostCompiler"
+        "CMAKE_LINKER=$hostLinker"
+    )
+    $CommonCMakeFlags += $hostToolchainFlags
+
+    $env:VSCMD_START_DIR = $BuildDir
+    $swigConfig = Find-Swig
+    # CMake and Ninja subprocesses need the library when invoking SWIG.
+    $env:SWIG_LIB = $swigConfig.LibraryDirectory
+
+    # Directory names (always computed, used by multiple steps)
+    $stage1Name = "build_${Arch}_stage1"
+    $stage2Name = "build_${Arch}_stage2"
+    $instrumentDir = Join-Path $BuildDir "instrument_${Arch}_stage2"
+    $trainDir      = Join-Path $BuildDir "train_${Arch}_stage2"
+    $profileFile   = Join-Path $BuildDir "profile_${Arch}_stage2.profdata"
+
+    #-------------------------------------------------------------------
+    # Step: libxml2
+    #-------------------------------------------------------------------
+    if (Test-ShouldRun 'libxml2') {
+        # Every step that runs starts from a clean directory, so artifacts of
+        # a previous run cannot leak into this one.
+        Remove-StepDirectory (Join-Path $BuildDir $stage1Name 'libxmlbuild')
+        Remove-StepDirectory (Join-Path $BuildDir $stage1Name 'zlibbuild')
+        Remove-StepDirectory (Join-Path $BuildDir $stage1Name 'zstdbuild')
+        New-Item -ItemType Directory -Path $stage1Name -Force | Out-Null
+        Push-Location $stage1Name
+        try {
+            Build-LibXml2 -SourceDir (Join-Path $ThirdPartyDir 'libxml2-v2.15.3') -ToolchainFlags $hostToolchainFlags
+            Build-Zlib -SourceDir (Join-Path $ThirdPartyDir 'zlib-1.3.2') -ToolchainFlags $hostToolchainFlags
+            Build-Zstd -SourceDir (Join-Path $ThirdPartyDir 'zstd-1.5.7') -ToolchainFlags $hostToolchainFlags
+        } finally {
+            Pop-Location
+        }
+    } else {
+        Write-Step "Skipping libxml2 (-StartAt $($script:StartAtStep))"
+        $libxmlInstallPath = Join-Path $BuildDir $stage1Name 'libxmlbuild' 'install'
+        $zlibInstallPath = Join-Path $BuildDir $stage1Name 'zlibbuild' 'install'
+        $zstdInstallPath = Join-Path $BuildDir $stage1Name 'zstdbuild' 'install'
+        Assert-PathExists -Path $libxmlInstallPath -Description 'libxml2 install directory'
+        Assert-PathExists -Path $zlibInstallPath -Description 'zlib install directory'
+        Assert-PathExists -Path $zstdInstallPath -Description 'zstd install directory'
+        $script:LibXmlInstallDir = Get-ForwardSlashPath $libxmlInstallPath
+        $script:ZlibInstallDir = Get-ForwardSlashPath $zlibInstallPath
+        $script:ZstdInstallDir = Get-ForwardSlashPath $zstdInstallPath
+    }
+    $libxmlDir = $script:LibXmlInstallDir
+    $zlibDir = $script:ZlibInstallDir
+    $zstdDir = $script:ZstdInstallDir
+
+    # The stage 1 bootstrap tools are needed by PGO and stage 2.
+    $stage1BinDir = Get-ForwardSlashPath (Join-Path $BuildDir $stage1Name 'bin')
+
+    # Compute cmakeFlags (always needed, cheap -- derived from parameters)
+    $cmakeFlags = $CommonCMakeFlags + @(
+        "Python3_ROOT_DIR=$env:PYTHONHOME"
+        "LIBXML2_INCLUDE_DIR=$libxmlDir/include/libxml2"
+        "LIBXML2_LIBRARY=$libxmlDir/lib/libxml2s.lib"
+        "LIBXML2_LIBRARIES=$libxmlDir/lib/libxml2s.lib;ws2_32.lib"
+        "ZLIB_INCLUDE_DIR=$zlibDir/include"
+        "ZLIB_LIBRARY=$zlibDir/lib/zs.lib"
+        "ZLIB_LIBRARY_RELEASE=$zlibDir/lib/zs.lib"
+        "zstd_INCLUDE_DIR=$zstdDir/include"
+        "zstd_LIBRARY=$zstdDir/lib/zstd_static.lib"
+    )
+
+    $cmakeFlags += "CLANG_DEFAULT_LINKER=lld"
+    if ($Arch -eq 'arm64') {
+        $cmakeFlags += "COMPILER_RT_BUILD_SANITIZERS=OFF"
+    }
+
+    #-------------------------------------------------------------------
+    # Step: stage 1 bootstrap compiler
+    #-------------------------------------------------------------------
+    if (Test-ShouldRun 'stage1') {
+        # Preserve the dependency installs needed by stage 1.
+        $stage1Dir = Join-Path $BuildDir $stage1Name
+        if (Test-Path $stage1Dir) {
+            Write-SubStep "Cleaning stage 1 bootstrap build artifacts (preserving dependency installs)..."
+            Get-ChildItem -Path $stage1Dir -Exclude 'libxmlbuild', 'zlibbuild', 'zstdbuild' |
+                Remove-Item -Recurse -Force
+        }
+
+        # Stage 2 needs these tools even when the fast build skips the rest
+        # of the stage 1 bootstrap build and its tests.
+        $stage1InitialTargets = @('clang', 'lld', 'llvm-lib', 'llvm-rc', 'runtimes')
+        if ($Arch -eq 'amd64') {
+            $stage1InitialTargets += 'llvm-ml64'
+        }
+        $skipStage1FullBuild = $UseFastBuild
+        $stage1Tests = @('check-llvm', 'check-clang', 'check-lld', 'check-runtimes')
+        $stage1Extra = @("LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON",
+                         "LLVM_TARGETS_TO_BUILD=Native")
+
+        Build-Stage -Name $stage1Name -Description 'stage 1 bootstrap compiler' `
+            -CMakeFlags $cmakeFlags `
+            -LlvmSrc $LlvmSrc `
+            -ExtraCMakeFlags $stage1Extra `
+            -InitialBuildTargets $stage1InitialTargets `
+            -SkipFullBuild:$skipStage1FullBuild `
+            -TestTargets $stage1Tests `
+            -Arch $Arch
+    } else {
+        Write-Step "Skipping stage 1 bootstrap compiler (-StartAt $($script:StartAtStep))"
+        Assert-PathExists -Path "$stage1BinDir/clang-cl.exe" -Description 'stage 1 bootstrap clang-cl.exe'
+        Assert-PathExists -Path "$stage1BinDir/lld-link.exe" -Description 'stage 1 bootstrap lld-link.exe'
+        Assert-PathExists -Path "$stage1BinDir/llvm-lib.exe" -Description 'stage 1 bootstrap llvm-lib.exe'
+        Assert-PathExists -Path "$stage1BinDir/llvm-rc.exe" -Description 'stage 1 bootstrap llvm-rc.exe'
+        if ($Arch -eq 'amd64') {
+            Assert-PathExists -Path "$stage1BinDir/llvm-ml64.exe" -Description 'stage 1 bootstrap llvm-ml64.exe'
+        }
+        # -FastBuild does not build llvm-profdata, which the PGO merge needs.
+        if (-not $UseFastBuild -and (Test-ShouldRun 'pgo')) {
+            Assert-PathExists -Path "$stage1BinDir/llvm-profdata.exe" -Description 'stage 1 bootstrap llvm-profdata.exe (not built by -FastBuild)'
+        }
+    }
+
+    # Stage 2 is built with the stage 1 bootstrap toolchain.
+    $stage2CompilerFlags = @(
+        "CMAKE_C_COMPILER=$stage1BinDir/clang-cl.exe"
+        "CMAKE_CXX_COMPILER=$stage1BinDir/clang-cl.exe"
+        "CMAKE_LINKER=$stage1BinDir/lld-link.exe"
+        "CMAKE_AR=$stage1BinDir/llvm-lib.exe"
+        "CMAKE_RC_COMPILER=$stage1BinDir/llvm-rc.exe"
+    )
+
+    if ($Arch -eq 'amd64') {
+        $stage2CompilerFlags += "CMAKE_ASM_MASM_COMPILER=$stage1BinDir/llvm-ml64.exe"
+    } else {
+        $stage2CompilerFlags += "CPACK_SYSTEM_NAME=woa64"
+    }
+
+    $stage2Flags = $cmakeFlags + $stage2CompilerFlags
+
+    #-------------------------------------------------------------------
+    # Step: pgo
+    #-------------------------------------------------------------------
+    $profileFlags = @()
+    if (-not $UseFastBuild) {
+        if (Test-ShouldRun 'pgo') {
+            Remove-StepDirectory $instrumentDir
+            Remove-StepDirectory $trainDir
+            Remove-StepDirectory $profileFile
+            New-PGOProfile -CMakeFlags $stage2Flags `
+                -Stage1BinDir $stage1BinDir -LlvmSrc $LlvmSrc `
+                -InstrumentDir $instrumentDir `
+                -TrainDir $trainDir `
+                -ProfilePath $profileFile
+        } else {
+            Write-Step "Skipping PGO (-StartAt $($script:StartAtStep))"
+            Assert-PathExists -Path $profileFile -Description 'PGO profile data'
+            $script:PGOProfilePath = Get-ForwardSlashPath $profileFile
+        }
+        $profilePath = $script:PGOProfilePath
+
+        $pgoCFlags = "$CommonCompilerFlags -Wno-backend-plugin"
+        $profileFlags = @(
+            "LLVM_PROFDATA_FILE=$profilePath"
+            "CMAKE_C_FLAGS=`"$pgoCFlags`""
+            "CMAKE_CXX_FLAGS=`"$pgoCFlags`""
+        )
+    }
+
+    #-------------------------------------------------------------------
+    # Step: stage 2 self-hosted compiler
+    #-------------------------------------------------------------------
+    $stage2Projects = '"clang;clang-tools-extra;lld;lldb;flang;mlir"'
+
+    $stage2Extra = @(
+        "LLVM_ENABLE_PROJECTS=$stage2Projects"
+        'LLVM_ENABLE_RUNTIMES="compiler-rt;openmp"'
+        "PYTHON_HOME=$env:PYTHONHOME"
+    ) + $CommonLLDBFlags + $Stage2CMakeFlags + @(
+        "SWIG_EXECUTABLE=$($swigConfig.Executable)"
+        "SWIG_DIR=$($swigConfig.LibraryDirectory)"
+    ) + $profileFlags
+
+    if (Test-ShouldRun 'stage2') {
+        # Also discards the settings the tarball step left in the CMake cache.
+        Remove-StepDirectory (Join-Path $BuildDir $stage2Name)
+
+        $stage2Tests = @('check-llvm', 'check-clang', 'check-lld', 'check-runtimes',
+                         'check-clang-tools', 'check-clangd')
+
+        Build-Stage -Name $stage2Name -Description 'stage 2 self-hosted compiler' `
+            -CMakeFlags $stage2Flags `
+            -LlvmSrc $LlvmSrc `
+            -ExtraCMakeFlags $stage2Extra `
+            -TestTargets $stage2Tests `
+            -Arch $Arch
+    } else {
+        Write-Step "Skipping stage 2 self-hosted compiler (-StartAt $($script:StartAtStep))"
+        Assert-PathExists -Path (Join-Path $BuildDir $stage2Name) -Description 'stage 2 build directory'
+    }
+
+    #-------------------------------------------------------------------
+    # Step: package (WiX MSI installer)
+    #-------------------------------------------------------------------
+    if (Test-ShouldRun 'package') {
+        Write-Step "Creating WiX MSI installer package"
+        Push-Location $stage2Name
+        try {
+            # The tarball step reconfigures this shared build directory with
+            # LLVM_INSTALL_TOOLCHAIN_ONLY=OFF. Pin the stage 2 settings again
+            # so a resumed package step still produces the MSI payload.
+            $packageFlags = $stage2Flags + $stage2Extra
+            $cache = Write-CMakeCacheFile -Flags $packageFlags -FileName 'package_cache.cmake'
+            $otherFlags = $cache.OtherFlags
+            Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags "$LlvmSrc/llvm"
+
+            # CPack does not remove stale files from its staging tree, so
+            # clean it after reconfiguring from a possible tarball build.
+            Remove-StepDirectory (Join-Path $PWD '_CPack_Packages')
+
+            Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs package
+            # Downloaded release tags can predate these MSI settings. Check
+            # each invariant only when the source being packaged defines it.
+            $llvmCMakeLists = Join-Path $LlvmSrc 'llvm/CMakeLists.txt'
+            if (Select-String -LiteralPath $llvmCMakeLists -Pattern 'set\(CPACK_WIX_UPGRADE_GUID' -Quiet) {
+                Assert-MsiUpgradeCode -BuildDirectory $PWD.Path
+            }
+            if (Select-String -LiteralPath $llvmCMakeLists -Pattern 'CPACK_WIX_DEDUPLICATION_PATCH_FILE' -Quiet) {
+                Assert-MsiFileDeduplication -BuildDirectory $PWD.Path
+            }
+        } finally {
+            Pop-Location
+        }
+    } else {
+        Write-Step "Skipping package (-StartAt $($script:StartAtStep))"
+    }
+
+    #-------------------------------------------------------------------
+    # Step: tarball
+    #-------------------------------------------------------------------
+    $tripleArch = if ($Arch -eq 'amd64') { 'x86_64' } else { 'aarch64' }
+    $filename = "clang+llvm-${PackageVersion}-${tripleArch}-pc-windows-msvc"
+
+    if (Test-ShouldRun 'tarball') {
+        Remove-StepDirectory (Join-Path $BuildDir $filename)
+        Remove-StepDirectory (Join-Path $BuildDir "$filename.tar")
+        Remove-StepDirectory (Join-Path $BuildDir "$filename.tar.xz")
+
+        Push-Location $stage2Name
+        try {
+            $tarballFlags = $stage2Flags + $stage2Extra + @(
+                "LLVM_INSTALL_TOOLCHAIN_ONLY=OFF"
+                "CMAKE_INSTALL_PREFIX=$BuildDir/$filename"
+                "LLVM_INCLUDE_TESTS=OFF"
+            )
+            $cache = Write-CMakeCacheFile -Flags $tarballFlags -FileName 'tarball_cache.cmake'
+            $otherFlags = $cache.OtherFlags
+            Invoke-NativeCommand cmake -GNinja -C $cache.CacheFile @otherFlags "$LlvmSrc/llvm"
+            Invoke-NativeCommand $script:NinjaCommand @script:NinjaExtraArgs install
+
+            # Verify llvm-config works
+            Invoke-NativeCommand "$BuildDir/$filename/bin/llvm-config.exe" --bindir
+        } finally {
+            Pop-Location
+        }
+
+        # Create the portable archive without changing executable layout.
+        Invoke-NativeCommand 7z a -ttar "$filename.tar" $filename
+        Invoke-NativeCommand 7z a -txz "$filename.tar.xz" "$filename.tar"
+        Remove-Item -LiteralPath "$filename.tar"
+    } else {
+        Write-Step "Skipping tarball (-StartAt $($script:StartAtStep))"
+    }
+}
+
+#===============================================================================
+# Main
+#===============================================================================
+
+if ($Help) {
+    Get-Help $MyInvocation.MyCommand.Path -Detailed
+    exit 0
+}
+
+if ($PrerequisitesOnly -and -not $InstallPrerequisites) {
+    throw '-PrerequisitesOnly requires -InstallPrerequisites.'
+}
+
+# Install prerequisites if requested
+if ($InstallPrerequisites) {
+    Install-Prerequisites
+}
+if ($PrerequisitesOnly) { exit 0 }
+
+# Offer to install missing tools in interactive mode, then validate again.
+$missing = @(Get-MissingPrerequisites)
+if ($missing.Count -gt 0 -and -not $InstallPrerequisites -and -not $Unattended) {
+    Write-Host "Missing prerequisites: $($missing -join ', ')" -ForegroundColor Yellow
+    $answer = Read-Host "Install prerequisites (including NetFx3 and Visual Studio Build Tools if needed)? [y/N]"
+    if ($answer -eq 'y' -or $answer -eq 'Y') {
+        Install-Prerequisites
+        $missing = @(Get-MissingPrerequisites)
+    } else {
+        Write-Host "Prerequisites were not installed. Aborted."
+        exit 1
+    }
+}
+
+if ($missing.Count -gt 0) {
+    Write-Host ""
+    Write-Error ("Missing prerequisites: $($missing -join ', ')`n" +
+        "Run the script with -InstallPrerequisites to install them, or install manually.")
+    exit 1
+}
+
+if ($DownloadSource -and -not $StartAt) {
+    Assert-SevenZipSymlinkSupport
+}
+Write-SubStep "All prerequisites found."
+
+# Detect Visual Studio
+$vsDevCmd = Find-VisualStudio
+
+# Determine LLVM source directory
+if ($DownloadSource) {
+    $llvmSrc = $null  # Set after the build directory is known.
+} else {
+    $llvmSrc = Resolve-Path (Join-Path $PSScriptRoot '..\..\..') | Select-Object -ExpandProperty Path
+}
+
+# Auto-detect or validate version
+if ($Version) {
+    $packageVersion = $Version
+    Write-Host "Using specified version: $packageVersion"
+} else {
+    if ($llvmSrc) {
+        $versionInfo = Get-LLVMVersionFromSource -SourceDir $llvmSrc
+        $packageVersion = $versionInfo.Full
+        Write-Host "Auto-detected LLVM version from source: $packageVersion"
+    } else {
+        # -DownloadSource without -Version: try detecting from the repo the script lives in.
+        $repoRoot = Join-Path $PSScriptRoot '..\..\..'
+        if (Test-Path (Join-Path $repoRoot 'cmake' 'Modules' 'LLVMVersion.cmake')) {
+            $versionInfo = Get-LLVMVersionFromSource -SourceDir (Resolve-Path $repoRoot)
+            $packageVersion = $versionInfo.Full
+            Write-Host "Auto-detected LLVM version from repo: $packageVersion"
+        } else {
+            Write-Error "Cannot auto-detect version. Use -Version <version> when using -DownloadSource."
+            exit 1
+        }
+    }
+}
+
+$revision = "llvmorg-$packageVersion"
+$buildDir = Join-Path $PWD "llvm_package_$packageVersion"
+$thirdPartyDir = Join-Path $buildDir 'third_party'
+
+Write-Step "Configuration"
+Write-Host "  Revision:        $revision"
+Write-Host "  Package version: $packageVersion"
+Write-Host "  Build dir:       $buildDir"
+Write-Host "  Architectures:   $((@('x64','arm64') | Where-Object { (Get-Variable $_ -ValueOnly) }) -join ', ')"
+
+if ($StartAt) {
+    # Resuming from a specific step: validate that the build directory exists.
+    if (-not (Test-Path $buildDir)) {
+        Write-Error "Build directory does not exist: $buildDir`nCannot use -StartAt without a previous build. Run a full build first."
+        exit 1
+    }
+    Write-Host "  Resuming from step: $StartAt" -ForegroundColor Yellow
+} else {
+    # Full build: prompt to delete existing build directory.
+    if (Test-Path $buildDir) {
+        if ($Unattended) {
+            Write-Error "Build directory already exists: $buildDir`nUse -StartAt to resume it, or remove it before running an unattended full build."
+            exit 1
+        }
+        $answer = Read-Host "Build directory already exists: $buildDir`nDelete and re-create it? [y/N]"
+        if ($answer -ne 'y' -and $answer -ne 'Y') {
+            Write-Host "Aborted."
+            exit 1
+        }
+        Remove-Item -Recurse -Force $buildDir
+    }
+}
+
+New-Item -ItemType Directory -Path $buildDir -Force | Out-Null
+Push-Location $buildDir
+
+try {
+    # Download source if requested (skip when resuming with -StartAt)
+    if ($DownloadSource) {
+        $llvmSrc = Join-Path $buildDir 'llvm-project'
+    }
+    if ($DownloadSource -and -not $StartAt) {
+        Write-Step "Downloading $revision"
+        Invoke-NativeCommand curl.exe -L `
+            "https://github.com/llvm/llvm-project/archive/$revision.zip" -o src.zip
+        Invoke-NativeCommand 7z x src.zip
+        Get-ChildItem -Directory -Filter 'llvm-project-*' |
+            Rename-Item -NewName 'llvm-project'
+    }
+    if ($DownloadSource) {
+        Assert-PathExists -Path $llvmSrc -Description 'downloaded LLVM source directory'
+    }
+
+    # Keep third-party archives and extracted sources together.
+    New-Item -ItemType Directory -Path $thirdPartyDir -Force | Out-Null
+    Push-Location $thirdPartyDir
+    try {
+        # Download and extract libxml2 (skip if resuming and it already exists)
+        if ($StartAt -and (Test-Path 'libxml2-v2.15.3')) {
+            Write-Step "Skipping libxml2 download (already exists)"
+        } else {
+            Write-Step "Downloading libxml2"
+            Invoke-NativeCommand curl.exe --remote-name `
+                'https://gitlab.gnome.org/GNOME/libxml2/-/archive/v2.15.3/libxml2-v2.15.3.tar.gz'
+            Test-FileChecksum -Path 'libxml2-v2.15.3.tar.gz' -Algorithm SHA256 `
+                -ExpectedHash '0DA50C1415F4EC0364569D2119B1436BA837B31DF44AF28569D234272C23CF1F'
+            # The test directory contains symlinks.
+            Invoke-NativeCommand tar zxf 'libxml2-v2.15.3.tar.gz' --exclude 'test/*'
+        }
+
+        # Download and extract zlib (skip if resuming and it already exists)
+        if ($StartAt -and (Test-Path 'zlib-1.3.2')) {
+            Write-Step "Skipping zlib download (already exists)"
+        } else {
+            Write-Step "Downloading zlib"
+            Invoke-NativeCommand curl.exe -LO `
+                'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz'
+            Test-FileChecksum -Path 'zlib-1.3.2.tar.gz' -Algorithm SHA256 `
+                -ExpectedHash 'BB329A0A2CD0274D05519D61C667C062E06990D72E125EE2DFA8DE64F0119D16'
+            Invoke-NativeCommand tar zxf 'zlib-1.3.2.tar.gz'
+        }
+
+        # Download and extract zstd (skip if resuming and it already exists)
+        if ($StartAt -and (Test-Path 'zstd-1.5.7')) {
+            Write-Step "Skipping zstd download (already exists)"
+        } else {
+            Write-Step "Downloading zstd"
+            Invoke-NativeCommand curl.exe -LO `
+                'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz'
+            Test-FileChecksum -Path 'zstd-1.5.7.tar.gz' -Algorithm SHA256 `
+                -ExpectedHash 'EB33E51F49A15E023950CD7825CA74A4A2B43DB8354825AC24FC1B7EE09E6FA3'
+            # 'tests' directory excluded because of symlinks.
+            Invoke-NativeCommand tar zxf 'zstd-1.5.7.tar.gz' --exclude 'tests/*'
+        }
+    } finally {
+        Pop-Location
+    }
+
+    # Preserve original PATH
+    $script:OriginalPath = $env:PATH
+
+    # Common flags
+    $commonCompilerFlags = '-DLIBXML_STATIC -D_SILENCE_NONFLOATING_COMPLEX_DEPRECATION_WARNING'
+    $releaseCompilerFlags = '/O2 /Ob2 /DNDEBUG'
+    # -FastBuild skips ThinLTO to keep the build time down.
+    $ltoFlags = if ($FastBuild) { '' } else { ' -flto=thin -fsplit-lto-unit' }
+    $wpvFlags = if ($FastBuild) { '' } else { ' -fwhole-program-vtables' }
+    $stage2CMakeFlags = @(
+        # Pass the stage 1 bootstrap assembler to OpenMP's runtimes build too.
+        'LLVM_EXTERNAL_PROJECT_PASSTHROUGH=CMAKE_ASM_MASM_COMPILER'
+        # Early CMake probes need ThinLTO, and whole-program vtables require
+        # every ThinLTO unit to use the same splitting mode.
+        "CMAKE_C_FLAGS_RELEASE=$releaseCompilerFlags -fstrict-aliasing$ltoFlags"
+        "CMAKE_CXX_FLAGS_RELEASE=$releaseCompilerFlags -Wno-unused-template -fstrict-aliasing$ltoFlags$wpvFlags"
+    )
+    if (-not $FastBuild) { $stage2CMakeFlags += 'LLVM_ENABLE_LTO=THIN' }
+    $commonCMakeFlags = @(
+        "CMAKE_BUILD_TYPE=Release"
+        "LLVM_ENABLE_ASSERTIONS=OFF"
+        "LLVM_INSTALL_TOOLCHAIN_ONLY=ON"
+        # lit retries each failing test once without rerunning passing tests.
+        "LLVM_LIT_ARGS=-sv --max-retries-per-test=1"
+        'LLVM_TARGETS_TO_BUILD="AArch64;ARM;X86;BPF;WebAssembly;RISCV;NVPTX"'
+        "LLVM_BUILD_LLVM_C_DYLIB=ON"
+        "Python3_FIND_REGISTRY=NEVER"
+        "PACKAGE_VERSION=$packageVersion"
+        'CMAKE_CL_SHOWINCLUDES_PREFIX="Note: including file: "'
+        "LLVM_ENABLE_LIBXML2=FORCE_ON"
+        "CLANG_ENABLE_LIBXML2=OFF"
+        "LLVM_ENABLE_ZLIB=FORCE_ON"
+        "LLVM_ENABLE_ZSTD=FORCE_ON"
+        "CMAKE_C_FLAGS=`"$commonCompilerFlags`""
+        "CMAKE_CXX_FLAGS=`"$commonCompilerFlags`""
+        "CMAKE_CXX_FLAGS_RELEASE=`"$releaseCompilerFlags`""
+        # libxml2 2.15 needs bcrypt on executable and shared links.
+        "CMAKE_EXE_LINKER_FLAGS=bcrypt.lib"
+        "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib"
+        "LLVM_ENABLE_RPMALLOC=ON"
+        'LLVM_ENABLE_PROJECTS="clang;lld"'
+        'LLVM_ENABLE_RUNTIMES="compiler-rt"'
+        "COMPILER_RT_BUILD_ORC=OFF"
+        "LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF"
+        "CMAKE_DISABLE_PRECOMPILE_HEADERS=ON"
+        # Configure WiX explicitly so llvm/CMakeLists.txt sets its MSI
+        # compression, install scope, and permanent UpgradeCode.
+        "CPACK_GENERATOR=WIX"
+    )
+
+    $commonLLDBFlags = @(
+        "LLDB_RELOCATABLE_PYTHON=1"
+        "LLDB_EMBED_PYTHON_HOME=OFF"
+    )
+
+    # Build each requested architecture
+    $buildParams = @{
+        VsDevCmd            = $vsDevCmd
+        LlvmSrc             = $llvmSrc
+        BuildDir            = $buildDir
+        ThirdPartyDir       = $thirdPartyDir
+        PackageVersion      = $packageVersion
+        CommonCMakeFlags    = $commonCMakeFlags
+        CommonCompilerFlags = $commonCompilerFlags
+        Stage2CMakeFlags    = $stage2CMakeFlags
+        CommonLLDBFlags     = $commonLLDBFlags
+        UseFastBuild        = $FastBuild
+    }
+
+    if ($x64)   { Build-Architecture -Arch 'amd64'  @buildParams }
+    if ($arm64) { Build-Architecture -Arch 'arm64'  @buildParams }
+
+    Write-Step "Build complete!"
+    Write-Host "Packages are in: $buildDir" -ForegroundColor Green
+
+} finally {
+    Pop-Location
+    # Restore the console mode that was saved at script start.
+    if ($null -ne $script:SavedConsoleMode) {
+        try { [ConsoleMode]::Set($script:SavedConsoleMode) } catch {}
+    }
+}

>From d35a12d6ae18417f2c90d3dc68b8306424e00035 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Fri, 2 Oct 2026 13:29:36 -0400
Subject: [PATCH 2/8] Update doc

---
 llvm/utils/release/build_llvm_release.ps1 | 59 ++++++++++++-----------
 1 file changed, 32 insertions(+), 27 deletions(-)

diff --git a/llvm/utils/release/build_llvm_release.ps1 b/llvm/utils/release/build_llvm_release.ps1
index 92fc701fff6b8a..23cf5f2e2fe990 100644
--- a/llvm/utils/release/build_llvm_release.ps1
+++ b/llvm/utils/release/build_llvm_release.ps1
@@ -10,7 +10,7 @@
     Can bootstrap a fresh VM by installing all prerequisites.
 
     By default, builds x64 using the local source tree and auto-detected
-    Python.  Use -DownloadSource to download a tagged release tarball
+    Python. Use -DownloadSource to download a tagged release tarball
     instead.
 
     Build steps (in order):
@@ -22,18 +22,18 @@
       6. tarball   - Generate full install tarball
 
 .PARAMETER Version
-    LLVM version string (e.g. "19.1.0").  If omitted, auto-detected from
+    LLVM version string (e.g. "19.1.0"). If omitted, auto-detected from
     the source tree.
 
 .PARAMETER x64
-    Build for x64 (64-bit).  This is the default if no architecture is specified.
+    Build for x64 (64-bit). This is the default if no architecture is specified.
 
 .PARAMETER arm64
     Build for ARM64 (AArch64).
 
 .PARAMETER DownloadSource
     Download and extract the tagged release source tarball from GitHub
-    instead of using the local source tree.  Requires -Version.
+    instead of using the local source tree. Requires -Version.
 
 .PARAMETER ForceMSVC
     Force using MSVC (cl.exe) as the stage 0 host compiler instead of the
@@ -45,20 +45,20 @@
 
 .PARAMETER InstallPrerequisites
     Install build prerequisites (NetFx3, Visual Studio, official LLVM release,
-    CMake, Python and its psutil module, etc.)
-    before building.  When used with -arm64, also installs ARM64 Python for
-    LLDB.  Requests administrator elevation once for the entire prerequisite
+    CMake, Python and its psutil module, etc.) before building.
+    When used with -arm64, also installs ARM64 Python for LLDB.
+    Requests administrator elevation once for the entire prerequisite
     installation when the current shell is not elevated.
 
 .PARAMETER Unattended
-    Never prompt for input.  Missing prerequisites cause the build to fail
-    unless -InstallPrerequisites is also specified.  An existing build
-    directory must be resumed with -StartAt or removed beforehand.  Run
+    Never prompt for input. Missing prerequisites cause the build to fail
+    unless -InstallPrerequisites is also specified. An existing build
+    directory must be resumed with -StartAt or removed beforehand. Run
     from an elevated shell when using -InstallPrerequisites.
 
 .PARAMETER StartAt
-    Resume a previous build from a specific step.  Artifacts from earlier
-    steps must already exist on disk.  The step being restarted gets a
+    Resume a previous build from a specific step. Artifacts from earlier
+    steps must already exist on disk. The step being restarted gets a
     clean directory; earlier steps are skipped entirely.
 
     Valid steps: libxml2, stage1, pgo, stage2, package, tarball
@@ -74,8 +74,8 @@
     Full x64 build using the local source tree.
 
 .EXAMPLE
-    .\build_llvm_release.ps1 -x64 -arm64
-    Build for both x64 and ARM64.
+    .\build_llvm_release.ps1 -arm64
+    Build for ARM64.
 
 .EXAMPLE
     .\build_llvm_release.ps1 -Version 19.1.0 -DownloadSource
@@ -85,10 +85,6 @@
     .\build_llvm_release.ps1 -InstallPrerequisites -x64
     Install prerequisites, then do a full x64 build.
 
-.EXAMPLE
-    .\build_llvm_release.ps1 -InstallPrerequisites -x64 -arm64
-    Install prerequisites (including ARM64 Python), then build both architectures.
-
 .EXAMPLE
     .\build_llvm_release.ps1 -Unattended -InstallPrerequisites
     From an elevated shell, install prerequisites and build without prompts.
@@ -106,13 +102,13 @@
     Display the list of available build steps and exit.
 
 .NOTES
-    Python is auto-detected from PATH for x64 builds.  For ARM64 builds,
-    the script probes standard install locations for ARM64 Python.  Use
+    Python is auto-detected from PATH for x64 builds. For ARM64 builds,
+    the script probes standard install locations for ARM64 Python. Use
     -InstallPrerequisites to install it automatically.
 
     Environment variables:
       LLVM_NINJA_OVERRIDE  - Override the ninja binary and optionally provide
-                             extra flags.  The first token is the executable,
+                             extra flags. The first token is the executable,
                              remaining tokens are prepended to every ninja
                              invocation.
                              Example: LLVM_NINJA_OVERRIDE="myninja.exe --flag1 --flag2"
@@ -136,7 +132,7 @@ param(
 # PowerShell 7 self-relaunch
 #
 # This script requires PowerShell 7+ features (ternary operator, null-
-# coalescing, improved error handling, etc.).  If we detect we are running
+# coalescing, improved error handling, etc.). If we detect we are running
 # under Windows PowerShell 5.x, we attempt to find or install PowerShell 7
 # and re-launch ourselves under it, forwarding all original arguments.
 #===============================================================================
@@ -210,7 +206,7 @@ Set-StrictMode -Version Latest
 $ErrorActionPreference = 'Stop'
 $script:ReleaseScriptPath = $PSCommandPath
 
-# Save the console mode so we can restore it at exit.  Child processes
+# Save the console mode so we can restore it at exit. Child processes
 # (cmake, ninja, link.exe, etc.) sometimes disable virtual terminal
 # processing and don't restore it, which breaks arrow keys and ESC in
 # the parent shell after the script finishes.
@@ -273,7 +269,7 @@ function Test-ShouldRun {
     <#
     .SYNOPSIS
         Returns $true if the given step should execute (i.e. it is at or after
-        the -StartAt step).  When -StartAt is not set, always returns $true.
+        the -StartAt step). When -StartAt is not set, always returns $true.
     #>
     param([Parameter(Mandatory)][string]$Step)
     if (-not $script:StartAtStep) { return $true }
@@ -293,7 +289,7 @@ function Test-IsStartStep {
 function Assert-PathExists {
     <#
     .SYNOPSIS
-        Validates that a path exists on disk.  Used when skipping steps to
+        Validates that a path exists on disk. Used when skipping steps to
         ensure the artifacts from a prior run are still present.
     #>
     param(
@@ -395,7 +391,7 @@ function Invoke-NativeCommand {
     .NOTES
         This is intentionally a simple function (no [Parameter()] attributes
         and no [CmdletBinding()]) so that PowerShell does NOT inject common
-        parameters (-Confirm, -OutVariable, -ErrorAction, etc.).  Those
+        parameters (-Confirm, -OutVariable, -ErrorAction, etc.). Those
         common parameters collide with native flags like -C, -O, -E, etc.
     #>
     $Command = $args[0]
@@ -1270,7 +1266,7 @@ function Find-Python {
 
         For x64 (amd64) builds, uses the system Python from PATH.
         For ARM64 builds, probes standard install locations for any ARM64
-        Python 3.x.  Falls back to the PATH Python with a warning if no
+        Python 3.x. Falls back to the PATH Python with a warning if no
         ARM64 install is found.
     #>
     param(
@@ -2196,6 +2192,14 @@ if ($StartAt) {
 New-Item -ItemType Directory -Path $buildDir -Force | Out-Null
 Push-Location $buildDir
 
+# Ninja's single-line progress display overprints cmake's output and garbles
+# the console on some hosts (notably conhost). TERM=dumb makes ninja print one
+# plain line per step, which also keeps CI logs readable. Windows Terminal
+# (identified by WT_SESSION) renders the progress line correctly, and an
+# explicit TERM set by the caller is respected, so neither is overridden.
+$script:SavedTerm = $env:TERM
+if (-not $env:TERM -and -not $env:WT_SESSION) { $env:TERM = 'dumb' }
+
 try {
     # Download source if requested (skip when resuming with -StartAt)
     if ($DownloadSource) {
@@ -2335,6 +2339,7 @@ try {
 
 } finally {
     Pop-Location
+    $env:TERM = $script:SavedTerm
     # Restore the console mode that was saved at script start.
     if ($null -ne $script:SavedConsoleMode) {
         try { [ConsoleMode]::Set($script:SavedConsoleMode) } catch {}

>From b4196a226c6870841ff3cdd8759646326a93e9a5 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Fri, 2 Oct 2026 17:04:59 -0400
Subject: [PATCH 3/8] Fix missing psutil

---
 .github/workflows/release-binaries.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index db64958052ebf9..c14156c742cffd 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -223,6 +223,10 @@ jobs:
       with:
         python-version: '3.14.7'
 
+    - name: Install Python psutil (Windows)
+      if: runner.os == 'Windows'
+      run: python -m pip install --disable-pip-version-check --no-input psutil
+
     # For some reason this is needed on Windows or else the build system can't find python3.lib.
     - name: Setup Python library path
       if: runner.os == 'Windows'

>From a594295403453f1257af176228376dd830894c2e Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Fri, 2 Oct 2026 17:11:12 -0400
Subject: [PATCH 4/8] Remove MFC requirement

---
 llvm/utils/release/build_llvm_release.ps1 | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/llvm/utils/release/build_llvm_release.ps1 b/llvm/utils/release/build_llvm_release.ps1
index 23cf5f2e2fe990..557d04eac20bff 100644
--- a/llvm/utils/release/build_llvm_release.ps1
+++ b/llvm/utils/release/build_llvm_release.ps1
@@ -1013,7 +1013,6 @@ function Install-VisualStudio {
     # Components required by LLVM:
     #   - VCTools workload: core C++ compiler, linker, libs
     #   - ATL: required by llvm/include/llvm/DebugInfo/PDB/DIA/DIASupport.h (<atlbase.h>)
-    #   - MFC: commonly needed alongside ATL
     #   - DIA SDK: PDB debug info reader, checked by cmake/config-ix.cmake (LLVM_ENABLE_DIA_SDK)
     #     The DIA SDK is part of the "Visual Studio C++ core features" component.
     #
@@ -1026,8 +1025,6 @@ function Install-VisualStudio {
         "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
         "Microsoft.VisualStudio.Component.VC.ATL"
         "Microsoft.VisualStudio.Component.VC.ATL.ARM64"
-        "Microsoft.VisualStudio.Component.VC.ATLMFC"
-        "Microsoft.VisualStudio.Component.VC.ATLMFC.ARM64"
         "Microsoft.VisualStudio.Component.VC.DiagnosticTools"  # includes DIA SDK
         # The latest Windows SDK is included automatically via --includeRecommended
     )

>From f1dfb8a66166a17c004d38a3d7a298fad1b4f105 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Fri, 2 Oct 2026 17:23:00 -0400
Subject: [PATCH 5/8] Use multiline litteral

---
 llvm/utils/release/build_llvm_release.ps1 | 30 ++++++++++++-----------
 1 file changed, 16 insertions(+), 14 deletions(-)

diff --git a/llvm/utils/release/build_llvm_release.ps1 b/llvm/utils/release/build_llvm_release.ps1
index 557d04eac20bff..b328895bb4faf8 100644
--- a/llvm/utils/release/build_llvm_release.ps1
+++ b/llvm/utils/release/build_llvm_release.ps1
@@ -786,21 +786,23 @@ function Invoke-ElevatedPrerequisiteInstallation {
     if ($arm64) { $invocation += ' -arm64' }
     if ($ForceMSVC) { $invocation += ' -ForceMSVC' }
     # Keep the elevated window open so the user can review the result.
-    $workerLines = @(
-        '$exitCode = 0'
-        'try {'
-        "    $invocation"
-        '} catch {'
-        '    Write-Host "Prerequisite installation failed:" -ForegroundColor Red'
-        '    Write-Host ($_ | Out-String) -ForegroundColor Red'
-        '    $exitCode = 1'
-        '}'
-    )
-    if (-not $Unattended) {
-        $workerLines += "Read-Host 'Press Enter to close this window' | Out-Null"
+    $pauseCommand = if (-not $Unattended) {
+        "Read-Host 'Press Enter to close this window' | Out-Null"
+    } else {
+        ''
     }
-    $workerLines += 'exit $exitCode'
-    $workerCommand = $workerLines -join "`n"
+    $workerCommand = @"
+`$exitCode = 0
+try {
+    $invocation
+} catch {
+    Write-Host "Prerequisite installation failed:" -ForegroundColor Red
+    Write-Host (`$_ | Out-String) -ForegroundColor Red
+    `$exitCode = 1
+}
+$pauseCommand
+exit `$exitCode
+"@
     $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($workerCommand))
 
     Write-SubStep 'Requesting administrator access to install prerequisites...'

>From 6139cceba3ef719f23ca8393314dfd7aa810aedd Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Fri, 2 Oct 2026 17:24:27 -0400
Subject: [PATCH 6/8] Add comment regarding deprecation status of WiX tools

---
 llvm/utils/release/build_llvm_release.ps1 | 1 +
 1 file changed, 1 insertion(+)

diff --git a/llvm/utils/release/build_llvm_release.ps1 b/llvm/utils/release/build_llvm_release.ps1
index b328895bb4faf8..f20c64a7529ddc 100644
--- a/llvm/utils/release/build_llvm_release.ps1
+++ b/llvm/utils/release/build_llvm_release.ps1
@@ -636,6 +636,7 @@ function Get-MissingPrerequisites {
         if ($tool.Cmd -eq 'make') {
             $toolPath = Find-MakeExecutable
         }
+        # These tools are deprecated and removed in newer version of WiX.
         if ($tool.Cmd -in @('candle', 'light') -and -not $wixBin) {
             $toolPath = $null
         }

>From c8bd2f61517bc6aea64b00b89af83a6603f69311 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Sat, 3 Oct 2026 13:48:11 -0400
Subject: [PATCH 7/8] Validate CMake version for Windows release builds

---
 llvm/utils/release/build_llvm_release.ps1 | 95 ++++++++++++++++++++++-
 1 file changed, 92 insertions(+), 3 deletions(-)

diff --git a/llvm/utils/release/build_llvm_release.ps1 b/llvm/utils/release/build_llvm_release.ps1
index f20c64a7529ddc..a8880b3e5277db 100644
--- a/llvm/utils/release/build_llvm_release.ps1
+++ b/llvm/utils/release/build_llvm_release.ps1
@@ -205,6 +205,7 @@ if ($PSVersionTable.PSVersion.Major -lt 7) {
 Set-StrictMode -Version Latest
 $ErrorActionPreference = 'Stop'
 $script:ReleaseScriptPath = $PSCommandPath
+$script:MinimumCMakeVersion = [version]'3.31.0'
 
 # Save the console mode so we can restore it at exit. Child processes
 # (cmake, ninja, link.exe, etc.) sometimes disable virtual terminal
@@ -506,6 +507,76 @@ function Find-Wix314Bin {
     return $null
 }
 
+function Get-CMakeVersion {
+    param([Parameter(Mandatory)][string]$Path)
+
+    try {
+        $versionOutput = (& $Path --version 2>&1 | Out-String)
+        if ($LASTEXITCODE -ne 0 -or
+            $versionOutput -notmatch '(?m)^cmake version (\d+(?:\.\d+){1,3})\s*$') {
+            return $null
+        }
+        return [version]$Matches[1]
+    } catch {
+        return $null
+    }
+}
+
+function Find-CMakeExecutable {
+    # Prefer the first usable version on PATH, then the standard CMake
+    # installation, and finally CMake bundled with any Visual Studio instance.
+    $candidates = @()
+    foreach ($pathDir in ($env:PATH -split ';')) {
+        if ($pathDir) {
+            $candidates += Join-Path $pathDir 'cmake.exe'
+        }
+    }
+
+    if ($env:ProgramFiles) {
+        $candidates += Join-Path $env:ProgramFiles 'CMake\bin\cmake.exe'
+    }
+
+    $vsInstallPaths = @()
+    if ($env:VSINSTALLDIR) {
+        $vsInstallPaths += $env:VSINSTALLDIR
+    }
+    $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
+    if (Test-Path -LiteralPath $vswhere -PathType Leaf) {
+        try {
+            $vsInstallPaths += @(& $vswhere -all -products '*' -property installationPath 2>$null)
+        } catch {
+            # CMake may still be available through PATH or the standard install.
+        }
+    }
+    foreach ($vsInstallPath in ($vsInstallPaths | Where-Object { $_ } | Select-Object -Unique)) {
+        $candidates += Join-Path $vsInstallPath 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
+    }
+
+    foreach ($candidate in ($candidates | Select-Object -Unique)) {
+        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
+
+        $version = Get-CMakeVersion -Path $candidate
+        if ($null -eq $version -or $version -lt $script:MinimumCMakeVersion) {
+            if ($null -ne $version) {
+                Write-SubStep "Ignoring CMake $version at $candidate; LLVM requires $($script:MinimumCMakeVersion) or newer."
+            }
+            continue
+        }
+
+        # Put the selected directory first so every later `cmake` invocation
+        # uses the validated executable, even if an older one appeared earlier.
+        $binDir = Split-Path -Parent $candidate
+        $otherPaths = @($env:PATH -split ';' | Where-Object {
+            $_ -and $_.TrimEnd('\') -ine $binDir.TrimEnd('\')
+        })
+        $env:PATH = (@($binDir) + $otherPaths) -join ';'
+        Write-SubStep "CMake ${version}: $candidate"
+        return $candidate
+    }
+
+    return $null
+}
+
 function Find-SevenZipExecutable {
     $candidates = @()
     $command = Get-Command 7z -CommandType Application -ErrorAction SilentlyContinue
@@ -588,7 +659,6 @@ function Get-MissingPrerequisites {
         "$env:ProgramFiles\Git\usr\bin"                     # sh, bash, sed, grep (needed by tests)
         "${env:ProgramFiles(x86)}\GnuWin32\bin"             # make (GnuWin32 default)
         "$env:ProgramFiles\GnuWin32\bin"                    # make (alt location)
-        "$env:ProgramFiles\CMake\bin"                       # cmake
     )
     foreach ($dir in $pathFixups) {
         if ((Test-Path $dir) -and ($env:PATH -notlike "*$dir*")) {
@@ -597,6 +667,8 @@ function Get-MissingPrerequisites {
         }
     }
 
+    $cmakePath = Find-CMakeExecutable
+
     # CPack finds WiX through PATH. An older candle/light pair can appear
     # before the installed 3.14 tools (for example, in an Unreal SDK).
     $wixBin = Find-Wix314Bin
@@ -608,7 +680,6 @@ function Get-MissingPrerequisites {
 
     # Check all required tools.
     $required = @(
-        @{ Name = "CMake";      Cmd = "cmake" }
         @{ Name = "Ninja";      Cmd = "ninja" }
         @{ Name = "Python";     Cmd = "python" }
         @{ Name = "7-Zip";      Cmd = "7z" }
@@ -647,6 +718,10 @@ function Get-MissingPrerequisites {
         }
     }
 
+    if (-not $cmakePath) {
+        $missing += "CMake $($script:MinimumCMakeVersion) or newer"
+    }
+
     if (-not $ForceMSVC) {
         $llvmBin = Find-OfficialLlvmBin
         if ($llvmBin) {
@@ -867,6 +942,9 @@ function Install-Prerequisites {
         }
         Invoke-ElevatedPrerequisiteInstallation
         Update-PathAfterPrerequisites
+        if (-not (Find-CMakeExecutable)) {
+            throw "CMake $($script:MinimumCMakeVersion) or newer could not be found after prerequisite installation."
+        }
         Install-PythonPsutilForRequestedArchitectures
         return
     }
@@ -877,6 +955,7 @@ function Install-Prerequisites {
     $wingetPath = Find-WingetExecutable
 
     # Version requirements for the release build:
+    #   - CMake 3.31 or newer
     #   - NetFx3 (.NET Framework 3.5) for WiX Toolset 3.14
     #   - Python 3.13 for all architectures
     #   - SWIG 4 or newer for LLDB (install the latest available WinGet version)
@@ -903,7 +982,8 @@ function Install-Prerequisites {
             -not (Test-ExecutableVersion -Path $cmd.Source)) {
             $cmd = $null
         }
-        $existingPath = if ($tool.Name -eq 'wix') { Find-Wix314Bin }
+        $existingPath = if ($tool.Name -eq 'cmake') { Find-CMakeExecutable }
+            elseif ($tool.Name -eq 'wix') { Find-Wix314Bin }
             elseif ($tool.Name -eq '7z') { Find-SevenZipExecutable }
             elseif ($tool.Name -eq 'make') { Find-MakeExecutable }
             elseif ($cmd) { $cmd.Source }
@@ -945,6 +1025,12 @@ function Install-Prerequisites {
                     continue
                 }
             }
+            if ($tool.Name -eq 'cmake') {
+                if (-not (Find-CMakeExecutable)) {
+                    throw "CMake installation finished, but version $($script:MinimumCMakeVersion) or newer could not be found."
+                }
+                continue
+            }
             if ($installExitCode -ne 0) {
                 if ($tool.Name -eq 'wix') {
                     throw "Failed to install WiX via winget (exit code $installExitCode). NetFx3 is installed; inspect winget's error above."
@@ -1007,6 +1093,9 @@ function Install-Prerequisites {
 
     # Get-MissingPrerequisites will add non-standard paths such as GnuWin32.
     Update-PathAfterPrerequisites
+    if (-not (Find-CMakeExecutable)) {
+        throw "CMake $($script:MinimumCMakeVersion) or newer could not be found after prerequisite installation."
+    }
     Install-PythonPsutilForRequestedArchitectures
 }
 

>From 0dee134ced7be6c9865bfb32c5a85b7eaa1e211f Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Sat, 3 Oct 2026 14:12:28 -0400
Subject: [PATCH 8/8] Fix Windows CI errors: * retry the installer copy more
 than once * attempt to install NetFx3 if it's not present

---
 .github/workflows/release-binaries.yml | 45 ++++++++++++++++++++++++--
 1 file changed, 42 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index c14156c742cffd..68151ab1bdea13 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -328,6 +328,25 @@ jobs:
         release_dir=`find "$BUILD_PREFIX/build" -iname 'stage2-bins'`
         mv "$release_dir/$RELEASE_BINARY_FILENAME" .
 
+    - name: Enable NetFx3 (Windows ARM64)
+      if: runner.os == 'Windows' && runner.arch == 'ARM64'
+      shell: pwsh
+      run: |
+        $feature = Get-WindowsOptionalFeature -Online -FeatureName NetFx3
+        if ($feature.State -ne 'Enabled') {
+          $result = Enable-WindowsOptionalFeature -Online -FeatureName NetFx3 `
+            -All -NoRestart -ErrorAction Stop
+          if ($result.RestartNeeded) {
+            throw 'Enabling NetFx3 requires a restart; the runner image must have it pre-enabled.'
+          }
+        }
+        $framework = Get-ItemProperty `
+          -LiteralPath 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5' `
+          -ErrorAction SilentlyContinue
+        if ($null -eq $framework -or $framework.Install -ne 1) {
+          throw 'NetFx3 did not register as installed after enabling the Windows feature.'
+        }
+
     # Wix is not installed by default on Windows ARM64
     - name: Install Wix (Windows ARM64)
       env:
@@ -346,6 +365,26 @@ jobs:
         INPUTS_RUNS_ON: ${{ inputs.runs-on }}
         LLVM_VERSION: ${{ needs.prepare.outputs.release-version }}
       run: |
+        function Move-ArtifactWithRetry {
+          param(
+            [Parameter(Mandatory)][string]$Source,
+            [Parameter(Mandatory)][string]$DestinationDirectory
+          )
+          $maxAttempts = 12
+          for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
+            try {
+              Move-Item -LiteralPath $Source -Destination $DestinationDirectory -ErrorAction Stop
+              return
+            } catch {
+              if ($attempt -eq $maxAttempts) {
+                throw "Could not move artifact '$Source' after $maxAttempts attempts: $($_.Exception.Message)"
+              }
+              Write-Warning "Artifact move attempt $attempt/$maxAttempts failed; retrying in 5 seconds: $($_.Exception.Message)"
+              Start-Sleep -Seconds 5
+            }
+          }
+        }
+
         subst S: "${{ github.workspace }}"
         cd S:\llvm\utils\release\
         $buildArgs = @{ Version = $env:LLVM_VERSION; FastBuild = $true; Unattended = $true }
@@ -357,13 +396,13 @@ jobs:
         & .\build_llvm_release.ps1 @buildArgs
         if ($env:INPUTS_RUNS_ON -eq "windows-11-arm") {
           $zstd = (Get-ChildItem -Recurse -Filter "zstd.exe" | Select-Object -First 1).fullName
-          mv $zstd $env:GITHUB_WORKSPACE
+          Move-ArtifactWithRetry -Source $zstd -DestinationDirectory $env:GITHUB_WORKSPACE
         }
         $installer = (Get-ChildItem -Recurse -Filter "LLVM-*.msi" | Select-Object -First 1).fullName
         $tarball = (Get-ChildItem -Recurse -Filter "*.tar.xz" | Select-Object -First 1).fullName
         # Move installer to top-level directory so it is easier to upload.
-        mv $installer $env:GITHUB_WORKSPACE
-        mv $tarball $env:GITHUB_WORKSPACE
+        Move-ArtifactWithRetry -Source $installer -DestinationDirectory $env:GITHUB_WORKSPACE
+        Move-ArtifactWithRetry -Source $tarball -DestinationDirectory $env:GITHUB_WORKSPACE
         echo "windows-installer-filename=$(Split-Path -Path $installer -Leaf)" >> $env:GITHUB_OUTPUT
     
     - name: Dump Wix logs



More information about the llvm-commits mailing list