627 lines
27 KiB
Bash
Executable File
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
|
|
}
|