[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