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

627 lines
27 KiB
Bash
Executable File

# 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
}