[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:24:17 PDT 2026
================
@@ -0,0 +1,2347 @@
+#!/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 -arm64
+ Build for 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 -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"
----------------
aganea wrote:
It was a mistake, fixed.
https://github.com/llvm/llvm-project/pull/228528
More information about the llvm-commits
mailing list