Movimiento de "Cuadrado" veloz en mapa blanco

This commit is contained in:
Clinv committed 2026-09-26 14:45:07 -03:00
commit b695b60153
55 files changed
+9391

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
# Shared helpers for scripts/*.sh. Sourced, never run directly (hence the
# leading underscore and no executable bit); every script sources it after
# it has cd'd to the project root, because both helpers below run
# `cmake -P cmake/<something>.cmake` relative to that.
#
# There is deliberately no Windows twin of this file: scripts/*.bat each keep
# their own copy of these two small wrappers as labels (:read_config and
# :resolve_build_dir), because sharing labels between .bat files is more
# fragile than repeating them. What the wrappers *ask* is answered by the
# same cmake scripts either way, so no logic is duplicated -- only the glue.
# Reads one value out of Configuration.cmake -- this project's single
# source of truth -- via cmake/PrintConfig.cmake, instead of hardcoding a
# second copy of a default that can silently drift out of sync. See that
# file for exactly what this strips off of its output.
_pmn_read_config() {
cmake "-DPRINT_VARS=$1" -P cmake/PrintConfig.cmake | sed -E 's/^-- [^=]*=//'
}
# _pmn_resolve_build_dir [<name>]
#
# Prints the build directory to use. With a <name> (a preset, or "default")
# that is simply BUILD_OUTPUT_DIR/<name>; without one it is discovered: the
# directory scripts/build.sh built last, else the only one that exists. If
# neither works, the reason goes to stderr and the return status is 1.
#
# The logic itself is cmake/BuildDir.cmake's -- see it for the details and
# the one-line answer protocol parsed here.
_pmn_resolve_build_dir() {
local answer
# run.sh never needed cmake before it had to ask which build directory to
# use, so say what's wrong instead of letting the shell's own "command not
# found" and an empty answer speak for it.
if ! command -v cmake >/dev/null 2>&1; then
echo "error: cmake not found on PATH -- it is needed to work out which build directory to use." >&2
echo " Put it on PATH, or set BUILD_DIR to the directory explicitly, e.g. BUILD_DIR=out/build/<preset>." >&2
return 1
fi
answer="$(cmake "-DPMN_NAME=${1:-}" -P cmake/BuildDir.cmake | sed -E 's/^-- //')"
case "$answer" in
BUILD_DIR=*)
printf '%s\n' "${answer#BUILD_DIR=}"
;;
ERROR=*)
echo "error: ${answer#ERROR=}" >&2
return 1
;;
*)
echo "error: cmake/BuildDir.cmake did not answer as expected: ${answer}" >&2
return 1
;;
esac
}
# _pmn_record_build_dir <name>
#
# Remembers <name> as the build directory built last, so the next
# `scripts/run.sh` / `install.sh` / `docs.sh` without a --preset uses it.
_pmn_record_build_dir() {
cmake "-DPMN_NAME=$1" -DPMN_RECORD=ON -P cmake/BuildDir.cmake >/dev/null
}
# On macOS, Homebrew's llvm formula -- the one that ships clang-tidy -- is
# keg-only: installed, but deliberately not linked into PATH, so a script
# looking for `clang-tidy` can't find it. Add its bin directory to the END of
# PATH (never the front: Apple's clang must stay the one builds use). A
# no-op everywhere else, and when llvm isn't installed.
_pmn_extend_tool_path() {
[ "$(uname -s)" = Darwin ] || return 0
local d
for d in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do
if [ -d "$d" ]; then
case ":$PATH:" in
*":$d:"*) ;;
*) PATH="$PATH:$d" ;;
esac
return 0
fi
done
return 0
}
+581
View File
@@ -0,0 +1,581 @@
<#
.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
}
+626
View File
@@ -0,0 +1,626 @@
# Interactive installer for the tools this project builds with. Sourced by
# scripts/setup.sh (Linux and macOS -- Windows has scripts/_tools.ps1); never
# run directly, hence the leading underscore and no executable bit.
#
# What it does: works out what OS and package manager it's on, checks each
# tool in a catalog, and for every one that's 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, see below).
#
# Written for bash 3.2, which is what macOS ships as /bin/bash: no
# associative arrays, no mapfile, no ${var,,}, and every array that can be
# empty is expanded with the ${arr[@]+"${arr[@]}"} idiom.
#
# Package managers: apt (Debian/Ubuntu), dnf (Fedora/RHEL), pacman (Arch) on
# Linux; Homebrew on macOS. Anything else is told what the tools are and
# left to install them itself. The cross-compilers, MinGW and emulators
# are offered on apt systems only: that is the package layout
# cmake/toolchains/ was written and tested against (a cross toolchain's
# target files under /usr/<triplet>), and offering a package that would
# then not work is worse than not offering it.
#
# Entry point: _pmn_tools_run
# Environment in (set by setup.sh from its flags):
# PMN_TOOLS_YES=1 install REQUIRED tools without asking
# PMN_TOOLS_ALL=1 also install OPTIONAL tools without asking
# PMN_TOOLS_CHECK=1 only report; change nothing
# Returns 0 if every required tool is present at the end, 1 if not.
#
# Without a terminal on stdin and without --yes/--all it does not prompt (a
# CI job must never hang on a question nobody can answer): it reports and
# leaves everything alone.
# Oldest CMake this project supports (cmake_minimum_required in
# CMakeLists.txt, and the presets file's cmakeMinimumRequired).
PMN_CMAKE_MIN_MAJOR=3
PMN_CMAKE_MIN_MINOR=25
PMN_TOOLS_YES="${PMN_TOOLS_YES:-0}"
PMN_TOOLS_ALL="${PMN_TOOLS_ALL:-0}"
PMN_TOOLS_CHECK="${PMN_TOOLS_CHECK:-0}"
# Answers that carry over from one prompt to the next.
_PMN_ANS_ALL=0 # "a": yes to everything still to come
_PMN_ANS_SKIP_OPT=0 # "s": no to every OPTIONAL tool still to come
_PMN_ANS_QUIT=0 # "q": stop asking altogether
_PMN_APT_UPDATED=0 # apt-get update runs once, before the first apt install
if [ -t 1 ]; then
_PMN_BOLD=$'\033[1m'; _PMN_DIM=$'\033[2m'; _PMN_GREEN=$'\033[32m'
_PMN_YELLOW=$'\033[33m'; _PMN_RED=$'\033[31m'; _PMN_RESET=$'\033[0m'
else
_PMN_BOLD=''; _PMN_DIM=''; _PMN_GREEN=''; _PMN_YELLOW=''; _PMN_RED=''; _PMN_RESET=''
fi
# ---------------------------------------------------------------------------
# Detection
# ---------------------------------------------------------------------------
_pmn_have() { command -v "$1" >/dev/null 2>&1; }
# The two directories Homebrew's llvm formula is keg-only in (Apple Silicon,
# Intel). Keg-only: installed, but deliberately not linked into PATH, which
# is where clang-tidy comes from.
_pmn_brew_llvm_bin() {
local d
for d in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do
if [ -d "$d" ]; then printf '%s\n' "$d"; return 0; fi
done
return 1
}
# Sets PMN_OS (linux|macos|other), PMN_ARCH (x64|arm64|arm32|""), PMN_PM
# (apt|dnf|pacman|brew|none) and PMN_SUDO ("" or "sudo").
_pmn_tools_detect() {
local machine
case "$(uname -s)" in
Linux) PMN_OS=linux ;;
Darwin) PMN_OS=macos ;;
*) PMN_OS=other ;;
esac
machine="$(uname -m)"
# An x86_64 shell under Rosetta 2 on an Apple Silicon Mac still reports
# x86_64 -- ask about the machine (see cmake/HostArch.cmake).
if [ "$PMN_OS" = macos ] && [ "$machine" = x86_64 ] \
&& [ "$(sysctl -in hw.optional.arm64 2>/dev/null || true)" = 1 ]; then
machine=arm64
fi
case "$machine" in
x86_64|amd64) PMN_ARCH=x64 ;;
aarch64|arm64) PMN_ARCH=arm64 ;;
arm*) PMN_ARCH=arm32 ;;
*) PMN_ARCH="" ;;
esac
PMN_PM=none
if [ "$PMN_OS" = macos ]; then
# Homebrew is often installed but not on PATH in a fresh shell.
if ! _pmn_have brew; then
local b
for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$b" ]; then eval "$("$b" shellenv)"; break; fi
done
fi
if _pmn_have brew; then PMN_PM=brew; fi
elif [ "$PMN_OS" = linux ]; then
if _pmn_have apt-get; then PMN_PM=apt
elif _pmn_have dnf; then PMN_PM=dnf
elif _pmn_have pacman; then PMN_PM=pacman
fi
fi
# Root is only a question for Linux's system package managers. Homebrew
# refuses to run as root and never needs it -- and decided HERE, before
# a fresh Homebrew exists, this would otherwise be computed against
# PMN_PM=none.
PMN_SUDO=""
if [ "$PMN_OS" = linux ] && [ "$(id -u)" != 0 ]; then
if _pmn_have sudo; then PMN_SUDO=sudo; else PMN_SUDO="?"; fi
fi
}
# "3.28.3" for the installed cmake, or "" if there isn't one.
_pmn_cmake_version() {
_pmn_have cmake || return 0
cmake --version 2>/dev/null | sed -n '1s/^[^0-9]*\([0-9][0-9.]*\).*/\1/p'
}
# 0 if the installed cmake is at least the minimum.
_pmn_cmake_new_enough() {
local v major minor
v="$(_pmn_cmake_version)"
[ -n "$v" ] || return 1
major="${v%%.*}"; minor="${v#*.}"; minor="${minor%%.*}"
if [ "$major" -gt "$PMN_CMAKE_MIN_MAJOR" ]; then return 0; fi
[ "$major" -eq "$PMN_CMAKE_MIN_MAJOR" ] && [ "$minor" -ge "$PMN_CMAKE_MIN_MINOR" ]
}
# macOS: /usr/bin/git, /usr/bin/clang and friends exist on every Mac as
# stubs that pop up an installer dialog when run without the Command Line
# Tools -- `command -v` finds them regardless. The developer directory is
# what says whether real tools sit behind them.
_pmn_clt_installed() {
local dir
dir="$(xcode-select -p 2>/dev/null)" || return 1
[ -x "$dir/usr/bin/clang" ]
}
# _pmn_tool_status <id>: prints "ok <detail>", "old <detail>" or "missing".
_pmn_tool_status() {
local v p
case "$1" in
clt)
if _pmn_clt_installed; then echo "ok $(xcode-select -p)"; else echo missing; fi ;;
brew)
if _pmn_have brew; then echo "ok $(command -v brew)"; else echo missing; fi ;;
git)
if _pmn_have git; then echo "ok $(git --version | sed 's/^git version //')"; else echo missing; fi ;;
cmake)
v="$(_pmn_cmake_version)"
if [ -z "$v" ]; then echo missing
elif _pmn_cmake_new_enough; then echo "ok $v"
else echo "old $v"; fi ;;
cxx)
if _pmn_have g++; then echo "ok g++ $(g++ -dumpversion 2>/dev/null)"
elif _pmn_have clang++; then echo "ok clang++"
elif _pmn_have c++; then echo "ok c++"
else echo missing; fi ;;
clang)
if _pmn_have clang++; then echo "ok $(clang++ --version 2>/dev/null | sed -n '1s/.*version \([0-9][0-9.]*\).*/\1/p')"; else echo missing; fi ;;
clang-format|clang-tidy)
if _pmn_have "$1"; then echo "ok $(command -v "$1")"
elif p="$(_pmn_brew_llvm_bin)" && [ -x "$p/$1" ]; then echo "ok $p/$1"
else echo missing; fi ;;
xc-arm64) if _pmn_have aarch64-linux-gnu-g++; then echo "ok"; else echo missing; fi ;;
xc-arm32) if _pmn_have arm-linux-gnueabihf-g++; then echo "ok"; else echo missing; fi ;;
xc-x64) if _pmn_have x86_64-linux-gnu-g++; then echo "ok"; else echo missing; fi ;;
mingw64) if _pmn_have x86_64-w64-mingw32-g++; then echo "ok"; else echo missing; fi ;;
mingw32) if _pmn_have i686-w64-mingw32-g++; then echo "ok"; else echo missing; fi ;;
qemu) if _pmn_have qemu-aarch64 || _pmn_have qemu-arm || _pmn_have qemu-x86_64; then echo "ok"; else echo missing; fi ;;
wine) if _pmn_have wine || _pmn_have wine64; then echo "ok"; else echo missing; fi ;;
ninja) if _pmn_have ninja; then echo "ok $(ninja --version 2>/dev/null)"; else echo missing; fi ;;
*) echo missing ;;
esac
}
# ---------------------------------------------------------------------------
# The catalog
#
# id | kind | label | why | apt | dnf | pacman | brew
#
# "-" means "not offered through that package manager". Rows are filtered per
# platform by _pmn_tools_rows below, and listed in the order they're asked.
# ---------------------------------------------------------------------------
_pmn_row_cat() {
case "$1" in
git) echo "git|required|Git|FetchContent clones dependencies (fmt) with it|git|git|git|git" ;;
cmake) echo "cmake|required|CMake $PMN_CMAKE_MIN_MAJOR.$PMN_CMAKE_MIN_MINOR or newer|configures and drives every build|cmake|cmake|cmake|cmake" ;;
cxx) echo "cxx|required|C++ compiler (GCC)|compiles the project|g++|gcc-c++|gcc|-" ;;
clang) echo "clang|optional|Clang|the linux-*-clang presets (GCC alone is enough otherwise)|clang|clang|clang|-" ;;
clang-format) echo "clang-format|optional|clang-format|scripts/refactor.sh and the pre-commit hook|clang-format|clang-tools-extra|clang|clang-format" ;;
clang-tidy) echo "clang-tidy|optional|clang-tidy|scripts/refactor.sh and the pre-commit hook|clang-tidy|clang-tools-extra|clang|llvm" ;;
xc-arm64) echo "xc-arm64|optional|AArch64 cross compiler|the linux-arm64-* presets, built from this machine|g++-aarch64-linux-gnu|-|-|-" ;;
xc-arm32) echo "xc-arm32|optional|32-bit ARM cross compiler|the linux-arm32-* presets, built from this machine|g++-arm-linux-gnueabihf|-|-|-" ;;
xc-x64) echo "xc-x64|optional|x86-64 cross compiler|the linux-x64-* presets, built from this machine|g++-x86-64-linux-gnu|-|-|-" ;;
mingw64) echo "mingw64|optional|MinGW-w64 (64-bit Windows)|the windows-x64-mingw64 preset, cross-built from Linux|g++-mingw-w64-x86-64|-|-|-" ;;
mingw32) echo "mingw32|optional|MinGW-w64 (32-bit Windows)|the windows-x86-mingw32 preset, cross-built from Linux|g++-mingw-w64-i686|-|-|-" ;;
qemu) echo "qemu|optional|qemu-user|runs ARM cross builds (scripts/run.sh, ctest)|qemu-user|-|-|-" ;;
wine) echo "wine|optional|wine|runs MinGW cross builds (scripts/run.sh, ctest)|wine|-|-|-" ;;
ninja) echo "ninja|required|Ninja|the build generator (no pinned download exists for this machine)|ninja-build|ninja-build|ninja|ninja" ;;
clt) echo "clt|required|Xcode Command Line Tools|Apple Clang, git and make: the compiler for the macos-* presets|-|-|-|-" ;;
brew) echo "brew|required|Homebrew|installs CMake and the other tools below|-|-|-|-" ;;
esac
}
# The ids to consider on this platform, in the order they are asked.
_pmn_tools_ids() {
if [ "$PMN_OS" = macos ]; then
echo "clt brew cmake clang-format clang-tidy"
return
fi
local ids="git cmake cxx clang clang-format clang-tidy"
# Cross toolchains: apt only (see the file header), and only for
# architectures this machine can build FOR without being them.
if [ "$PMN_PM" = apt ]; then
case "$PMN_ARCH" in
x64) ids="$ids xc-arm64 xc-arm32 mingw64 mingw32 qemu wine" ;;
arm64) ids="$ids xc-x64 xc-arm32 mingw64 mingw32 qemu" ;;
esac
fi
echo "$ids"
}
# Field <n> (1-based) of a catalog row. A `read` into named variables rather
# than word-splitting the row: the rows contain "*" (linux-*-clang), and an
# unquoted expansion would be subject to pathname globbing.
_pmn_field() {
local f1 f2 f3 f4 f5 f6 f7 f8
IFS='|' read -r f1 f2 f3 f4 f5 f6 f7 f8 <<< "$1"
case "$2" in
1) printf '%s\n' "$f1" ;; 2) printf '%s\n' "$f2" ;; 3) printf '%s\n' "$f3" ;;
4) printf '%s\n' "$f4" ;; 5) printf '%s\n' "$f5" ;; 6) printf '%s\n' "$f6" ;;
7) printf '%s\n' "$f7" ;; 8) printf '%s\n' "$f8" ;;
esac
}
# The package(s) that provide a row's tool through the current package
# manager, or "" if it isn't offered there.
_pmn_row_packages() {
local row="$1" col
case "$PMN_PM" in
apt) col=5 ;; dnf) col=6 ;; pacman) col=7 ;; brew) col=8 ;; *) return 0 ;;
esac
local p
p="$(_pmn_field "$row" "$col")"
[ "$p" = "-" ] && p=""
printf '%s\n' "$p"
}
# ---------------------------------------------------------------------------
# Asking
# ---------------------------------------------------------------------------
_pmn_say() { printf '%s\n' "$*"; }
_pmn_note() { printf '%s\n' "${_PMN_DIM}$*${_PMN_RESET}"; }
# Would an OPTIONAL tool be offered (asked about, or installed by --all) at
# this point? If not, the caller skips it silently and mentions it once at
# the end, instead of printing a "left alone" block for each.
_pmn_optional_wanted() {
if [ "$_PMN_ANS_QUIT" = 1 ] || [ "$_PMN_ANS_SKIP_OPT" = 1 ]; then return 1; fi
if [ "$PMN_TOOLS_ALL" = 1 ] || [ "$_PMN_ANS_ALL" = 1 ]; then return 0; fi
[ -t 0 ]
}
# _pmn_confirm <required|optional> <question>
#
# Returns 0 for "install it", 1 for "leave it". Sets _PMN_ANS_* for answers
# that outlast one prompt (see their comments up top).
_pmn_confirm() {
local kind="$1" question="$2" prompt ans
if [ "$_PMN_ANS_QUIT" = 1 ]; then return 1; fi
if [ "$_PMN_ANS_ALL" = 1 ]; then return 0; fi
if [ "$kind" = optional ] && [ "$_PMN_ANS_SKIP_OPT" = 1 ]; then return 1; fi
# Flags answer for you...
if [ "$kind" = required ] && [ "$PMN_TOOLS_YES" = 1 ]; then return 0; fi
if [ "$PMN_TOOLS_ALL" = 1 ]; then return 0; fi
# ...and with no terminal there is nobody to ask.
if [ ! -t 0 ]; then
_pmn_note " (not asking: no terminal. Re-run interactively, or pass --yes / --all.)"
return 1
fi
if [ "$kind" = required ]; then
prompt=" ${question} [Y/n/a/q] "
else
prompt=" ${question} [y/N/a/s/q] "
fi
while :; do
if ! IFS= read -r -p "$prompt" ans; then
# EOF (Ctrl-D): stop asking rather than loop on it.
echo
_PMN_ANS_QUIT=1
return 1
fi
case "$ans" in
y|Y|yes|YES|Yes) return 0 ;;
n|N|no|NO|No) return 1 ;;
"") if [ "$kind" = required ]; then return 0; else return 1; fi ;;
a|A|all) _PMN_ANS_ALL=1; return 0 ;;
s|S|skip) _PMN_ANS_SKIP_OPT=1; return 1 ;;
q|Q|quit) _PMN_ANS_QUIT=1; return 1 ;;
*)
if [ "$kind" = required ]; then
_pmn_note " y = install n = not this one a = install everything left q = stop asking"
else
_pmn_note " y = install n = not this one a = install everything left s = skip the rest of the optional tools q = stop asking"
fi
;;
esac
done
}
# ---------------------------------------------------------------------------
# Installing
# ---------------------------------------------------------------------------
# _pmn_run_shown <command...>: print the command, then run it.
_pmn_run_shown() {
printf ' %s$ %s%s\n' "${_PMN_DIM}" "$*" "${_PMN_RESET}"
"$@"
}
# Sudo, when the package manager needs it. A machine that is neither root
# nor has sudo can't install system packages; say so once, plainly.
_pmn_pkg_install() {
local pkgs="$1" rc=0
if [ "$PMN_SUDO" = "?" ] && [ "$PMN_PM" != brew ]; then
_pmn_say " ${_PMN_RED}cannot install:${_PMN_RESET} this needs root, and sudo isn't available."
_pmn_say " Run this script as root, or install by hand: $pkgs"
return 1
fi
# shellcheck disable=SC2086 # $pkgs is a deliberate word list
case "$PMN_PM" in
apt)
if [ "$_PMN_APT_UPDATED" = 0 ]; then
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} apt-get update || true
_PMN_APT_UPDATED=1
fi
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} apt-get install -y $pkgs || rc=$?
;;
dnf)
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} dnf install -y $pkgs || rc=$?
;;
pacman)
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} pacman -S --needed --noconfirm $pkgs || rc=$?
;;
brew)
_pmn_run_shown brew install $pkgs || rc=$?
;;
*)
return 1 ;;
esac
return "$rc"
}
# macOS: `xcode-select --install` returns immediately after opening a GUI
# installer, so wait for the developer directory to appear. Enter re-checks
# right away; otherwise it looks every 5 seconds.
_pmn_install_clt() {
_pmn_run_shown xcode-select --install || true
_pmn_say " A dialog should have opened: click Install and wait for it to finish."
_pmn_say " Waiting for the tools (Enter checks now; Ctrl-C to stop waiting) ..."
local waited=0
until _pmn_clt_installed; do
# Not `read -t 5` with stdin at EOF: that returns at once and would
# spin. sleep is the wait; read only makes Enter an early re-check.
if [ -t 0 ]; then
IFS= read -r -t 5 _ 2>/dev/null || true
else
sleep 5
fi
waited=$((waited + 5))
if [ "$waited" -ge 3600 ]; then
_pmn_say " Gave up waiting after an hour -- re-run this script once the install has finished."
return 1
fi
done
return 0
}
# Homebrew's own documented installer. It asks for your password and for a
# RETURN of its own -- those prompts are Homebrew's, and are left alone
# unless --yes was given (then NONINTERACTIVE=1 tells it not to ask).
_pmn_install_brew() {
if ! _pmn_have curl; then
_pmn_say " curl not found -- see https://brew.sh for manual installation steps."
return 1
fi
printf ' %s$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"%s\n' "${_PMN_DIM}" "${_PMN_RESET}"
local rc=0
if [ "$PMN_TOOLS_YES" = 1 ] || [ "$PMN_TOOLS_ALL" = 1 ]; then
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || rc=$?
else
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || rc=$?
fi
# A fresh install isn't on PATH in this shell yet.
local b
for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$b" ]; then eval "$("$b" shellenv)"; PMN_PM=brew; break; fi
done
return "$rc"
}
# _pmn_install_row <row>: install one catalog row; returns its status.
_pmn_install_row() {
local row="$1" id pkgs
id="$(_pmn_field "$row" 1)"
case "$id" in
clt) _pmn_install_clt; return $? ;;
brew) _pmn_install_brew; return $? ;;
esac
pkgs="$(_pmn_row_packages "$row")"
if [ -z "$pkgs" ]; then
_pmn_say " ${_PMN_YELLOW}no package known for this on ${PMN_PM}${_PMN_RESET} -- install it yourself."
return 1
fi
_pmn_pkg_install "$pkgs"
}
# What to tell someone whose package manager's CMake is too old (Ubuntu 22.04
# ships 3.22, Debian 11 ships 3.18). Not run automatically: which of these is
# right depends on the machine, and each one changes more than a package.
_pmn_cmake_too_old_help() {
_pmn_say " ${_PMN_YELLOW}CMake $1 is older than the ${PMN_CMAKE_MIN_MAJOR}.${PMN_CMAKE_MIN_MINOR} this project needs.${_PMN_RESET} Any one of:"
_pmn_say " pipx install cmake # Kitware's own wheels (pipx from your package manager)"
_pmn_say " sudo snap install cmake --classic # if you use snap"
_pmn_say " https://cmake.org/download/ # official binary tarball"
}
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
# Rows are collected into an array first, then looped over -- rather than
# reading a here-doc in the loop -- so a command run inside the loop (a
# package manager, sudo) can never swallow the rest of the list from stdin.
_pmn_tools_run() {
local rows=() id row kind label why status detail pkgs missing_required=0
local failed=""
local pm_desc
_pmn_tools_detect
case "$PMN_OS" in
linux) pm_desc="Linux ${PMN_ARCH:-?}, package manager: ${PMN_PM}" ;;
macos) pm_desc="macOS ${PMN_ARCH:-?}, package manager: ${PMN_PM}" ;;
*) pm_desc="$(uname -s) (not supported by this installer)" ;;
esac
_pmn_say "==> Checking the tools this project builds with (${pm_desc})"
if [ "$PMN_OS" = other ]; then
_pmn_say " This installer knows Linux and macOS; on Windows use scripts\\setup.bat."
return 0
fi
if [ "$PMN_OS" = linux ] && [ "$PMN_PM" = none ]; then
_pmn_say " ${_PMN_YELLOW}No supported package manager found${_PMN_RESET} (apt, dnf and pacman are)."
_pmn_say " Install these yourself: a C++ compiler, git, CMake ${PMN_CMAKE_MIN_MAJOR}.${PMN_CMAKE_MIN_MINOR}+ (and clang-format/clang-tidy, optionally)."
fi
for id in $(_pmn_tools_ids); do
row="$(_pmn_row_cat "$id")"
[ -n "$row" ] && rows+=("$row")
done
# ---- report ----------------------------------------------------------
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"; kind="$(_pmn_field "$row" 2)"; label="$(_pmn_field "$row" 3)"
status="$(_pmn_tool_status "$id")"
detail="${status#* }"; [ "$detail" = "$status" ] && detail=""
case "${status%% *}" in
ok)
if [ -n "$detail" ]; then
printf ' %s[ ok ]%s %-30s %s%s%s\n' "$_PMN_GREEN" "$_PMN_RESET" "$label" "$_PMN_DIM" "$detail" "$_PMN_RESET"
else
printf ' %s[ ok ]%s %s\n' "$_PMN_GREEN" "$_PMN_RESET" "$label"
fi ;;
old)
printf ' %s[old ]%s %-30s %s (needs %s.%s+)\n' "$_PMN_YELLOW" "$_PMN_RESET" "$label" "$detail" "$PMN_CMAKE_MIN_MAJOR" "$PMN_CMAKE_MIN_MINOR" ;;
*)
if [ "$kind" = required ]; then
printf ' %s[ !! ]%s %-30s missing (required)\n' "$_PMN_RED" "$_PMN_RESET" "$label"
else
printf ' %s[ -- ]%s %-30s missing (optional)\n' "$_PMN_DIM" "$_PMN_RESET" "$label"
fi ;;
esac
done
if [ "$PMN_TOOLS_CHECK" = 1 ]; then
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"
if [ "$(_pmn_field "$row" 2)" = required ] && [ "$(_pmn_tool_status "$id" | cut -d' ' -f1)" != ok ]; then
# brew is only "required" for what it installs; not a failure on its own if nothing needs it
[ "$id" = brew ] && continue
missing_required=1
fi
done
return "$missing_required"
fi
# ---- offer installs, one at a time -----------------------------------
local offered=0 skipped_optional=""
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"; kind="$(_pmn_field "$row" 2)"; label="$(_pmn_field "$row" 3)"
why="$(_pmn_field "$row" 4)"
# Re-check now: an earlier install may already have provided this
# one (dnf's clang-tools-extra is both clang-format and clang-tidy).
status="$(_pmn_tool_status "$id")"
if [ "${status%% *}" = ok ]; then continue; fi
if [ "$_PMN_ANS_QUIT" = 1 ]; then break; fi
# Everything through Homebrew needs Homebrew first.
if [ "$PMN_OS" = macos ] && [ "$id" != clt ] && [ "$id" != brew ] && [ "$PMN_PM" != brew ]; then
_pmn_say
_pmn_say " ${_PMN_BOLD}${label}${_PMN_RESET} -- ${why}"
_pmn_note " skipped: needs Homebrew, which isn't installed"
failed="$failed $id"
continue
fi
if [ "$PMN_OS" = linux ] && [ "$id" != brew ]; then
pkgs="$(_pmn_row_packages "$row")"
if [ "$PMN_PM" = none ] || [ -z "$pkgs" ]; then
continue # nothing this package manager can do; already said so above
fi
fi
if [ "$kind" = optional ] && ! _pmn_optional_wanted; then
skipped_optional="${skipped_optional:+$skipped_optional, }$label"
continue
fi
offered=$((offered + 1))
_pmn_say
_pmn_say " ${_PMN_BOLD}${label}${_PMN_RESET} -- ${why}"
if [ "${status%% *}" = old ]; then
_pmn_note " (installed: ${status#* }, but too old)"
fi
if _pmn_confirm "$kind" "Install ${label}?"; then
if _pmn_install_row "$row"; then
status="$(_pmn_tool_status "$id")"
case "${status%% *}" in
ok) _pmn_say " ${_PMN_GREEN}installed${_PMN_RESET}" ;;
old) _pmn_cmake_too_old_help "${status#* }"; failed="$failed $id" ;;
*) _pmn_say " ${_PMN_YELLOW}the install finished, but ${label} still isn't on PATH${_PMN_RESET} -- open a new terminal, or check the output above."
failed="$failed $id" ;;
esac
else
_pmn_say " ${_PMN_RED}install failed${_PMN_RESET} -- see the output above."
failed="$failed $id"
fi
else
# Declined (or quit): nothing to echo back to someone who just
# typed it, but an optional one belongs in the closing summary.
if [ "$kind" = optional ]; then
skipped_optional="${skipped_optional:+$skipped_optional, }$label"
fi
fi
done
if [ "$offered" -eq 0 ] && [ -z "$skipped_optional" ] && [ "$_PMN_ANS_QUIT" != 1 ]; then
_pmn_say " Nothing to install."
fi
if [ -n "$skipped_optional" ]; then
_pmn_say
_pmn_note " Optional, not installed: ${skipped_optional}"
_pmn_note " (scripts/setup.sh, run interactively, offers each one; --all installs them all.)"
fi
# ---- final verdict ---------------------------------------------------
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"
if [ "$(_pmn_field "$row" 2)" = required ] && [ "$id" != brew ] \
&& [ "$(_pmn_tool_status "$id" | cut -d' ' -f1)" != ok ]; then
missing_required=1
_pmn_say " ${_PMN_RED}still missing (required):${_PMN_RESET} $(_pmn_field "$row" 3)"
fi
done
return "$missing_required"
}
# _pmn_tools_offer <id>: offer to install ONE catalog tool -- for callers that
# find out mid-way that they need it (setup.sh, when there is no pinned Ninja
# for this machine). Same prompt, same command display, same rules.
_pmn_tools_offer() {
local id="$1" row status
_pmn_tools_detect
row="$(_pmn_row_cat "$id")"
[ -n "$row" ] || return 1
status="$(_pmn_tool_status "$id")"
[ "${status%% *}" = ok ] && return 0
if [ -z "$(_pmn_row_packages "$row")" ] || [ "$PMN_PM" = none ]; then
_pmn_say " Install $(_pmn_field "$row" 3) yourself: $(_pmn_field "$row" 4)."
return 1
fi
_pmn_say
_pmn_say " ${_PMN_BOLD}$(_pmn_field "$row" 3)${_PMN_RESET} -- $(_pmn_field "$row" 4)"
if _pmn_confirm "$(_pmn_field "$row" 2)" "Install $(_pmn_field "$row" 3)?"; then
if ! _pmn_install_row "$row"; then
_pmn_say " ${_PMN_RED}install failed${_PMN_RESET} -- see the output above."
return 1
fi
if [ "$(_pmn_tool_status "$id" | cut -d' ' -f1)" = ok ]; then
_pmn_say " ${_PMN_GREEN}installed${_PMN_RESET}"
return 0
fi
_pmn_say " ${_PMN_YELLOW}the install finished, but $(_pmn_field "$row" 3) still isn't on PATH${_PMN_RESET} -- open a new terminal, or check the output above."
return 1
fi
return 1
}
+358
View File
@@ -0,0 +1,358 @@
@echo off
REM Configure (if needed) and build the project, preferably with Ninja.
REM
REM Usage (from the project root):
REM scripts\build.bat [--preset ^<name^>] [BUILD_TYPE] [CMAKE_ARGS...]
REM scripts\build.bat --list-presets
REM scripts\build.bat --help
REM
REM --preset <name> Use a CMakePresets.json preset. A preset selects that
REM target's toolchain file (cmake\toolchains\<name>.cmake),
REM which picks the compiler and declares the DIST_TARGET
REM together, instead of you having to pass a matching
REM pair by hand:
REM scripts\build.bat --preset windows-x64-mingw64
REM Each preset builds in its own directory,
REM out\build\<name>, so switching between presets never
REM invalidates -- or deletes -- another one's build.
REM Run --list-presets to see every preset for your OS.
REM Must come first if given at all; --preset=<name>
REM also works. Given with no value (or a value that
REM looks like another flag), this fails fast with that
REM same preset list shown right there. A value that IS
REM given but isn't a real preset name is instead
REM caught by cmake itself failing to configure, at
REM which point this script shows the preset list too
REM -- see the "configure" step below.
REM BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel
REM (default: Configuration.cmake's DEFAULT_BUILD_TYPE)
REM CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
REM scripts\build.bat --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
REM scripts\build.bat -DBUILD_TESTS=ON
REM (BUILD_TYPE omitted -- recognized by not
REM starting with "-")
REM
REM Picking a DIST_TARGET: three ways; the first two set the compiler and the
REM label together, and this script rejects mixing --preset with a
REM -DDIST_TARGET rather than leaving it ambiguous which one wins.
REM 1. --preset <name> The recommended way; see --list-presets.
REM 2. -DCMAKE_TOOLCHAIN_FILE=cmake\toolchains\<name>.cmake
REM (no --preset) What a preset does, by hand, as one of
REM CMAKE_ARGS above.
REM 3. -DDIST_TARGET=<value> Sets the label alone, as one of CMAKE_ARGS
REM (no --preset) above. Without also passing a matching
REM compiler (or, better, the toolchain file
REM above), DIST_TARGET is auto-detected from
REM whatever compiler CMake finds on its own; if
REM the two disagree, Configuration.cmake
REM FATAL_ERRORs rather than silently building
REM the wrong toolchain and mislabeling the
REM result -- see that file and the README.
REM Passing --preset together with an explicit -DDIST_TARGET=... in
REM CMAKE_ARGS is rejected by this script before cmake even runs: the
REM preset's toolchain already declares DIST_TARGET, so a second value
REM alongside it is always either redundant or contradictory -- never
REM useful.
REM
REM cmd.exe splits %1, %2, ... (and SHIFT) on space, comma, semicolon, AND
REM "=" -- type "-DFOO=BAR" as an argument and it arrives as TWO separate
REM parameters, %1=-DFOO %2=BAR, silently, with no way to tell after the
REM fact. This script never reads a "="-laden argument (or --preset's own
REM value) through %1 or SHIFT; everything is peeled off of the raw,
REM unsplit %* instead using FOR /F (which only ever splits on space/tab),
REM so "=" survives intact -- see the parsing block below. (bash's
REM "$@"/shift has no such bug -- see build.sh for that half of this same
REM feature.)
REM
REM Build directory: out\build\<preset> for --preset <preset>; without a
REM preset, out\build\default (the compiler is auto-detected there, so keep
REM to one compiler per directory or use presets). Set BUILD_DIR to use an
REM exact directory of your own instead. scripts\run.bat, install.bat and
REM docs.bat use the directory built last, or --preset <name> to pick another.
setlocal enabledelayedexpansion
REM -h/--help is checked here, off the raw %1, before anything else --
REM including whether cmake is even on PATH below -- since help shouldn't
REM depend on that.
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\build.bat [--preset ^<name^>] [BUILD_TYPE] [CMAKE_ARGS...]
echo scripts\build.bat --list-presets
echo scripts\build.bat --help
echo.
echo --preset ^<name^> Use a CMakePresets.json preset: it selects the target's
echo toolchain file ^(compiler and DIST_TARGET together^) and
echo builds in its own directory, out\build\^<name^>. Must come
echo first if given at all; --preset=^<name^> also works. Run
echo --list-presets to see every preset for your OS.
echo BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel
echo ^(default: Configuration.cmake's DEFAULT_BUILD_TYPE^)
echo CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.
echo -DDIST_TARGET=^<value^> or -DBUILD_TESTS=ON.
echo.
echo Don't combine --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS --
echo the preset's toolchain already declares one; see the comment at the top of
echo this script ^(scripts\build.bat^) and Configuration.cmake's "Target selection"
echo section for why picking a DIST_TARGET is one or the other, not both.
echo.
echo Examples:
echo scripts\build.bat --preset linux-x64-clang
echo scripts\build.bat --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
echo scripts\build.bat -DBUILD_TESTS=ON
exit /b 0
:after_help
cd /d "%~dp0.."
REM An explicit BUILD_DIR from the environment beats the one-directory-per-
REM preset layout resolved below, and is then also not recorded as "the
REM build made last" (run/install/docs would otherwise be sent to a
REM directory the layout doesn't know about).
set "_pmn_record_build=1"
if not "%BUILD_DIR%"=="" set "_pmn_record_build=0"
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH.
exit /b 1
)
REM See the header comment above for why this doesn't use %1/SHIFT.
set "ALL_ARGS=%*"
set "PRESET="
set "BUILD_TYPE="
set "EXTRA_ARGS="
if not defined ALL_ARGS goto args_done
for /f "tokens=1,* delims= " %%A in ("%ALL_ARGS%") do (
set "_FIRST=%%A"
set "_REST=%%B"
)
if /i "%_FIRST%"=="--list-presets" (
cmake --list-presets
exit /b !ERRORLEVEL!
)
if /i "%_FIRST%"=="--preset" goto preset_space_form
if /i "%_FIRST:~0,9%"=="--preset=" goto preset_equals_form
goto no_preset
:preset_space_form
if not defined _REST (
echo error: --preset requires a value, e.g. --preset windows-x64-mingw64
echo.
cmake --list-presets
exit /b 1
)
for /f "tokens=1,* delims= " %%A in ("%_REST%") do (
set "PRESET=%%A"
set "_REST=%%B"
)
if "%PRESET:~0,1%"=="-" (
echo error: --preset requires a value, e.g. --preset windows-x64-mingw64
echo.
cmake --list-presets
exit /b 1
)
goto have_preset
:preset_equals_form
set "PRESET=%_FIRST:~9%"
if not defined PRESET (
echo error: --preset= requires a value, e.g. --preset=windows-x64-mingw64
echo.
cmake --list-presets
exit /b 1
)
goto have_preset
:have_preset
set "ALL_ARGS=%_REST%"
if not defined ALL_ARGS goto args_done
for /f "tokens=1,* delims= " %%A in ("%ALL_ARGS%") do (
set "_FIRST=%%A"
set "_REST=%%B"
)
:no_preset
if "%_FIRST:~0,1%"=="-" (
set "EXTRA_ARGS=%ALL_ARGS%"
) else (
set "BUILD_TYPE=%_FIRST%"
set "EXTRA_ARGS=%_REST%"
)
:args_done
if "%BUILD_TYPE%"=="" call :read_config DEFAULT_BUILD_TYPE BUILD_TYPE
if "%TOOLCACHE_DIR%"=="" call :read_config TOOLCACHE_DIR TOOLCACHE_DIR
REM One build directory per preset (out\build\<preset>; "default" when no
REM preset was given), so switching presets never has to reuse -- or wipe -- a
REM directory configured for another compiler. cmake\BuildDir.cmake owns the
REM layout (Configuration.cmake's BUILD_OUTPUT_DIR); goto rather than an
REM if-block for the reason given further down about nesting.
set "_pmn_name=default"
if defined PRESET set "_pmn_name=%PRESET%"
if not "%BUILD_DIR%"=="" goto build_dir_known
call :resolve_build_dir "%_pmn_name%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
REM Combining --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS is
REM ambiguous (the preset already sets one) -- caught here, before cmake
REM ever runs, rather than leaving it to whichever of the two CMake
REM happens to prefer. findstr's /C: does a literal (non-regex) substring
REM search, which sidesteps cmd.exe's own "=" splitting quirk (see the
REM header comment) entirely -- there's no argument-parsing involved here,
REM just a plain string search over EXTRA_ARGS as already assembled.
if defined PRESET (
echo %EXTRA_ARGS%| findstr /C:"-DDIST_TARGET=" >nul
if not errorlevel 1 (
echo error: both --preset %PRESET% and an explicit -DDIST_TARGET=... were given.
echo A preset's toolchain already declares DIST_TARGET ^(and picks the compiler^) -- pick one:
echo scripts\build.bat --preset %PRESET%
echo scripts\build.bat -DCMAKE_TOOLCHAIN_FILE=cmake\toolchains\^<target^>.cmake
echo See the comment at the top of this script, or Configuration.cmake's
echo "Target selection" section, for why these two aren't combined.
exit /b 1
)
)
REM Prefer a pinned ninja (fetched by scripts\setup.bat -- see
REM cmake\FetchNinja.cmake) over a system one, same "always the pinned
REM copy" preference as Doxygen. Needed whether or not --preset is used:
REM every preset sets its generator to Ninja but doesn't hardcode which
REM ninja binary to use (that would just be TOOLCACHE_DIR's default
REM duplicated a third time -- see Configuration.cmake) --
REM CMAKE_MAKE_PROGRAM supplies it either way; only -G Ninja itself is
REM skipped below when a preset already sets the generator.
REM
REM Deliberately goto-based, and deliberately `pushd` + a bare wildcard
REM rather than `for /d %%D in ("path\*")` -- both tested directly; a
REM path-prefixed wildcard didn't match here, this form does, and it also
REM sidesteps nesting a FOR loop inside an if/else block, which
REM separately corrupts cmd.exe's paren-matching for the enclosing block
REM (see scripts\refactor.bat for that one).
set "NINJA_BIN="
if not exist "%TOOLCACHE_DIR%" goto check_system_ninja
pushd "%TOOLCACHE_DIR%"
for /d %%D in (ninja-*) do if exist "%%D\ninja.exe" if not defined NINJA_BIN set "NINJA_BIN=%%~fD\ninja.exe"
popd
if defined NINJA_BIN goto have_pinned_ninja
:check_system_ninja
where ninja >nul 2>nul
if errorlevel 1 goto no_ninja
echo ==^> Using system ninja
if not defined PRESET set "GENERATOR_ARGS=-G Ninja"
goto configure
:have_pinned_ninja
echo ==^> Using pinned ninja: %NINJA_BIN%
if defined PRESET (
set "GENERATOR_ARGS=-DCMAKE_MAKE_PROGRAM=%NINJA_BIN%"
) else (
set "GENERATOR_ARGS=-G Ninja -DCMAKE_MAKE_PROGRAM=%NINJA_BIN%"
)
goto configure
:no_ninja
if defined PRESET (
echo warning: ninja not found ^(no pinned copy, none on PATH^) -- the '%PRESET%'
echo preset requires it; configure will likely fail. Run
echo scripts\setup.bat to fetch a pinned ninja, or install ninja
echo yourself.
) else (
echo warning: ninja not found ^(no pinned copy, none on PATH^) -- falling back to
echo CMake's default generator for this platform. Run scripts\setup.bat
echo to fetch a pinned ninja, or install ninja yourself, for a faster
echo and more consistent build. Continuing without it.
)
set "GENERATOR_ARGS="
:configure
echo ==^> Configuring (%BUILD_TYPE%) into %BUILD_DIR%\
if defined PRESET echo Preset: %PRESET%
if defined EXTRA_ARGS echo Extra CMake arguments: %EXTRA_ARGS%
if defined PRESET (
cmake --preset %PRESET% -S . -B "%BUILD_DIR%" %GENERATOR_ARGS% -DCMAKE_BUILD_TYPE=%BUILD_TYPE% %EXTRA_ARGS%
) else (
cmake %GENERATOR_ARGS% -S . -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=%BUILD_TYPE% %EXTRA_ARGS%
)
if errorlevel 1 (
if defined PRESET (
echo.
echo error: configuring with preset '%PRESET%' failed -- see the cmake output above.
echo If '%PRESET%' isn't the preset name you meant, here is what --list-presets
echo shows for this OS:
echo.
cmake --list-presets
)
exit /b 1
)
REM Configure succeeded: this is now the build the other scripts default to.
if "%_pmn_record_build%"=="1" cmake "-DPMN_NAME=%_pmn_name%" -DPMN_RECORD=ON -P cmake\BuildDir.cmake >nul
echo ==^> Building
cmake --build "%BUILD_DIR%"
if errorlevel 1 exit /b 1
echo ==^> Done. Run a module with: scripts\run.bat ^<target-name^>
exit /b 0
REM ---------------------------------------------------------------------
REM :read_config <ConfigurationVarName> <LocalVarName>
REM
REM Reads one value out of Configuration.cmake -- this project's single
REM source of truth -- via cmake\PrintConfig.cmake, instead of hardcoding
REM a second copy of a default that can silently drift out of sync, and
REM stores it into %2 in the CALLER's scope. See cmake\PrintConfig.cmake
REM for exactly what's being stripped off of its output below.
REM ---------------------------------------------------------------------
:read_config
setlocal
set "_pmn_line="
for /f "usebackq delims=" %%V in (`cmake "-DPRINT_VARS=%~1" -P cmake\PrintConfig.cmake`) do set "_pmn_line=%%V"
for /f "tokens=1,* delims==" %%A in ("%_pmn_line%") do set "_pmn_value=%%B"
endlocal & set "%~2=%_pmn_value%"
goto :eof
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as :read_config above, and the same
REM `cmake -P` one-line-answer protocol: "-- BUILD_DIR=<path>" or
REM "-- ERROR=<text>" (see that file). On failure it prints the reason to
REM stderr, unless a third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env bash
# Configure (if needed) and build the project, preferably with Ninja.
#
# Usage (from the project root):
# scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
# scripts/build.sh --list-presets
# scripts/build.sh --help
#
# --preset <name> Use a CMakePresets.json preset. A preset selects that
# target's toolchain file (cmake/toolchains/<name>.cmake),
# which picks the compiler and declares the DIST_TARGET
# together, instead of you having to pass a matching
# pair by hand:
# scripts/build.sh --preset windows-x64-mingw64
# Each preset builds in its own directory,
# out/build/<name>, so switching between presets never
# invalidates -- or deletes -- another one's build.
# Run --list-presets to see every preset for your OS.
# Must come first if given at all; --preset=<name> also
# works. Given with no value, or a value that isn't one
# of --list-presets' own, this fails fast with that
# same preset list shown right there, rather than
# letting a typo fall through to a less obvious `cmake`
# error further down.
# BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
# (default: Configuration.cmake's DEFAULT_BUILD_TYPE)
# CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
# scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
# scripts/build.sh -DBUILD_TESTS=ON
# (BUILD_TYPE omitted -- recognized by not
# starting with "-")
#
# Picking a DIST_TARGET: three ways; the first two set the compiler and the
# label together, and this script rejects mixing --preset with a
# -DDIST_TARGET rather than leaving it ambiguous which one wins.
# 1. --preset <name> The recommended way; see --list-presets.
# 2. -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<name>.cmake
# (no --preset) What a preset does, by hand, as one of
# CMAKE_ARGS above.
# 3. -DDIST_TARGET=<value> Sets the label alone, as one of CMAKE_ARGS
# (no --preset) above. Without also passing a matching
# compiler (or, better, the toolchain file
# above), DIST_TARGET is auto-detected from
# whatever compiler CMake finds on its own; if
# the two disagree, Configuration.cmake
# FATAL_ERRORs rather than silently building
# the wrong toolchain and mislabeling the
# result -- see that file and the README.
# Passing --preset together with an explicit -DDIST_TARGET=... in
# CMAKE_ARGS is rejected by this script before cmake even runs: the
# preset's toolchain already declares DIST_TARGET, so a second value
# alongside it is always either redundant (same value) or contradictory
# (different value) -- never useful.
#
# Build directory: out/build/<preset> for --preset <preset>; without a
# preset, out/build/default (the compiler is auto-detected there, so keep
# to one compiler per directory or use presets). Set BUILD_DIR to use an
# exact directory of your own instead. scripts/run.sh, install.sh and
# docs.sh use the directory built last, or --preset <name> to pick another.
set -euo pipefail
shopt -s nullglob
usage() {
cat <<'EOF'
Usage: scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
scripts/build.sh --list-presets
scripts/build.sh --help
--preset <name> Use a CMakePresets.json preset: it selects the target's
toolchain file (compiler and DIST_TARGET together) and
builds in its own directory, out/build/<name>. Must come
first if given at all; --preset=<name> also works. Run
--list-presets to see every preset for your OS.
BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
(default: Configuration.cmake's DEFAULT_BUILD_TYPE)
CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.
-DDIST_TARGET=<value> or -DBUILD_TESTS=ON.
Don't combine --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS --
the preset's toolchain already declares one; see the comment at the top of
this script (scripts/build.sh) and Configuration.cmake's "Target selection"
section for why picking a DIST_TARGET is one or the other, not both.
Examples:
scripts/build.sh --preset linux-x64-clang
scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
scripts/build.sh -DBUILD_TESTS=ON
EOF
}
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
command -v cmake >/dev/null 2>&1 || { echo "error: cmake not found on PATH." >&2; exit 1; }
if [ "${1:-}" = "--list-presets" ]; then
exec cmake --list-presets
fi
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
source "$SCRIPT_DIR/_common.sh"
# Prints CMake's own preset listing (already filtered to this OS by each
# preset's "condition" in CMakePresets.json) to stderr -- used to show the
# actual options right alongside a preset-related error below, instead of
# just telling the user to go run --list-presets separately.
_pmn_show_presets() {
echo "" >&2
cmake --list-presets >&2
}
# Membership test against that same listing -- $1 matches a real preset
# name for this OS only if it appears there as a quoted entry (anchored on
# both quote characters so e.g. "windows-x64-mingw32" can't false-match a
# check for "windows-x64-mingw3").
_pmn_known_preset() {
cmake --list-presets 2>/dev/null | grep -qF "\"$1\""
}
_pmn_has_explicit_dist_target() {
local arg
for arg in "$@"; do
case "$arg" in
-DDIST_TARGET=*) return 0 ;;
esac
done
return 1
}
# --preset must come first if given at all -- see Configuration.cmake's
# and CMakePresets.json's own comments for why this is the recommended
# way to pick a non-default DIST_TARGET: its toolchain file sets the
# compiler and the label together, instead of you having to pass both by
# hand.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset windows-x64-mingw64" >&2
_pmn_show_presets
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=windows-x64-mingw64" >&2
_pmn_show_presets
exit 1
fi
;;
esac
if [ -n "$PRESET" ] && ! _pmn_known_preset "$PRESET"; then
echo "error: '${PRESET}' is not a preset --list-presets knows about for this OS." >&2
_pmn_show_presets
exit 1
fi
# One build directory per preset (out/build/<preset>; "default" when no
# preset was given), so switching presets never has to reuse -- or wipe -- a
# directory configured for another compiler. cmake/BuildDir.cmake owns the
# layout (Configuration.cmake's BUILD_OUTPUT_DIR); an explicit BUILD_DIR
# from the environment beats it, and is then also not recorded as "the one
# built last" (run/install/docs would otherwise be sent to a directory the
# layout doesn't know about).
_pmn_record_build=1
if [ -n "${BUILD_DIR:-}" ]; then
_pmn_record_build=0
else
BUILD_DIR="$(_pmn_resolve_build_dir "${PRESET:-default}")"
fi
# If the first remaining argument doesn't start with "-", it's BUILD_TYPE
# and everything after it is CMAKE_ARGS; otherwise there's no BUILD_TYPE
# override and every argument is CMAKE_ARGS. Plain bash "$@"/shift, no
# special-casing needed: unlike Windows cmd.exe (see build.bat), bash
# never splits a "-DFOO=BAR"-style argument on its own "=".
if [ "$#" -gt 0 ] && [[ "$1" != -* ]]; then
BUILD_TYPE="$1"
shift
else
BUILD_TYPE="$(_pmn_read_config DEFAULT_BUILD_TYPE)"
fi
CMAKE_ARGS=("$@")
# ${arr[@]+"${arr[@]}"} rather than a plain "${arr[@]}" wherever an array can be
# empty: under `set -u`, bash before 4.4 -- which includes the 3.2 that macOS
# still ships as /bin/bash -- treats expanding an EMPTY array as an unset
# variable and aborts with "unbound variable".
if [ -n "$PRESET" ] && _pmn_has_explicit_dist_target ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}; then
echo "error: both --preset ${PRESET} and an explicit -DDIST_TARGET=... were given." >&2
echo " A preset's toolchain already declares DIST_TARGET (and picks the compiler) -- pick one:" >&2
echo " scripts/build.sh --preset ${PRESET}" >&2
echo " scripts/build.sh -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<target>.cmake" >&2
echo " See the comment at the top of this script, or Configuration.cmake's" >&2
echo " \"Target selection\" section, for why these two aren't combined." >&2
exit 1
fi
# Prefer a pinned ninja (fetched by scripts/setup.sh -- see
# cmake/FetchNinja.cmake) over a system one, same "always the pinned copy"
# preference as Doxygen. Needed whether or not --preset is used: every
# preset sets its generator to Ninja but doesn't hardcode which ninja
# binary to use (that would just be TOOLCACHE_DIR's default duplicated a
# third time -- see Configuration.cmake) -- CMAKE_MAKE_PROGRAM supplies it
# either way; only -G Ninja itself is skipped below when a preset already
# sets the generator.
TOOLCACHE_DIR="${TOOLCACHE_DIR:-$(_pmn_read_config TOOLCACHE_DIR)}"
NINJA_BIN=""
for _candidate in "${TOOLCACHE_DIR}"/ninja-*/ninja; do
if [ -x "$_candidate" ]; then
# Must be absolute: CMake invokes CMAKE_MAKE_PROGRAM from several
# different working directories during its own internal
# try-compile steps, where a relative path doesn't resolve.
NINJA_BIN="$(cd "$(dirname "$_candidate")" && pwd)/$(basename "$_candidate")"
break
fi
done
GENERATOR_ARGS=()
if [ -n "$NINJA_BIN" ]; then
echo "==> Using pinned ninja: ${NINJA_BIN}"
if [ -z "$PRESET" ]; then
GENERATOR_ARGS=(-G Ninja "-DCMAKE_MAKE_PROGRAM=${NINJA_BIN}")
else
GENERATOR_ARGS=("-DCMAKE_MAKE_PROGRAM=${NINJA_BIN}")
fi
elif command -v ninja >/dev/null 2>&1; then
echo "==> Using system ninja: $(command -v ninja)"
if [ -z "$PRESET" ]; then
GENERATOR_ARGS=(-G Ninja)
fi
else
if [ -n "$PRESET" ]; then
echo "warning: ninja not found (no pinned copy, none on PATH) -- the '${PRESET}'" >&2
echo " preset requires it; configure will likely fail. Run" >&2
echo " scripts/setup.sh to fetch a pinned ninja, or install ninja" >&2
echo " yourself." >&2
else
echo "warning: ninja not found (no pinned copy, none on PATH) -- falling back to" >&2
echo " CMake's default generator for this platform. Run scripts/setup.sh" >&2
echo " to fetch a pinned ninja, or install ninja yourself, for a faster" >&2
echo " and more consistent build. Continuing without it." >&2
fi
fi
CONFIGURE_ARGS=(-S . -B "${BUILD_DIR}")
if [ -n "$PRESET" ]; then
CONFIGURE_ARGS=(--preset "${PRESET}" "${CONFIGURE_ARGS[@]}")
fi
echo "==> Configuring (${BUILD_TYPE}) into ${BUILD_DIR}/"
if [ -n "$PRESET" ]; then
echo " Preset: ${PRESET}"
fi
if [ "${#CMAKE_ARGS[@]}" -gt 0 ]; then
echo " Extra CMake arguments: ${CMAKE_ARGS[*]}"
fi
cmake "${CONFIGURE_ARGS[@]}" ${GENERATOR_ARGS[@]+"${GENERATOR_ARGS[@]}"} -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}
# Configure succeeded: this is now the build the other scripts default to.
if [ "$_pmn_record_build" = 1 ]; then
_pmn_record_build_dir "${PRESET:-default}"
fi
echo "==> Building"
cmake --build "${BUILD_DIR}"
echo "==> Done. Run a module with: scripts/run.sh <target-name>"
+136
View File
@@ -0,0 +1,136 @@
@echo off
REM Build Doxygen documentation for every module (see cmake\Documentation.cmake)
REM and print the path to the landing page. A no-op with a clear message if
REM Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
REM
REM Output goes to out\docs\ (DOCS_OUTPUT_DIR in Configuration.cmake) --
REM note this is a sibling of out\build, not nested inside it, and shared by
REM every preset: documentation doesn't depend on which compiler built the
REM code. The docs targets just need SOME configured build directory to run
REM from, so this uses the one built last unless you pick with --preset. If
REM you've overridden DOCS_OUTPUT_DIR at configure time, also set DOCS_DIR here
REM to match so the printed path is correct (docs still land in the right
REM place either way -- this only affects the message below).
REM
REM Usage (from the project root):
REM scripts\docs.bat REM every module
REM scripts\docs.bat core_lib REM just one module's docs (docs_<target>)
REM scripts\docs.bat --preset windows-x64-clang-cl REM ... from that preset's build directory
REM scripts\docs.bat --help
REM
REM --preset must come first if given at all; --preset=<name> also works. Set
REM BUILD_DIR to use an exact build directory instead.
setlocal
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\docs.bat [--preset ^<name^>] [module-target-name]
echo.
echo Builds Doxygen documentation for every module ^(see cmake\Documentation.cmake^)
echo and prints the path to the landing page. A no-op with a clear message if
echo Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
echo.
echo --preset ^<name^> Run from that preset's build directory
echo ^(out\build\^<name^>^). Default: the build
echo scripts\build.bat made last. Must come first if
echo given at all; --preset=^<name^> also works.
echo ^(no argument^) Build docs for every module.
echo module-target-name Build docs for just that one module ^(its own
echo docs_^<name^> target^), e.g.:
echo scripts\docs.bat core_lib
echo.
echo Output goes to out\docs\ ^(DOCS_OUTPUT_DIR in Configuration.cmake^), shared
echo by every preset -- see the comment at the top of this script if you've
echo overridden that.
exit /b 0
:after_help
cd /d "%~dp0.."
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH.
exit /b 1
)
REM --preset must come first if given at all, same as scripts\build.bat.
REM cmd.exe splits "--preset=<name>" into "--preset" and "<name>" on its own
REM (see build.bat's note on "="), so both spellings arrive here identically
REM and this one branch handles both.
set "PRESET="
if /i not "%~1"=="--preset" goto preset_done
if "%~2"=="" (
echo error: --preset requires a value, e.g. --preset windows-x64-clang-cl 1>&2
exit /b 1
)
set "PRESET=%~2"
shift
shift
:preset_done
REM Suffix for the error hints below that say to run build.bat first.
set "_pmn_hint="
if defined PRESET set "_pmn_hint= --preset %PRESET%"
REM BUILD_DIR from the environment beats everything; otherwise --preset, else
REM whichever build was made last (see cmake\BuildDir.cmake). Explains itself
REM and stops if there's nothing to pick.
if not "%BUILD_DIR%"=="" goto build_dir_known
call :resolve_build_dir "%PRESET%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
if "%DOCS_DIR%"=="" set "DOCS_DIR=out\docs"
if not exist "%BUILD_DIR%" (
echo error: "%BUILD_DIR%" not found. Run scripts\build.bat%_pmn_hint% first.
exit /b 1
)
echo ==^> Using build directory %BUILD_DIR%
if "%~1"=="" (
cmake --build "%BUILD_DIR%" --target docs
if exist "%DOCS_DIR%\index.html" (
echo ==^> Open: %DOCS_DIR%\index.html
)
) else (
cmake --build "%BUILD_DIR%" --target docs_%1
echo ==^> Open: %DOCS_DIR%\%1\html\index.html
)
exit /b %ERRORLEVEL%
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as build.bat's, and the same `cmake -P`
REM one-line-answer protocol: "-- BUILD_DIR=<path>" or "-- ERROR=<text>"
REM (see that file). On failure it prints the reason to stderr, unless a
REM third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Build Doxygen documentation for every module (see cmake/Documentation.cmake)
# and print the path to the landing page. A no-op with a clear message if
# Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
#
# Output goes to out/docs/ (DOCS_OUTPUT_DIR in Configuration.cmake) --
# note this is a sibling of out/build, not nested inside it, and shared by
# every preset: documentation doesn't depend on which compiler built the
# code. The docs targets just need SOME configured build directory to run
# from, so this uses the one built last unless you pick with --preset. If
# you've overridden DOCS_OUTPUT_DIR at configure time, also set DOCS_DIR here
# to match so the printed path is correct (docs still land in the right
# place either way -- this only affects the message below).
#
# Usage (from the project root):
# scripts/docs.sh # every module
# scripts/docs.sh core_lib # just one module's docs (docs_<target>)
# scripts/docs.sh --preset linux-x64-gcc # ... from that preset's build directory
# scripts/docs.sh --help
#
# --preset must come first if given at all; --preset=<name> also works. Set
# BUILD_DIR to use an exact build directory instead.
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'HELP'
Usage: scripts/docs.sh [--preset <name>] [module-target-name]
Builds Doxygen documentation for every module (see cmake/Documentation.cmake)
and prints the path to the landing page. A no-op with a clear message if
Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
--preset <name> Run from that preset's build directory
(out/build/<name>). Default: the build
scripts/build.sh made last. Must come first if
given at all; --preset=<name> also works.
(no argument) Build docs for every module.
module-target-name Build docs for just that one module (its own
docs_<name> target), e.g.:
scripts/docs.sh core_lib
Output goes to out/docs/ (DOCS_OUTPUT_DIR in Configuration.cmake), shared
by every preset -- see the comment at the top of this script if you've
overridden that.
HELP
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
source "$SCRIPT_DIR/_common.sh"
# --preset must come first if given at all, same as scripts/build.sh.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset linux-x64-gcc" >&2
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=linux-x64-gcc" >&2
exit 1
fi
;;
esac
if [ -z "${BUILD_DIR:-}" ]; then
# Explains itself and stops if there's nothing to pick.
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET")"
fi
DOCS_DIR="${DOCS_DIR:-out/docs}"
if [ ! -d "$BUILD_DIR" ]; then
echo "error: '${BUILD_DIR}' not found. Run scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
echo "==> Using build directory ${BUILD_DIR}"
if [ $# -ge 1 ]; then
TARGET="docs_$1"
cmake --build "${BUILD_DIR}" --target "${TARGET}"
echo "==> Open: ${DOCS_DIR}/$1/html/index.html"
else
cmake --build "${BUILD_DIR}" --target docs
if [ -f "${DOCS_DIR}/index.html" ]; then
echo "==> Open: ${DOCS_DIR}/index.html"
fi
fi
+151
View File
@@ -0,0 +1,151 @@
@echo off
REM Install this project's own build artifacts to
REM out\install\<platform>-<arch>-<compiler>\ (INSTALL_OUTPUT_DIR in
REM Configuration.cmake; see CMakeLists.txt's install() DESTINATION paths) --
REM the same name as the out\build\<preset> directory they came from, since
REM a preset is named after its target.
REM
REM Always passes --component to `cmake --install`. Without it, any
REM vendored/fetched dependency's own install() rules (see
REM cmake\Dependencies.cmake) would also run, typically dumping files under
REM CMAKE_INSTALL_PREFIX -- COMPONENT is what keeps `cmake --install`
REM scoped to just this project's own targets.
REM
REM Usage (from the project root):
REM scripts\install.bat [--preset <name>] [BUILD_TYPE]
REM scripts\install.bat --help
REM
REM --preset <name> Install the build made with that preset
REM (out\build\<name>). Without it: the build
REM scripts\build.bat made last, or the only one that
REM exists. Must come first if given at all;
REM --preset=<name> also works.
REM BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
REM (default: Configuration.cmake's DEFAULT_BUILD_TYPE) --
REM only relevant for a Ninja Multi-Config build.
REM
REM Set BUILD_DIR to install from an exact build directory instead.
setlocal enabledelayedexpansion
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\install.bat [--preset ^<name^>] [BUILD_TYPE]
echo.
echo Installs this project's own build artifacts to
echo out\install\^<platform^>-^<arch^>-^<compiler^>\ ^(INSTALL_OUTPUT_DIR in
echo Configuration.cmake^), scoped to just this project via `cmake --install
echo --component` -- see the comment at the top of this script.
echo.
echo --preset ^<name^> Install the build made with that preset
echo ^(out\build\^<name^>^). Default: the build
echo scripts\build.bat made last. Must come first if given
echo at all; --preset=^<name^> also works.
echo BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel
echo ^(default: Configuration.cmake's DEFAULT_BUILD_TYPE^) --
echo only relevant for a Ninja Multi-Config build.
exit /b 0
:after_help
cd /d "%~dp0.."
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH.
exit /b 1
)
REM --preset must come first if given at all, same as scripts\build.bat.
REM cmd.exe splits "--preset=<name>" into "--preset" and "<name>" on its own
REM (see build.bat's note on "="), so both spellings arrive here identically
REM and this one branch handles both.
set "PRESET="
if /i not "%~1"=="--preset" goto preset_done
if "%~2"=="" (
echo error: --preset requires a value, e.g. --preset windows-x64-clang-cl 1>&2
exit /b 1
)
set "PRESET=%~2"
shift
shift
:preset_done
REM Suffix for the error hints below that say to run build.bat first.
set "_pmn_hint="
if defined PRESET set "_pmn_hint= --preset %PRESET%"
REM BUILD_DIR from the environment beats everything; otherwise --preset, else
REM whichever build was made last (see cmake\BuildDir.cmake). Explains itself
REM and stops if there's nothing to pick.
if not "%BUILD_DIR%"=="" goto build_dir_known
call :resolve_build_dir "%PRESET%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
REM A plain %~1 is safe here (unlike build.bat's arguments): a build type
REM like "Debug" never contains "=", so cmd.exe's positional-parameter
REM splitting on it never comes into play.
set "BUILD_TYPE=%~1"
if "%BUILD_TYPE%"=="" call :read_config DEFAULT_BUILD_TYPE BUILD_TYPE
call :read_config PROJECT_CODE_NAME COMPONENT
if not exist "%BUILD_DIR%" (
echo error: "%BUILD_DIR%" not found. Run scripts\build.bat%_pmn_hint% first.
exit /b 1
)
echo ==^> Installing (component: %COMPONENT%) from %BUILD_DIR%
cmake --install "%BUILD_DIR%" --config %BUILD_TYPE% --component %COMPONENT%
exit /b %ERRORLEVEL%
REM ---------------------------------------------------------------------
REM :read_config <ConfigurationVarName> <LocalVarName>
REM
REM Reads one value out of Configuration.cmake -- this project's single
REM source of truth -- via cmake\PrintConfig.cmake, instead of hardcoding
REM a second copy of a default that can silently drift out of sync, and
REM stores it into %2 in the CALLER's scope. See cmake\PrintConfig.cmake
REM for exactly what's being stripped off of its output below.
REM ---------------------------------------------------------------------
:read_config
setlocal
set "_pmn_line="
for /f "usebackq delims=" %%V in (`cmake "-DPRINT_VARS=%~1" -P cmake\PrintConfig.cmake`) do set "_pmn_line=%%V"
for /f "tokens=1,* delims==" %%A in ("%_pmn_line%") do set "_pmn_value=%%B"
endlocal & set "%~2=%_pmn_value%"
goto :eof
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as build.bat's, and the same `cmake -P`
REM one-line-answer protocol: "-- BUILD_DIR=<path>" or "-- ERROR=<text>"
REM (see that file). On failure it prints the reason to stderr, unless a
REM third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Install this project's own build artifacts to
# out/install/<platform>-<arch>-<compiler>/ (INSTALL_OUTPUT_DIR in
# Configuration.cmake; see CMakeLists.txt's install() DESTINATION paths) --
# the same name as the out/build/<preset> directory they came from, since
# a preset is named after its target.
#
# Always passes --component to `cmake --install`. Without it, any
# vendored/fetched dependency's own install() rules (see
# cmake/Dependencies.cmake) would also run, typically dumping files under
# CMAKE_INSTALL_PREFIX (e.g. /usr/local) -- COMPONENT is what keeps
# `cmake --install` scoped to just this project's own targets.
#
# Usage (from the project root):
# scripts/install.sh [--preset <name>] [BUILD_TYPE]
# scripts/install.sh --help
#
# --preset <name> Install the build made with that preset
# (out/build/<name>). Without it: the build
# scripts/build.sh made last, or the only one that
# exists. Must come first if given at all;
# --preset=<name> also works.
# BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
# (default: Configuration.cmake's DEFAULT_BUILD_TYPE) --
# only relevant for a Ninja Multi-Config build.
#
# Set BUILD_DIR to install from an exact build directory instead.
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'HELP'
Usage: scripts/install.sh [--preset <name>] [BUILD_TYPE]
Installs this project's own build artifacts to
out/install/<platform>-<arch>-<compiler>/ (INSTALL_OUTPUT_DIR in
Configuration.cmake), scoped to just this project via `cmake --install
--component` -- see the comment at the top of this script.
--preset <name> Install the build made with that preset (out/build/<name>).
Default: the build scripts/build.sh made last. Must come
first if given at all; --preset=<name> also works.
BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
(default: Configuration.cmake's DEFAULT_BUILD_TYPE) --
only relevant for a Ninja Multi-Config build.
HELP
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
command -v cmake >/dev/null 2>&1 || { echo "error: cmake not found on PATH." >&2; exit 1; }
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir. Reading
# values out of Configuration.cmake this way -- its single source of truth
# -- is the bug fix that replaced COMPONENT being a literal "demo" here,
# with a comment warning it had to be kept in sync with PROJECT_CODE_NAME by
# hand.
source "$SCRIPT_DIR/_common.sh"
# --preset must come first if given at all, same as scripts/build.sh.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset linux-x64-gcc" >&2
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=linux-x64-gcc" >&2
exit 1
fi
;;
esac
if [ -z "${BUILD_DIR:-}" ]; then
# Explains itself and stops if there's nothing to pick.
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET")"
fi
BUILD_TYPE="${1:-$(_pmn_read_config DEFAULT_BUILD_TYPE)}"
COMPONENT="$(_pmn_read_config PROJECT_CODE_NAME)"
if [ ! -d "$BUILD_DIR" ]; then
echo "error: '${BUILD_DIR}' not found. Run scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
echo "==> Installing (component: ${COMPONENT}) from ${BUILD_DIR}"
cmake --install "${BUILD_DIR}" --config "${BUILD_TYPE}" --component "${COMPONENT}"
+104
View File
@@ -0,0 +1,104 @@
@echo off
REM Applies clang-format and clang-tidy's --fix across this project's own
REM C/C++ code. The pre-commit hook only CHECKS; this is the "just fix it"
REM counterpart -- review the result with `git diff` afterward, same as any
REM auto-formatter.
REM
REM Defaults to source\ and c-api\, this template's own-code roots -- NEVER
REM walks out\, .cache\, vendor\, or .git\ by default, so it can't
REM accidentally spend minutes reformatting/analyzing a fetched dependency's
REM entire source tree the way scanning the whole repo from "." would.
REM
REM Usage, from the project root: scripts\refactor.bat [optional path]
REM scripts\refactor.bat every file under source\ and c-api\
REM scripts\refactor.bat source\Core just one file or directory -- taken
REM as given, with no exclusion filter,
REM so pick a path that doesn't overlap
REM with out\/.cache\/vendor\/.git\
REM scripts\refactor.bat --help
setlocal enabledelayedexpansion
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\refactor.bat [scope]
echo.
echo Applies clang-format and clang-tidy's --fix across this project's own
echo C/C++ code -- review the result with `git diff` afterward. See the
echo comment at the top of this script for the default scope and why it
echo never walks out\, .cache\, vendor\, or .git\.
echo.
echo ^(no argument^) Every file under source\ and c-api\ ^(whichever exist^).
echo scope Just one file or directory, e.g.:
echo scripts\refactor.bat source\Core
exit /b 0
:after_help
cd /d "%~dp0.."
where clang-format >nul 2>nul
set "HAVE_FORMAT=0"
if not errorlevel 1 set "HAVE_FORMAT=1"
where clang-tidy >nul 2>nul
set "HAVE_TIDY=0"
if not errorlevel 1 set "HAVE_TIDY=1"
if "%HAVE_FORMAT%"=="1" goto have_a_tool
if "%HAVE_TIDY%"=="1" goto have_a_tool
echo error: neither clang-format nor clang-tidy found on PATH. Run scripts\setup.bat for install guidance.
exit /b 1
:have_a_tool
set "FILELIST=%TEMP%\refactor_files_%RANDOM%.txt"
if exist "%FILELIST%" del "%FILELIST%"
REM Collect the file list. Deliberately NOT using an if/else block here --
REM a for /f or for /r loop nested inside a parenthesized if(...) else(...)
REM block confuses cmd.exe's paren-matching for the enclosing block (this
REM was tested and confirmed, not a guess); goto-based branching sidesteps
REM the problem entirely by never nesting a FOR loop inside another block.
if not "%~1"=="" goto collect_custom
set "SCOPE_DESC=source and c-api"
if exist "source" for /r "source" %%F in (*.c *.h *.cpp *.hpp *.cc *.cxx *.tpp) do echo %%F>>"%FILELIST%"
if exist "c-api" for /r "c-api" %%F in (*.c *.h *.cpp *.hpp *.cc *.cxx *.tpp) do echo %%F>>"%FILELIST%"
goto collected
:collect_custom
set "SCOPE_DESC=%~1"
if not exist "%~1" goto collected
for /r "%~1" %%F in (*.c *.h *.cpp *.hpp *.cc *.cxx *.tpp) do echo %%F>>"%FILELIST%"
:collected
if exist "%FILELIST%" goto have_files
echo No C/C++ files found under: %SCOPE_DESC%
exit /b 0
:have_files
echo ==^> Files in scope: %SCOPE_DESC%
if "%HAVE_FORMAT%"=="1" goto do_format
echo ==^> Skipping clang-format ^(not found on PATH -- see scripts\setup.bat^)
goto after_format
:do_format
echo ==^> Running clang-format -i
for /f "usebackq delims=" %%F in ("%FILELIST%") do clang-format -i "%%F"
:after_format
if "%HAVE_TIDY%"=="1" goto do_tidy
echo ==^> Skipping clang-tidy ^(not found on PATH -- see scripts\setup.bat^)
goto after_tidy
:do_tidy
if exist compile_commands.json goto run_tidy
echo ==^> Skipping clang-tidy ^(no compile_commands.json -- run scripts\build.bat first^)
goto after_tidy
:run_tidy
echo ==^> Running clang-tidy --fix ^(auto-fixable issues only; build once first if this is stale^)
for /f "usebackq delims=" %%F in ("%FILELIST%") do clang-tidy --quiet --fix "%%F"
:after_tidy
del "%FILELIST%" 2>nul
echo ==^> Done. Review the changes: git diff
exit /b 0
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Applies clang-format and clang-tidy's --fix across this project's own
# C/C++ code. The pre-commit hook only CHECKS; this is the "just fix it"
# counterpart -- review the result with `git diff` afterward, same as any
# auto-formatter.
#
# Defaults to source/ and c-api/ (this template's own-code roots) --
# NEVER walks out/, .cache/, vendor/, or .git/ by default, so it can't
# accidentally spend minutes reformatting/analyzing a fetched dependency's
# entire source tree (e.g. out/build/_deps/*-src/) the way scanning the
# whole repo from "." would. Passing an explicit scope still excludes
# those four directories defensively, in case the scope you give overlaps
# with them.
#
# Usage (from the project root):
# scripts/refactor.sh # every file under source/ and c-api/
# scripts/refactor.sh source/Core # just one file or directory
# scripts/refactor.sh --help
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'EOF'
Usage: scripts/refactor.sh [scope]
Applies clang-format and clang-tidy's --fix across this project's own
C/C++ code -- review the result with `git diff` afterward. See the
comment at the top of this script for the default scope and why it never
walks out/, .cache/, vendor/, or .git/.
(no argument) Every file under source/ and c-api/ (whichever exist).
scope Just one file or directory, e.g.:
scripts/refactor.sh source/Core
EOF
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_extend_tool_path: finds Homebrew's keg-only llvm (clang-tidy) on macOS.
source "$SCRIPT_DIR/_common.sh"
_pmn_extend_tool_path
if [ $# -ge 1 ]; then
scopes=("$1")
else
scopes=()
[ -d source ] && scopes+=(source)
[ -d c-api ] && scopes+=(c-api)
fi
if [ "${#scopes[@]}" -eq 0 ]; then
echo "No default scope found (expected a source/ and/or c-api/ directory here)." >&2
exit 1
fi
if ! command -v clang-format >/dev/null 2>&1 && ! command -v clang-tidy >/dev/null 2>&1; then
echo "error: neither clang-format nor clang-tidy found on PATH. Run scripts/setup.sh for install guidance." >&2
exit 1
fi
# A read loop rather than mapfile/readarray: those are bash 4+, and macOS
# ships bash 3.2.
files=()
while IFS= read -r _file; do
files+=("$_file")
done < <(
find "${scopes[@]}" \
\( -path './vendor' -o -path './out' -o -path './.cache' -o -path './.git' \) -prune -o \
-type f \( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.tpp' \) -print \
| sed 's|^\./||' | sort
)
if [ "${#files[@]}" -eq 0 ]; then
echo "No C/C++ files found under: ${scopes[*]}"
exit 0
fi
echo "==> ${#files[@]} file(s) in scope: ${scopes[*]}"
if command -v clang-format >/dev/null 2>&1; then
echo "==> Running clang-format -i"
clang-format -i "${files[@]}"
else
echo "==> Skipping clang-format (not found on PATH -- see scripts/setup.sh)"
fi
if command -v clang-tidy >/dev/null 2>&1; then
if [ ! -f compile_commands.json ]; then
echo "==> Skipping clang-tidy (no compile_commands.json -- run scripts/build.sh first)"
else
echo "==> Running clang-tidy --fix (auto-fixable issues only; build once first if this is stale)"
for f in "${files[@]}"; do
clang-tidy --quiet --fix "$f" || true
done
fi
else
echo "==> Skipping clang-tidy (not found on PATH -- see scripts/setup.sh)"
fi
echo "==> Done. Review the changes: git diff"
+185
View File
@@ -0,0 +1,185 @@
@echo off
REM Run an executable module by its CMake target name (as registered in
REM that module's own CMakeLists.txt), resolved via the manifest that
REM _write_module_manifest() generates at configure time -- so this script
REM never needs updating when modules are added, removed, or renamed.
REM
REM Usage (from the project root):
REM scripts\run.bat [--preset <name>] <target-name> [-- program-args...]
REM
REM --preset <name> Run the build made with that preset (out\build\<name>).
REM Without it: the build scripts\build.bat made last, or
REM the only one that exists. Must come first if given at
REM all; --preset=<name> also works.
REM
REM Examples:
REM scripts\run.bat hello_exe
REM scripts\run.bat --preset windows-x64-clang-cl hello_exe
REM scripts\run.bat farewell_exe -- Ale
REM
REM Set BUILD_DIR to run out of an exact build directory instead.
setlocal enabledelayedexpansion
cd /d "%~dp0.."
REM --preset must come first if given at all, same as scripts\build.bat.
REM cmd.exe splits "--preset=<name>" into "--preset" and "<name>" on its own
REM (see build.bat's note on "="), so both spellings arrive here identically
REM and this one branch handles both.
set "PRESET="
if /i not "%~1"=="--preset" goto preset_done
if "%~2"=="" (
echo error: --preset requires a value, e.g. --preset windows-x64-clang-cl 1>&2
exit /b 1
)
set "PRESET=%~2"
shift
shift
:preset_done
REM Suffix for the error hints below that say to run build.bat first.
set "_pmn_hint="
if defined PRESET set "_pmn_hint= --preset %PRESET%"
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
if "%~1"=="" goto usage
set "TARGET_NAME=%~1"
shift
REM Collect any remaining args to pass through to the program (skipping a
REM leading "--" separator if present).
set "PASSTHROUGH="
set "skip_next_dashes=1"
:collect
if "%~1"=="" goto collected
if "!skip_next_dashes!"=="1" if "%~1"=="--" (
set "skip_next_dashes=0"
shift
goto collect
)
set "skip_next_dashes=0"
set "PASSTHROUGH=!PASSTHROUGH! "%~1""
shift
goto collect
:collected
REM BUILD_DIR from the environment beats everything; otherwise --preset, else
REM whichever build was made last (see cmake\BuildDir.cmake). Explains itself
REM and stops if there's nothing to pick.
if not "%BUILD_DIR%"=="" goto build_dir_known
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH -- it is needed to work out which build directory to use. 1>&2
echo Put it on PATH, or set BUILD_DIR to the directory explicitly, e.g. out\build\^<preset^>. 1>&2
exit /b 1
)
call :resolve_build_dir "%PRESET%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
set "MANIFEST="
for %%F in ("%BUILD_DIR%\module_manifest-*.txt") do (
if not defined MANIFEST set "MANIFEST=%%F"
)
if not defined MANIFEST (
echo error: no build manifest found in "%BUILD_DIR%". Run scripts\build.bat%_pmn_hint% first. 1>&2
exit /b 1
)
set "BIN_PATH="
for /f "usebackq tokens=1,* delims==" %%K in ("%MANIFEST%") do (
if "%%K"=="%TARGET_NAME%" set "BIN_PATH=%%L"
)
if not defined BIN_PATH (
echo error: no executable target named "%TARGET_NAME%" in "%BUILD_DIR%". 1>&2
echo Available targets: 1>&2
for /f "usebackq tokens=1 delims==" %%T in ("%MANIFEST%") do echo %%T 1>&2
exit /b 1
)
if not exist "%BIN_PATH%" (
echo error: "%BIN_PATH%" was not found. Rebuild with scripts\build.bat%_pmn_hint%. 1>&2
exit /b 1
)
REM Which build this came from -- to stderr, so a program's own stdout stays
REM clean to pipe.
echo ==^> Running %TARGET_NAME% ^(%BUILD_DIR%^) 1>&2
"%BIN_PATH%" !PASSTHROUGH!
exit /b %ERRORLEVEL%
:usage
REM Best-effort here: usage should still print with nothing built yet.
if "%BUILD_DIR%"=="" call :resolve_build_dir "%PRESET%" BUILD_DIR quiet
echo Usage: %~nx0 [--preset ^<name^>] ^<cmake-target-name^> [-- program-args...] 1>&2
echo. 1>&2
echo Available executable targets: 1>&2
set "found=0"
for %%F in ("%BUILD_DIR%\module_manifest-*.txt") do (
set "found=1"
for /f "usebackq tokens=1 delims==" %%T in ("%%F") do echo %%T 1>&2
)
if "!found!"=="0" echo (none found -- did you run scripts\build.bat?) 1>&2
exit /b 1
:show_help
REM Best-effort here: help should still print with nothing built yet.
if "%BUILD_DIR%"=="" call :resolve_build_dir "%PRESET%" BUILD_DIR quiet
echo Usage: %~nx0 [--preset ^<name^>] ^<cmake-target-name^> [-- program-args...]
echo.
echo Runs an executable module by its CMake target name, resolved via the
echo manifest scripts\build.bat generates at configure time -- see the
echo comment at the top of this script.
echo.
echo --preset ^<name^> Run the build made with that preset. Default: the
echo build scripts\build.bat made last.
echo.
echo Available executable targets:
set "found=0"
for %%F in ("%BUILD_DIR%\module_manifest-*.txt") do (
set "found=1"
for /f "usebackq tokens=1 delims==" %%T in ("%%F") do echo %%T
)
if "!found!"=="0" echo (none found -- did you run scripts\build.bat?)
echo.
echo Examples:
echo scripts\run.bat hello_exe
echo scripts\run.bat --preset windows-x64-clang-cl hello_exe
echo scripts\run.bat farewell_exe -- Ale
exit /b 0
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as build.bat's, and the same `cmake -P`
REM one-line-answer protocol: "-- BUILD_DIR=<path>" or "-- ERROR=<text>"
REM (see that file). On failure it prints the reason to stderr, unless a
REM third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
Executable
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env bash
# Run an executable module by its CMake target name (as registered in
# that module's own CMakeLists.txt), resolved via the manifest that
# _write_module_manifest() generates at configure time -- so this script
# never needs updating when modules are added, removed, or renamed.
#
# Usage (from the project root):
# scripts/run.sh [--preset <name>] <target-name> [-- program-args...]
#
# --preset <name> Run the build made with that preset (out/build/<name>).
# Without it: the build scripts/build.sh made last, or
# the only one that exists. Must come first if given at
# all; --preset=<name> also works.
#
# Examples:
# scripts/run.sh hello_exe
# scripts/run.sh --preset linux-x64-clang hello_exe
# scripts/run.sh farewell_exe -- Ale
#
# Set BUILD_DIR to run out of an exact build directory instead.
#
# An executable built for another architecture or OS -- linux-arm64-* on an
# x64 machine, windows-*-mingw* from Linux -- is run under the emulator its
# toolchain registered (qemu-user, wine) if one was installed when the build
# was configured; otherwise it can only run on the target itself.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
source "$SCRIPT_DIR/_common.sh"
# --preset must come first if given at all, same as scripts/build.sh.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset linux-x64-gcc" >&2
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=linux-x64-gcc" >&2
exit 1
fi
;;
esac
# BUILD_DIR from the environment beats everything; otherwise --preset, else
# whichever build was made last (see cmake/BuildDir.cmake). Help and the
# usage message look it up best-effort -- they should still print on a
# fresh checkout with nothing built yet -- while actually running
# something needs a real answer, and stops with the reason if there isn't
# one.
_pmn_try_build_dir() {
if [ -z "${BUILD_DIR:-}" ]; then
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET" 2>/dev/null || true)"
fi
}
list_targets() {
local manifest
[ -n "${BUILD_DIR:-}" ] || return 0
for manifest in "${BUILD_DIR}"/module_manifest-*.txt; do
[ -e "$manifest" ] || continue
cut -d= -f1 "$manifest"
done | sort -u
}
usage() {
_pmn_try_build_dir
echo "Usage: $0 [--preset <name>] <cmake-target-name> [-- program-args...]" >&2
echo >&2
echo "Available executable targets:" >&2
local targets
targets="$(list_targets)"
if [ -n "$targets" ]; then
echo "$targets" | sed 's/^/ /' >&2
else
echo " (none found -- did you run scripts/build.sh?)" >&2
fi
exit 1
}
show_help() {
_pmn_try_build_dir
echo "Usage: $0 [--preset <name>] <cmake-target-name> [-- program-args...]"
echo
echo "Runs an executable module by its CMake target name, resolved via the"
echo "manifest scripts/build.sh generates at configure time -- see the"
echo "comment at the top of this script."
echo
echo " --preset <name> Run the build made with that preset. Default: the"
echo " build scripts/build.sh made last."
echo
echo "Available executable targets:"
local targets
targets="$(list_targets)"
if [ -n "$targets" ]; then
echo "$targets" | sed 's/^/ /'
else
echo " (none found -- did you run scripts/build.sh?)"
fi
echo
echo "Examples:"
echo " scripts/run.sh hello_exe"
echo " scripts/run.sh --preset linux-x64-clang hello_exe"
echo " scripts/run.sh farewell_exe -- Ale"
exit 0
}
case "${1:-}" in
-h|--help) show_help ;;
esac
[ $# -ge 1 ] || usage
TARGET_NAME="$1"
shift
if [ "${1:-}" = "--" ]; then
shift
fi
if [ -z "${BUILD_DIR:-}" ]; then
# Explains itself and stops if there's nothing to pick.
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET")"
fi
MANIFEST=""
for m in "${BUILD_DIR}"/module_manifest-*.txt; do
[ -e "$m" ] || continue
MANIFEST="$m"
break
done
if [ -z "$MANIFEST" ]; then
echo "error: no build manifest found in '${BUILD_DIR}'. Run scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
BIN_PATH="$(grep "^${TARGET_NAME}=" "$MANIFEST" | head -n1 | cut -d= -f2- || true)"
if [ -z "$BIN_PATH" ]; then
echo "error: no executable target named '${TARGET_NAME}' in '${BUILD_DIR}'." >&2
echo "Available targets:" >&2
list_targets | sed 's/^/ /' >&2
exit 1
fi
if [ ! -x "$BIN_PATH" ]; then
echo "error: '${BIN_PATH}' was not found or is not executable. Rebuild with scripts/build.sh${PRESET:+ --preset $PRESET}." >&2
exit 1
fi
# A cross build's toolchain file (cmake/toolchains/) registers an emulator --
# qemu-user for another CPU, wine for Windows -- as
# CMAKE_CROSSCOMPILING_EMULATOR when one is installed, which is also what
# lets ctest run the same binaries. Use it the same way. It's a
# ";"-separated list (the emulator, then its arguments).
RUN_PREFIX=()
if [ -f "${BUILD_DIR}/CMakeCache.txt" ]; then
_pmn_emulator="$(sed -n 's/^CMAKE_CROSSCOMPILING_EMULATOR:[A-Z]*=//p' "${BUILD_DIR}/CMakeCache.txt")"
if [ -n "$_pmn_emulator" ]; then
IFS=';' read -r -a RUN_PREFIX <<< "$_pmn_emulator"
fi
fi
# Which build this came from -- to stderr, so a program's own stdout stays
# clean to pipe.
echo "==> Running ${TARGET_NAME} (${BUILD_DIR})${RUN_PREFIX[0]:+ under ${RUN_PREFIX[0]##*/}}" >&2
# Not `exec`, so a failure to start at all can be explained below.
_pmn_status=0
${RUN_PREFIX[@]+"${RUN_PREFIX[@]}"} "$BIN_PATH" "$@" || _pmn_status=$?
# 126 is the shell's "found it, couldn't execute it" -- which is what running
# a binary built for another architecture or OS looks like.
if [ "$_pmn_status" -eq 126 ]; then
echo "error: '${BIN_PATH}' couldn't be started on this machine. If it is a cross build" >&2
echo " (built for another CPU or OS), run it on the target, or install an" >&2
echo " emulator -- qemu-user for another CPU, wine for Windows -- and reconfigure" >&2
echo " (scripts/build.sh --preset <name>) so the toolchain picks it up." >&2
fi
exit "$_pmn_status"
+219
View File
@@ -0,0 +1,219 @@
@echo off
REM One-time (or re-run any time) environment setup for this template.
REM Windows; on Linux and macOS it is scripts/setup.sh.
REM
REM 1. Checks for the tools this project builds with -- Git, CMake 3.25+ and a
REM C++ compiler (Visual Studio Build Tools, or MinGW-w64 through MSYS2:
REM required), plus LLVM and the other MSYS2 toolchains (optional) -- and
REM offers to install whatever is missing, ONE TOOL AT A TIME, showing the
REM exact winget command before running it. Nothing is installed without
REM your yes. The work is done by scripts\_tools.ps1 (PowerShell: prompts,
REM PATH handling and winget are far more robust there than in batch).
REM 2. Fetches the pinned ninja (cmake\FetchNinja.cmake) -- used by
REM scripts\build.bat if found (see there); a system ninja still
REM works fine too, this just avoids needing one installed at all.
REM 3. Fetches the pinned Doxygen (cmake\FetchDoxygen.cmake) -- NOT
REM required to build the project itself (see cmake\Documentation.cmake),
REM only to run scripts\docs.bat afterward.
REM 4. Activates the pre-commit hook (.githooks\pre-commit) for this
REM clone, if it's a git repository.
REM
REM A failure in step 2 or 3 (no pinned binary for this machine, no network)
REM is reported and setup carries on -- it used to stop there, before the hook
REM was activated.
REM
REM Usage (from the project root):
REM scripts\setup.bat [--yes] [--all] [--check] [--skip-tools]
REM scripts\setup.bat --help
REM
REM No delayed expansion in this file, on purpose: PATH is edited below, and
REM with delayed expansion on, a "!" in any directory name would be eaten.
setlocal
REM %~dp0 is captured first: a plain SHIFT (used to walk the arguments below)
REM shifts %0 as well.
set "_PMN_HERE=%~dp0"
set "_PMN_TOOLS_ARGS="
set "_PMN_SKIP_TOOLS=0"
set "_PMN_CHECK=0"
set "_PMN_TOOLS_RC=0"
:parse_args
if "%~1"=="" goto args_done
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
if /i "%~1"=="-y" goto opt_yes
if /i "%~1"=="--yes" goto opt_yes
if /i "%~1"=="--all" goto opt_all
if /i "%~1"=="--check" goto opt_check
if /i "%~1"=="--skip-tools" goto opt_skip_tools
echo error: unknown option "%~1" 1>&2
echo Run scripts\setup.bat --help for usage. 1>&2
exit /b 1
:opt_yes
set "_PMN_TOOLS_ARGS=%_PMN_TOOLS_ARGS% -Yes"
shift
goto parse_args
:opt_all
set "_PMN_TOOLS_ARGS=%_PMN_TOOLS_ARGS% -All"
shift
goto parse_args
:opt_check
set "_PMN_TOOLS_ARGS=%_PMN_TOOLS_ARGS% -Check"
set "_PMN_CHECK=1"
shift
goto parse_args
:opt_skip_tools
set "_PMN_SKIP_TOOLS=1"
shift
goto parse_args
:show_help
echo Usage: scripts\setup.bat [--yes] [--all] [--check] [--skip-tools]
echo.
echo One-time ^(or re-run any time^) environment setup for this template:
echo 1. Checks the tools the project builds with and offers to install what's
echo missing, one tool at a time ^(Git, CMake and a C++ compiler are
echo required; LLVM and the other MSYS2 toolchains are optional^). Each
echo install shows its exact winget command and waits for your yes.
echo 2. Fetches the pinned ninja ^(cmake\FetchNinja.cmake^).
echo 3. Fetches the pinned Doxygen ^(cmake\FetchDoxygen.cmake^).
echo 4. Activates the pre-commit hook ^(.githooks\pre-commit^) for this clone,
echo if it's a git repository.
echo.
echo Options:
echo -y, --yes Install the REQUIRED tools without asking. For scripts and
echo CI; without a terminal and without this, nothing is asked
echo and nothing is installed.
echo --all Also install the optional tools without asking ^(implies
echo --yes^).
echo --check Only report what is and isn't installed; change nothing,
echo fetch nothing. Exits 1 if a required tool is missing.
echo --skip-tools Skip step 1 and go straight to the pinned downloads.
echo.
echo At each question: y install it n not this one a install everything
echo left s skip the rest of the optional tools q stop asking.
echo.
echo See the comment at the top of this script for more on each step.
exit /b 0
:args_done
cd /d "%_PMN_HERE%.."
REM ---- 1. tools --------------------------------------------------------------
if "%_PMN_SKIP_TOOLS%"=="1" goto tools_done
where powershell >nul 2>nul
if errorlevel 1 goto no_powershell
set "_PMN_PATHFILE=%TEMP%\pmn-path-%RANDOM%%RANDOM%.txt"
powershell -NoProfile -ExecutionPolicy Bypass -File "%_PMN_HERE%_tools.ps1" %_PMN_TOOLS_ARGS% -PathOut "%_PMN_PATHFILE%"
set "_PMN_TOOLS_RC=%ERRORLEVEL%"
REM The installer adds tool folders (CMake, Git, ...) to ITS copy of PATH so
REM it can use them; a child process can't change ours, so it hands the list
REM back in a file and they're appended here, for the steps below.
if exist "%_PMN_PATHFILE%" call :take_path_additions
echo.
goto tools_done
:no_powershell
echo error: powershell.exe not found on PATH -- it runs the tool installer. 1>&2
echo Install the tools yourself and re-run with --skip-tools. 1>&2
exit /b 1
:tools_done
if not "%_PMN_CHECK%"=="1" goto after_check
if "%_PMN_TOOLS_RC%"=="0" goto check_ok
echo ==^> Required tools are missing. Run scripts\setup.bat to install them.
exit /b %_PMN_TOOLS_RC%
:check_ok
echo ==^> All required tools are present.
exit /b 0
:after_check
REM Everything below runs CMake scripts.
where cmake >nul 2>nul
if errorlevel 1 goto no_cmake
goto have_cmake
:no_cmake
echo error: cmake not found on PATH -- nothing below can run without it. 1>&2
if "%_PMN_SKIP_TOOLS%"=="1" goto no_cmake_skip
echo Install CMake ^(3.25 or newer^), open a new terminal, and re-run this script. 1>&2
exit /b 1
:no_cmake_skip
echo Re-run without --skip-tools to be offered an install. 1>&2
exit /b 1
:have_cmake
REM ---- 2. pinned ninja -------------------------------------------------------
echo ==^> Fetching pinned ninja (cmake\FetchNinja.cmake)
cmake -P cmake\FetchNinja.cmake
if errorlevel 1 goto ninja_failed
goto ninja_done
:ninja_failed
echo.
echo No pinned ninja here ^(no upstream binary for this machine, or the
echo download failed -- see above^).
where ninja >nul 2>nul
if errorlevel 1 goto ninja_missing
echo The ninja on PATH will be used instead.
goto ninja_done
:ninja_missing
echo scripts\build.bat needs a ninja: e.g. winget install Ninja-build.Ninja
:ninja_done
REM ---- 3. pinned Doxygen -----------------------------------------------------
echo.
echo ==^> Fetching pinned Doxygen (cmake\FetchDoxygen.cmake)
cmake -P cmake\FetchDoxygen.cmake
if errorlevel 1 goto doxygen_failed
goto doxygen_done
:doxygen_failed
echo.
echo Documentation ^(scripts\docs.bat^) won't be available: there's no pinned
echo Doxygen for this machine, or the download failed -- see above.
echo The project itself builds fine without it.
:doxygen_done
REM ---- 4. pre-commit hook ----------------------------------------------------
echo.
git rev-parse --is-inside-work-tree >nul 2>nul
if errorlevel 1 goto hook_skip
echo ==^> Activating pre-commit hook
git config core.hooksPath .githooks
echo core.hooksPath = .githooks
goto hook_done
:hook_skip
echo ==^> Skipping pre-commit hook activation ^(not a git repository yet^)
echo Once you've run "git init", re-run this script or:
echo git config core.hooksPath .githooks
:hook_done
echo.
if "%_PMN_TOOLS_RC%"=="0" goto setup_ok
echo ==^> Setup finished, but a required tool is still missing -- see above.
exit /b 1
:setup_ok
echo ==^> Setup complete. scripts\build.bat and scripts\docs.bat are ready to use.
exit /b 0
REM ---------------------------------------------------------------------
REM :take_path_additions
REM
REM Appends every directory listed in %_PMN_PATHFILE% to PATH, then deletes
REM the file. One "for /f ... do call" line, deliberately not a parenthesized
REM block: a directory like "C:\Program Files (x86)\..." would close it early.
REM ---------------------------------------------------------------------
:take_path_additions
for /f "usebackq delims=" %%D in ("%_PMN_PATHFILE%") do call :add_path "%%D"
del "%_PMN_PATHFILE%" >nul 2>nul
goto :eof
:add_path
set "PATH=%PATH%;%~1"
goto :eof
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# One-time (or re-run any time) environment setup for this template. Linux
# and macOS; on Windows it is scripts\setup.bat.
#
# 1. Checks for the tools this project builds with -- git, CMake 3.25+ and a
# C++ compiler (both of which are required), and Clang, clang-format,
# clang-tidy, cross compilers, MinGW and emulators (all optional) -- and
# offers to install whatever is missing, ONE TOOL AT A TIME, showing
# the exact command before running it. Nothing is installed without
# your yes. See scripts/_tools.sh for what it knows and how.
# 2. Fetches the pinned ninja (cmake/FetchNinja.cmake) -- used by
# scripts/build.sh / build.bat if found (see there); a system ninja
# still works fine too, this just avoids needing one installed at all.
# Where upstream ships no ninja binary for your machine, it offers to
# install one through your package manager instead.
# 3. Fetches the pinned Doxygen (cmake/FetchDoxygen.cmake) -- NOT
# required to build the project itself (see cmake/Documentation.cmake),
# only to run scripts/docs.sh / docs.bat afterward.
# 4. Activates the pre-commit hook (.githooks/pre-commit) for this
# clone, if it's a git repository.
#
# A failure in step 2 or 3 (no pinned binary for this machine, no network)
# is reported and setup carries on -- it used to abort here, before the hook
# was activated.
#
# Usage (from the project root):
# scripts/setup.sh [--yes] [--all] [--check] [--skip-tools]
# scripts/setup.sh --help
set -euo pipefail
_usage() {
cat <<'EOF'
Usage: scripts/setup.sh [--yes] [--all] [--check] [--skip-tools]
One-time (or re-run any time) environment setup for this template:
1. Checks the tools the project builds with and offers to install what's
missing, one tool at a time (git, CMake and a C++ compiler are
required; Clang, clang-format/clang-tidy, cross compilers, MinGW and
emulators are optional). Each install shows its exact command and
waits for your yes.
2. Fetches the pinned ninja (cmake/FetchNinja.cmake).
3. Fetches the pinned Doxygen (cmake/FetchDoxygen.cmake).
4. Activates the pre-commit hook (.githooks/pre-commit) for this clone,
if it's a git repository.
Options:
-y, --yes Install the REQUIRED tools without asking. For scripts and
CI; without a terminal and without this, nothing is asked
and nothing is installed.
--all Also install the optional tools without asking (implies
--yes).
--check Only report what is and isn't installed; change nothing,
fetch nothing. Exits 1 if a required tool is missing.
--skip-tools Skip step 1 and go straight to the pinned downloads.
At each question: y install it n not this one a install everything
left s skip the rest of the optional tools q stop asking.
See the comment at the top of this script for more on each step.
EOF
}
PMN_TOOLS_YES=0
PMN_TOOLS_ALL=0
PMN_TOOLS_CHECK=0
SKIP_TOOLS=0
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) _usage; exit 0 ;;
-y|--yes) PMN_TOOLS_YES=1 ;;
--all) PMN_TOOLS_ALL=1; PMN_TOOLS_YES=1 ;;
--check) PMN_TOOLS_CHECK=1 ;;
--skip-tools) SKIP_TOOLS=1 ;;
*)
echo "error: unknown option '$1'" >&2
echo "Run scripts/setup.sh --help for usage." >&2
exit 1
;;
esac
shift
done
export PMN_TOOLS_YES PMN_TOOLS_ALL PMN_TOOLS_CHECK
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_tools_run, _pmn_tools_offer and their helpers.
source "$SCRIPT_DIR/_tools.sh"
# ---- 1. tools --------------------------------------------------------------
_tools_rc=0
if [ "$SKIP_TOOLS" = 0 ]; then
_pmn_tools_run || _tools_rc=$?
echo
fi
if [ "$PMN_TOOLS_CHECK" = 1 ]; then
if [ "$_tools_rc" -ne 0 ]; then
echo "==> Required tools are missing. Run scripts/setup.sh to install them."
else
echo "==> All required tools are present."
fi
exit "$_tools_rc"
fi
# Everything below runs CMake scripts.
if ! command -v cmake >/dev/null 2>&1; then
echo "error: cmake not found on PATH -- nothing below can run without it." >&2
if [ "$SKIP_TOOLS" = 1 ]; then
echo " Re-run without --skip-tools to be offered an install." >&2
else
echo " Install CMake (3.25 or newer), open a new terminal, and re-run this script." >&2
fi
exit 1
fi
# ---- 2. pinned ninja -------------------------------------------------------
echo "==> Fetching pinned ninja (cmake/FetchNinja.cmake)"
if ! cmake -P cmake/FetchNinja.cmake; then
echo
echo " No pinned ninja here (no upstream binary for this machine, or the"
echo " download failed -- see above)."
if command -v ninja >/dev/null 2>&1; then
echo " The ninja on PATH will be used instead: $(command -v ninja)"
elif [ "$SKIP_TOOLS" = 0 ]; then
# A build needs SOME ninja; this is the machine-specific moment to say so.
_pmn_tools_offer ninja || echo " scripts/build.sh needs a ninja: install one and put it on PATH."
else
echo " scripts/build.sh needs a ninja: install one and put it on PATH."
fi
fi
# ---- 3. pinned Doxygen -----------------------------------------------------
echo
echo "==> Fetching pinned Doxygen (cmake/FetchDoxygen.cmake)"
if ! cmake -P cmake/FetchDoxygen.cmake; then
echo
echo " Documentation (scripts/docs.sh) won't be available: there's no pinned"
echo " Doxygen for this machine, or the download failed -- see above."
echo " The project itself builds fine without it."
fi
# ---- 4. pre-commit hook ----------------------------------------------------
echo
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "==> Activating pre-commit hook"
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
echo " core.hooksPath = .githooks"
else
echo "==> Skipping pre-commit hook activation (not a git repository yet)"
echo " Once you've run 'git init', re-run this script or:"
echo " chmod +x .githooks/pre-commit && git config core.hooksPath .githooks"
fi
echo
if [ "$_tools_rc" -ne 0 ]; then
echo "==> Setup finished, but a required tool is still missing -- see above."
exit 1
fi
echo "==> Setup complete. scripts/build.sh and scripts/docs.sh are ready to use."