Files
Attacks-for-Bombs-1/scripts/_tools.ps1
T

582 lines
25 KiB
PowerShell
Executable File

<#
.SYNOPSIS
Interactive installer for the tools this project builds with. Windows;
called by scripts\setup.bat (Linux and macOS have scripts/_tools.sh).
.DESCRIPTION
Checks each tool, and for every one that is missing asks -- ONE TOOL AT
A TIME -- whether to install it, showing the exact command first. Nothing
is installed without an explicit yes (or -Yes / -All).
Required Git, CMake 3.25+, and a C++ compiler: at least one of
Visual Studio Build Tools (MSVC) or MinGW-w64 (via MSYS2)
Optional LLVM (clang-cl, Clang, clang-format, clang-tidy), and the
other MSYS2 toolchains (32-bit MinGW, CLANG64)
Installs go through winget (Windows Package Manager, part of the App
Installer that ships with Windows 10 1809+ and Windows 11). Two things
winget does not do for you, and this script does:
* put CMake and LLVM on PATH -- their installers leave it to you, and
every guide that runs `winget install Kitware.CMake` ends with "now
add C:\Program Files\CMake\bin to PATH by hand". A tool found in its
usual folder but not on PATH is added to this session's PATH (which
is handed back to setup.bat through -PathOut so the rest of setup
can use it) and, if you agree, to your user PATH permanently.
* update a fresh MSYS2, whose package databases are stale (twice, as
MSYS2 itself recommends -- updating its runtime asks for a restart)
before installing MinGW-w64 into it.
Written for Windows PowerShell 5.1 as much as for PowerShell 7: no ??,
no ternary, no && / ||, nested Join-Path (5.1 has no -AdditionalChildPath),
and plain ASCII only (5.1 reads a BOM-less file as the ANSI code page).
.PARAMETER Yes
Install the REQUIRED tools without asking. For scripts and CI. Without a
terminal and without this, nothing is asked and nothing is installed.
.PARAMETER All
Also install the optional tools without asking (implies -Yes).
.PARAMETER Check
Only report what is and is not installed; change nothing. Exits 1 if a
required tool is missing.
.PARAMETER PathOut
File to write the directories that were added to this session's PATH to,
one per line, so the calling batch file can add them to its own.
.NOTES
Exit status: 0 if every required tool is present at the end, 1 if not.
#>
[CmdletBinding()]
param(
[switch]$Yes,
[switch]$All,
[switch]$Check,
[string]$PathOut = ''
)
if ($All) { $Yes = $true }
# Oldest CMake this project supports (cmake_minimum_required in
# CMakeLists.txt, and the presets file's cmakeMinimumRequired).
$script:MinCmake = New-Object System.Version 3, 25
# The Visual Studio Build Tools package. 'Microsoft.VisualStudio.BuildTools'
# (no year) is the newest release; the 2022 one is the long-established,
# documented path, so it is the default -- change the id here to prefer
# the newer one.
$script:MsvcWingetId = 'Microsoft.VisualStudio.2022.BuildTools'
$script:MsvcOverride = '--passive --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
# Answers that carry over from one prompt to the next.
$script:AnsAll = $false # "a": yes to everything still to come
$script:AnsSkipOptional = $false # "s": no to every OPTIONAL tool still to come
$script:AnsQuit = $false # "q": stop asking altogether
# Directories added to this session's PATH, for setup.bat to pick up.
$script:PathAdds = New-Object System.Collections.ArrayList
# ---------------------------------------------------------------------------
# Small helpers
# ---------------------------------------------------------------------------
function Say([string]$Text = '') { Write-Host $Text }
function Note([string]$Text) { Write-Host $Text -ForegroundColor DarkGray }
function Good([string]$Text) { Write-Host $Text -ForegroundColor Green }
function Warn([string]$Text) { Write-Host $Text -ForegroundColor Yellow }
function Bad([string]$Text) { Write-Host $Text -ForegroundColor Red }
# Nested Join-Path: Windows PowerShell 5.1's Join-Path takes only two parts.
function Join-Parts {
$p = [string]$args[0]
for ($i = 1; $i -lt $args.Count; $i++) { $p = Join-Path $p ([string]$args[$i]) }
return $p
}
function Get-ProgramFiles { return $env:ProgramFiles }
function Get-ProgramFilesX86 { return ${env:ProgramFiles(x86)} }
# MSYS2's install folder: C:\msys64 unless MSYS2_ROOT says otherwise.
function Get-Msys2Root {
if ($env:MSYS2_ROOT) { return $env:MSYS2_ROOT }
$drive = $env:SystemDrive
if (-not $drive) { $drive = 'C:' }
return (Join-Path ($drive + '\') 'msys64')
}
# The first executable called $Name on PATH, or one in $FallbackDirs (the
# folder the installer puts it in even when it doesn't touch PATH), or $null.
function Find-Exe([string]$Name, [string[]]$FallbackDirs) {
$cmd = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($cmd) { return $cmd.Source }
foreach ($dir in $FallbackDirs) {
if (-not $dir) { continue }
foreach ($ext in @('.exe', '')) {
$candidate = Join-Path $dir ($Name + $ext)
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
}
}
return $null
}
function New-Status([string]$State, [string]$Detail = '') {
return [pscustomobject]@{ State = $State; Detail = $Detail }
}
# ---------------------------------------------------------------------------
# PATH (session and user)
#
# Wrapped in functions so a test can replace the two that touch the real
# registry.
# ---------------------------------------------------------------------------
function Get-UserPathValue { return [Environment]::GetEnvironmentVariable('Path', 'User') }
function Set-UserPathValue([string]$Value) { [Environment]::SetEnvironmentVariable('Path', $Value, 'User') }
function Get-MachinePathValue { return [Environment]::GetEnvironmentVariable('Path', 'Machine') }
function Split-PathList([string]$List) {
if (-not $List) { return @() }
$sep = [System.IO.Path]::PathSeparator
$items = @()
foreach ($part in $List.Split($sep)) {
$trimmed = $part.Trim().TrimEnd('\', '/')
if ($trimmed) { $items += $trimmed }
}
return $items
}
function Test-InList([string]$List, [string]$Dir) {
$want = $Dir.TrimEnd('\', '/')
foreach ($item in (Split-PathList $List)) {
if ($item -ieq $want) { return $true }
}
return $false
}
function Test-OnSessionPath([string]$Dir) { return (Test-InList $env:Path $Dir) }
function Test-OnPersistentPath([string]$Dir) {
return ((Test-InList (Get-UserPathValue) $Dir) -or (Test-InList (Get-MachinePathValue) $Dir))
}
function Add-ToSessionPath([string]$Dir) {
if (Test-OnSessionPath $Dir) { return }
$sep = [System.IO.Path]::PathSeparator
$current = $env:Path
if (-not $current) { $current = '' }
$env:Path = $current.TrimEnd($sep) + $sep + $Dir
[void]$script:PathAdds.Add($Dir)
}
function Add-ToUserPath([string]$Dir) {
$current = Get-UserPathValue
$sep = [System.IO.Path]::PathSeparator
if ($current) { $new = $current.TrimEnd($sep) + $sep + $Dir } else { $new = $Dir }
Set-UserPathValue $new
}
# ---------------------------------------------------------------------------
# Detection
# ---------------------------------------------------------------------------
function Get-WingetStatus {
if (Get-Command winget -ErrorAction SilentlyContinue) { return (New-Status 'ok' 'winget') }
return (New-Status 'missing')
}
function Get-GitStatus {
$exe = Find-Exe 'git' @((Join-Parts (Get-ProgramFiles) 'Git' 'cmd'))
if (-not $exe) { return (New-Status 'missing') }
$ver = ''
try { $ver = (& $exe --version 2>$null | Select-Object -First 1) -replace '^git version ', '' } catch { }
return (New-Status 'ok' $ver)
}
function Get-CmakeStatus {
$exe = Find-Exe 'cmake' @((Join-Parts (Get-ProgramFiles) 'CMake' 'bin'))
if (-not $exe) { return (New-Status 'missing') }
$ver = ''
try {
$first = & $exe --version 2>$null | Select-Object -First 1
if ($first -match '(\d+\.\d+(\.\d+)?)') { $ver = $Matches[1] }
} catch { }
if (-not $ver) { return (New-Status 'ok' '') }
$parsed = $null
if ([System.Version]::TryParse($ver, [ref]$parsed) -and $parsed -lt $script:MinCmake) {
return (New-Status 'old' $ver)
}
return (New-Status 'ok' $ver)
}
# MSVC: the Visual Studio Installer ships vswhere.exe, which knows whether
# the C++ x86/x64 tools are installed in ANY Visual Studio or Build Tools.
function Get-MsvcStatus {
$pf86 = Get-ProgramFilesX86
if (-not $pf86) { return (New-Status 'missing') }
$vswhere = Join-Parts $pf86 'Microsoft Visual Studio' 'Installer' 'vswhere.exe'
if (-not (Test-Path -LiteralPath $vswhere -PathType Leaf)) { return (New-Status 'missing') }
$where = $null
try {
$where = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null | Select-Object -First 1
} catch { }
if ($where) { return (New-Status 'ok' ([string]$where)) }
return (New-Status 'missing')
}
function Get-LlvmStatus {
$exe = Find-Exe 'clang-cl' @((Join-Parts (Get-ProgramFiles) 'LLVM' 'bin'))
if (-not $exe) { return (New-Status 'missing') }
return (New-Status 'ok' (Split-Path -Parent $exe))
}
function Get-Msys2Status {
$bash = Join-Parts (Get-Msys2Root) 'usr' 'bin' 'bash.exe'
if (Test-Path -LiteralPath $bash -PathType Leaf) { return (New-Status 'ok' (Get-Msys2Root)) }
return (New-Status 'missing')
}
function Get-MingwStatus([string]$Subdir) {
$gpp = Join-Parts (Get-Msys2Root) $Subdir 'bin' 'g++.exe'
if (Test-Path -LiteralPath $gpp -PathType Leaf) { return (New-Status 'ok' (Split-Path -Parent $gpp)) }
return (New-Status 'missing')
}
function Get-Clang64Status {
$clang = Join-Parts (Get-Msys2Root) 'clang64' 'bin' 'clang++.exe'
if (Test-Path -LiteralPath $clang -PathType Leaf) { return (New-Status 'ok' (Split-Path -Parent $clang)) }
return (New-Status 'missing')
}
# ---------------------------------------------------------------------------
# Asking
# ---------------------------------------------------------------------------
function Test-CanAsk { return (-not [Console]::IsInputRedirected) }
# One line from the user, or $null at end of input. A function of its own so
# a test can stand in for the console.
function Read-Answer { return [Console]::ReadLine() }
# Would an OPTIONAL tool be offered (asked about, or installed by -All) now?
function Test-OptionalWanted {
if ($script:AnsQuit -or $script:AnsSkipOptional) { return $false }
if ($All -or $script:AnsAll) { return $true }
return (Test-CanAsk)
}
# Returns $true for "install it". Sets $script:Ans* for answers that outlast
# one prompt.
function Confirm-Install([string]$Kind, [string]$Question) {
if ($script:AnsQuit) { return $false }
if ($script:AnsAll) { return $true }
if ($Kind -eq 'optional' -and $script:AnsSkipOptional) { return $false }
# Flags answer for you...
if ($Kind -eq 'required' -and $Yes) { return $true }
if ($All) { return $true }
# ...and with no terminal there is nobody to ask.
if (-not (Test-CanAsk)) {
Note ' (not asking: no terminal. Re-run interactively, or pass --yes / --all.)'
return $false
}
if ($Kind -eq 'required') { $prompt = " $Question [Y/n/a/q] " } else { $prompt = " $Question [y/N/a/s/q] " }
while ($true) {
Write-Host $prompt -NoNewline
$answer = Read-Answer
if ($null -eq $answer) {
# End of input (Ctrl-Z): stop asking rather than loop on it.
Write-Host ''
$script:AnsQuit = $true
return $false
}
switch ($answer.Trim().ToLowerInvariant()) {
{ $_ -in @('y', 'yes') } { return $true }
{ $_ -in @('n', 'no') } { return $false }
'' { return ($Kind -eq 'required') }
{ $_ -in @('a', 'all') } { $script:AnsAll = $true; return $true }
{ $_ -in @('s', 'skip') } { $script:AnsSkipOptional = $true; return $false }
{ $_ -in @('q', 'quit') } { $script:AnsQuit = $true; return $false }
default {
if ($Kind -eq 'required') {
Note ' y = install n = not this one a = install everything left q = stop asking'
} else {
Note ' y = install n = not this one a = install everything left s = skip the rest of the optional tools q = stop asking'
}
}
}
}
}
# ---------------------------------------------------------------------------
# Installing
# ---------------------------------------------------------------------------
function Invoke-Winget([string]$Id, [string]$Override = '') {
$winget = @('install', '--id', $Id, '-e', '--source', 'winget',
'--accept-package-agreements', '--accept-source-agreements')
$shown = 'winget ' + ($winget -join ' ')
if ($Override) {
$winget += @('--override', $Override)
$shown += ' --override "' + $Override + '"'
}
Write-Host (' $ ' + $shown) -ForegroundColor DarkGray
& winget @winget
return ($LASTEXITCODE -eq 0)
}
# MSYS2's own advice for a fresh install: its package databases are stale, so
# update the base system first, twice (updating the runtime asks for a
# restart of the shell), then install into it.
function Invoke-Msys2Packages([string[]]$Packages) {
$bash = Join-Parts (Get-Msys2Root) 'usr' 'bin' 'bash.exe'
if (-not (Test-Path -LiteralPath $bash -PathType Leaf)) {
Bad ' MSYS2 is not installed at the expected place -- install it first.'
return $false
}
Write-Host (' $ ' + $bash + ' -lc "pacman --noconfirm -Syuu" (twice; a fresh MSYS2 needs it)') -ForegroundColor DarkGray
& $bash -lc 'pacman --noconfirm -Syuu'
& $bash -lc 'pacman --noconfirm -Syuu'
$list = $Packages -join ' '
Write-Host (' $ ' + $bash + ' -lc "pacman -S --needed --noconfirm ' + $list + '"') -ForegroundColor DarkGray
& $bash -lc ('pacman -S --needed --noconfirm ' + $list)
return ($LASTEXITCODE -eq 0)
}
# ---------------------------------------------------------------------------
# The catalog
#
# Ordered as they are asked. Each row: a stable Id, whether it is Required or
# optional (msvc is decided at run time -- see below), a label, why it
# matters, how to tell if it is there, how to install it, and the folder
# that has to be on PATH for it to be usable (or $null).
# ---------------------------------------------------------------------------
function Get-Catalog {
$pf = Get-ProgramFiles
$msys = Get-Msys2Root
return @(
@{ Id = 'git'; Kind = 'required'; Label = 'Git';
Why = 'FetchContent clones dependencies (fmt) with it';
Status = { Get-GitStatus }; Install = { Invoke-Winget 'Git.Git' };
PathDir = (Join-Parts $pf 'Git' 'cmd'); PathDefault = $true },
@{ Id = 'cmake'; Kind = 'required'; Label = 'CMake 3.25 or newer';
Why = 'configures and drives every build';
Status = { Get-CmakeStatus }; Install = { Invoke-Winget 'Kitware.CMake' };
PathDir = (Join-Parts $pf 'CMake' 'bin'); PathDefault = $true },
# A compiler: at least one of MSVC or MinGW-w64 is required. Which
# kind msvc is depends on whether one is already there (see Main).
@{ Id = 'msvc'; Kind = 'compiler'; Label = 'Visual Studio Build Tools (MSVC)';
Why = 'the windows-*-msvc presets, and the headers/libraries the windows-*-clang-cl ones need';
Status = { Get-MsvcStatus };
Install = { Invoke-Winget $script:MsvcWingetId $script:MsvcOverride };
PathDir = $null },
@{ Id = 'llvm'; Kind = 'optional'; Label = 'LLVM (clang-cl, Clang, clang-format, clang-tidy)';
Why = 'the windows-*-clang-cl presets, scripts\refactor.bat and the pre-commit hook';
Status = { Get-LlvmStatus }; Install = { Invoke-Winget 'LLVM.LLVM' };
PathDir = (Join-Parts $pf 'LLVM' 'bin'); PathDefault = $true },
@{ Id = 'msys2'; Kind = 'optional'; Label = 'MSYS2';
Why = 'the home of MinGW-w64 on Windows (needed for the windows-*-mingw* presets)';
Status = { Get-Msys2Status }; Install = { Invoke-Winget 'MSYS2.MSYS2' };
PathDir = $null },
@{ Id = 'mingw64'; Kind = 'optional'; Label = 'MinGW-w64 GCC, 64-bit';
Why = 'the windows-x64-mingw64 preset'; Needs = 'msys2';
Status = { Get-MingwStatus 'mingw64' };
Install = { Invoke-Msys2Packages @('mingw-w64-x86_64-gcc') };
PathDir = (Join-Parts $msys 'mingw64' 'bin'); PathDefault = $false },
@{ Id = 'mingw32'; Kind = 'optional'; Label = 'MinGW-w64 GCC, 32-bit';
Why = 'the windows-x86-mingw32 preset'; Needs = 'msys2';
Status = { Get-MingwStatus 'mingw32' };
Install = { Invoke-Msys2Packages @('mingw-w64-i686-gcc') };
PathDir = (Join-Parts $msys 'mingw32' 'bin'); PathDefault = $false },
@{ Id = 'clang64'; Kind = 'optional'; Label = 'MSYS2 CLANG64 (GNU-style Clang)';
Why = 'the windows-x64-clang preset'; Needs = 'msys2';
Status = { Get-Clang64Status };
Install = { Invoke-Msys2Packages @('mingw-w64-clang-x86_64-clang') };
PathDir = (Join-Parts $msys 'clang64' 'bin'); PathDefault = $false }
)
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
function Show-ManualHelp {
Say ' Install these yourself instead:'
Say ' Git https://git-scm.com/download/win'
Say ' CMake https://cmake.org/download/ (tick "Add CMake to the system PATH")'
Say ' MSVC https://visualstudio.microsoft.com/visual-cpp-build-tools/ (workload: Desktop development with C++)'
Say ' LLVM https://github.com/llvm/llvm-project/releases'
Say ' MSYS2 https://www.msys2.org/'
}
function Invoke-Main {
$catalog = Get-Catalog
$wingetStatus = Get-WingetStatus
$haveWinget = ($wingetStatus.State -eq 'ok')
Say ('==> Checking the tools this project builds with (Windows, winget: ' + $(if ($haveWinget) { 'found' } else { 'NOT found' }) + ')')
# ---- report ---------------------------------------------------------
$status = @{}
foreach ($row in $catalog) { $status[$row.Id] = (& $row.Status) }
$haveCompiler = ($status['msvc'].State -eq 'ok') -or ($status['mingw64'].State -eq 'ok') -or ($status['mingw32'].State -eq 'ok')
foreach ($row in $catalog) {
$st = $status[$row.Id]
$kind = $row.Kind
if ($kind -eq 'compiler') { if ($haveCompiler) { $kind = 'optional' } else { $kind = 'required' } }
$row.EffKind = $kind
switch ($st.State) {
'ok' {
if ($st.Detail) { Write-Host (' [ ok ] ' + $row.Label.PadRight(50) + ' ') -NoNewline -ForegroundColor Green; Note $st.Detail }
else { Good (' [ ok ] ' + $row.Label) }
}
'old' { Warn (' [old ] ' + $row.Label.PadRight(50) + ' ' + $st.Detail + ' (needs ' + $script:MinCmake.ToString() + '+)') }
default {
if ($kind -eq 'required') { Bad (' [ !! ] ' + $row.Label.PadRight(50) + ' missing (required)') }
else { Note (' [ -- ] ' + $row.Label.PadRight(50) + ' missing (optional)') }
}
}
}
if (-not $haveCompiler) {
Warn ' A C++ compiler is required: Visual Studio Build Tools, or MinGW-w64 through MSYS2 (either one).'
}
# ---- -Check: report only --------------------------------------------
if ($Check) {
$missing = ($status['git'].State -ne 'ok') -or ($status['cmake'].State -ne 'ok') -or (-not $haveCompiler)
if ($missing) { return 1 }
return 0
}
$anythingToInstall = $false
foreach ($row in $catalog) { if ($status[$row.Id].State -ne 'ok') { $anythingToInstall = $true } }
if ($anythingToInstall -and -not $haveWinget) {
Say ''
Warn ' winget (Windows Package Manager) was not found, so this script cannot install anything.'
Say ' It comes with the App Installer: https://aka.ms/getwinget (or the Microsoft Store).'
Show-ManualHelp
}
# ---- offer installs, one at a time ----------------------------------
$skipped = @()
$failed = @()
foreach ($row in $catalog) {
if ($script:AnsQuit) { break }
$st = & $row.Status # re-check: an earlier install may have provided it
if ($st.State -eq 'ok') { continue }
if (-not $haveWinget) { continue }
# MinGW rows live inside MSYS2.
if ($row.Needs) {
$needRow = $catalog | Where-Object { $_.Id -eq $row.Needs } | Select-Object -First 1
$needed = & $needRow.Status
if ($needed.State -ne 'ok') { continue }
}
# After the msvc row, "a compiler exists" may have changed the kind
# of the rows still to come.
$kind = $row.EffKind
if ($kind -eq 'required' -and $row.Id -eq 'msvc') {
$now = ((Get-MingwStatus 'mingw64').State -eq 'ok') -or ((Get-MingwStatus 'mingw32').State -eq 'ok')
if ($now) { $kind = 'optional' }
}
if ($kind -eq 'optional' -and -not (Test-OptionalWanted)) {
$skipped += $row.Label
continue
}
Say ''
Write-Host (' ' + $row.Label) -NoNewline -ForegroundColor White
Say (' -- ' + $row.Why)
if ($st.State -eq 'old') { Note (' (installed: ' + $st.Detail + ', but too old)') }
if (Confirm-Install $kind ('Install ' + $row.Label + '?')) {
$ok = & $row.Install
$after = & $row.Status
if ($after.State -eq 'ok') {
Good ' installed'
} elseif ($after.State -eq 'old') {
Warn (' CMake ' + $after.Detail + ' is older than the ' + $script:MinCmake.ToString() + ' this project needs.')
$failed += $row.Label
} elseif (-not $ok) {
Bad ' install failed -- see the output above.'
$failed += $row.Label
} else {
Warn (' the install finished, but ' + $row.Label + ' still is not found -- open a new terminal, or check the output above.')
$failed += $row.Label
}
} elseif ($kind -eq 'optional') {
$skipped += $row.Label
}
}
if ($skipped.Count -gt 0) {
Say ''
Note (' Optional, not installed: ' + ($skipped -join ', '))
Note ' (scripts\setup.bat, run interactively, offers each one; --all installs them all.)'
}
# ---- PATH ------------------------------------------------------------
# A tool that is installed but whose folder is not on PATH is unusable
# from a plain terminal -- and, for CMake, from the rest of setup.bat.
foreach ($row in $catalog) {
if (-not $row.PathDir) { continue }
$st = & $row.Status
if ($st.State -ne 'ok') { continue }
if (-not (Test-Path -LiteralPath $row.PathDir -PathType Container)) { continue }
if (Test-OnSessionPath $row.PathDir) { continue }
# This session, always: what follows in setup needs it.
Add-ToSessionPath $row.PathDir
if (Test-OnPersistentPath $row.PathDir) { continue }
Say ''
Say (' ' + $row.Label + ' is installed, but its folder is not on your PATH:')
Say (' ' + $row.PathDir)
if ($row.PathDefault) { $kind = 'required' } else { $kind = 'optional' }
if (-not $row.PathDefault) {
Note ' (Adding MSYS2 toolchain folders puts their g++/gcc on PATH for every program; the MinGW presets use'
Note ' the prefixed names, so you can also leave this out and use an MSYS2 shell instead.)'
}
if (Confirm-Install $kind 'Add it to your user PATH (new terminals will see it)?') {
Add-ToUserPath $row.PathDir
Good ' added -- open a new terminal to use it there.'
}
}
# ---- hand this session's PATH additions back to setup.bat ------------
if ($PathOut -and $script:PathAdds.Count -gt 0) {
Set-Content -LiteralPath $PathOut -Value ($script:PathAdds.ToArray()) -Encoding ASCII
}
# ---- verdict ---------------------------------------------------------
$exit = 0
$needGit = (Get-GitStatus).State -ne 'ok'
$needCmake = (Get-CmakeStatus).State -ne 'ok'
$needCompiler = -not (((Get-MsvcStatus).State -eq 'ok') -or ((Get-MingwStatus 'mingw64').State -eq 'ok') -or ((Get-MingwStatus 'mingw32').State -eq 'ok'))
if ($needGit) { Bad ' still missing (required): Git'; $exit = 1 }
if ($needCmake) { Bad ' still missing (required): CMake 3.25 or newer'; $exit = 1 }
if ($needCompiler) { Bad ' still missing (required): a C++ compiler (Visual Studio Build Tools, or MinGW-w64 via MSYS2)'; $exit = 1 }
if ($failed.Count -gt 0 -and $exit -eq 0) { Warn (' These did not install: ' + ($failed -join ', ')) }
return $exit
}
# Run only when executed, not when dot-sourced (a test dot-sources this file
# to replace the functions that touch the registry).
if ($MyInvocation.InvocationName -ne '.') {
$code = Invoke-Main
exit $code
}