# Interactive installer for the tools this project builds with. Sourced by # breeze/scripts/setup.sh (Linux and macOS -- Windows has breeze/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. MinGW and the emulators (qemu-user, wine) are # offered on all three Linux package managers; the Linux-target cross # compilers (xc-arm64, xc-arm32, xc-x64) are offered wherever a package is # actually known to produce a working one for THIS project (a real libstdc++ # for the target, laid out under /usr/ the way breeze/cmake/toolchains/ # expects) -- apt and pacman ship one of their own for some architectures; # where neither does, dnf's own cross-gcc ships without libstdc++ at all (see # _pmn_arch_extra_notes), so the working toolchain there comes from a # community COPR repository instead, offered with its own explicit consent # separate from the "install this tool?" question (see # _pmn_dnf_enable_copr), the same way pacman's wine asks separately before # enabling the multilib repository it needs. Offering a package that would # then not work is worse than not offering it, so anywhere none of this # applies, the gap is explained in place (_pmn_arch_extra_notes) rather than # silently missing. # # 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_HEAVY=1 on top of ALL: also do the third-party extras that are # otherwise never done without a y typed at their own # question (--all and an earlier "a" don't count) -- # today, on Arch, installing the prebuilt 32-bit ARM # cross toolchain from the AUR (see _pmn_offer_aur_arm32). # setup.sh --everything sets this (and ALL, and YES). # 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, breeze/cmake/Breeze.cmake, and the presets files' 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_HEAVY="${PMN_TOOLS_HEAVY:-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 breeze/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 : prints "ok ", "old " 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 ;; # Compiled and linked for real, not just found on PATH -- see # _pmn_cross_probe for why a mere `command -v` isn't enough. xc-arm64) _pmn_cross_status aarch64-linux-gnu ;; xc-arm32) _pmn_cross_status arm-linux-gnueabihf ;; xc-x64) _pmn_cross_status x86_64-linux-gnu ;; 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 if [ "$PMN_PM" = apt ] && [ "$PMN_ARCH" = x64 ] && ! _pmn_apt_wine64_installed; then echo "partial wine64 is missing (64-bit Windows programs need it)" elif [ "$PMN_PM" = apt ] && [ "$PMN_ARCH" = x64 ] && ! _pmn_apt_wine32_installed; then echo "partial wine32:i386 is missing (32-bit Windows programs need it)" else echo "ok" fi 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|breeze/scripts/refactor.sh and the pre-commit hook|clang-format|clang-tools-extra|clang|clang-format" ;; clang-tidy) echo "clang-tidy|optional|clang-tidy|breeze/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|aarch64-linux-gnu-binutils aarch64-linux-gnu-gcc aarch64-linux-gnu-glibc|aarch64-linux-gnu-gcc|-" ;; xc-arm32) echo "xc-arm32|optional|32-bit ARM cross compiler|the linux-arm32-* presets, built from this machine|g++-arm-linux-gnueabihf|arm-linux-gnueabihf-binutils arm-linux-gnueabihf-gcc arm-linux-gnueabihf-glibc|-|-" ;; 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|mingw64-gcc-c++|mingw-w64-gcc|-" ;; mingw32) echo "mingw32|optional|MinGW-w64 (32-bit Windows)|the windows-x86-mingw32 preset, cross-built from Linux|g++-mingw-w64-i686|mingw32-gcc-c++|mingw-w64-gcc|-" ;; qemu) echo "qemu|optional|qemu-user|runs ARM cross builds (breeze/scripts/run.sh, ctest)|qemu-user|qemu-user|qemu-user|-" ;; # apt names wine64 outright: `wine` only depends on "wine64 | wine32", # an either/or, so `apt-get install wine wine32:i386` lets apt satisfy # it with wine32 alone and leave wine64 out (it installs wine, # wine32:i386 and libwine:i386 and nothing else). Without wine64 no # 64-bit Windows program -- windows-x64-mingw64 -- can run at all. wine) echo "wine|optional|wine|runs MinGW cross builds (breeze/scripts/run.sh, ctest)|wine wine64 wine32:i386|wine|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: only for architectures this machine can build FOR # without being them, and only where a package (or, for dnf, a COPR -- # see _pmn_dnf_copr_for) is verified to actually provide a working one -- # confirmed against each distro's own package pages (and, for apt and # the pacman rows below, this project's own users), not assumed from one # distro's naming carrying over to another's. See _pmn_arch_extra_notes # for what's deliberately still left out here and why: Fedora's own # repos have no C++-capable Linux-target cross compiler at all (its # cross-gcc packages ship no libstdc++, which is why dnf's rows below # point at a COPR instead), no package manager here has one for # x86-64-from-arm64, and pacman has no OFFICIAL package for 32-bit ARM # either (AUR-only -- see _pmn_offer_aur_arm32). case "$PMN_PM" in apt) 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 ;; pacman) case "$PMN_ARCH" in # xc-arm32 has no pacman package (its row says "-"): it is # listed so the report shows whether it works, and # _pmn_offer_aur_arm32 does the installing. x64) ids="$ids xc-arm64 xc-arm32 mingw64 mingw32 qemu wine" ;; arm64) ids="$ids mingw64 mingw32 qemu" ;; esac ;; dnf) case "$PMN_ARCH" in x64) ids="$ids xc-arm64 xc-arm32 mingw64 mingw32 qemu wine" ;; arm64) ids="$ids xc-arm32 mingw64 mingw32 qemu wine" ;; *) ids="$ids mingw64 mingw32 qemu wine" ;; esac ;; esac echo "$ids" } # ----------------------------------------------------------------------- # Distro situations the catalog's plain "-" (not offered through this # package manager) doesn't explain on its own. Printed once, right after # the report, so "why isn't X in this list" has an answer instead of # looking like an oversight. # ----------------------------------------------------------------------- _pmn_arch_extra_notes() { case "$PMN_PM" in apt) if [ "$PMN_ARCH" = arm64 ] && ! _pmn_have wine && ! _pmn_have wine64; then _pmn_note " wine isn't offered here: Debian/Ubuntu do package it for arm64, but it's" _pmn_note " had long-standing arm64-host reliability problems there (hangs on" _pmn_note " startup on recent releases -- e.g. Ubuntu bug 2102681) serious enough" _pmn_note " that offering it would likely just fail. Install wine by hand if you" _pmn_note " want to try it anyway; the windows-*-mingw* presets don't need it --" _pmn_note " it's ctest and breeze/scripts/run.sh that use it to run what they build." fi ;; dnf) _pmn_note " Fedora's own gcc-aarch64-linux-gnu / gcc-arm-linux-gnu / gcc-x86-64-linux-gnu" _pmn_note " packages ship no libstdc++ -- their own package description says" _pmn_note " cross-building userspace (i.e. non-kernel) programs isn't supported, and a" _pmn_note " C++ project like this one needs it. Where the list above offers an AArch64" _pmn_note " or 32-bit ARM cross compiler on dnf, it's from lantw44's COPR instead (a" _pmn_note " full GCC+glibc build, not Fedora's own package) -- accepting it enables" _pmn_note " that community repository, with its own separate confirmation. It can lag" _pmn_note " a new Fedora release by a while, so a just-released Fedora may not have a" _pmn_note " build there yet. MinGW (Windows) cross-compilation is a separate, complete" _pmn_note " Fedora project and works fine through Fedora's own mingw64-gcc-c++ /" _pmn_note " mingw32-gcc-c++ -- no COPR needed." ;; pacman) if [ "$PMN_ARCH" = x64 ] && [ "$(_pmn_tool_status xc-arm32 | cut -d' ' -f1)" != ok ]; then _pmn_note " 32-bit ARM cross compiler (linux-arm32-*): no official Arch package. The AUR" _pmn_note " has ARM's own prebuilt GNU toolchain split into four packages" _pmn_note " (arm-linux-gnueabihf-{binutils,glibc-headers,glibc,gcc}-bin): a download of a" _pmn_note " couple of hundred MB, not a compile. It is offered as its own question when" _pmn_note " you run this interactively; breeze/scripts/setup.sh --everything does it without" _pmn_note " asking. (Installing just arm-linux-gnueabihf-gcc-bin through an AUR helper" _pmn_note " builds the source-built binutils and glibc instead -- hours, and a toolchain" _pmn_note " whose parts don't fit together.)" elif [ "$PMN_ARCH" = arm64 ]; then _pmn_note " 32-bit ARM cross compiler (linux-arm32-*): not offered here -- the prebuilt" _pmn_note " AUR toolchain (arm-linux-gnueabihf-*-bin) is ARM's x86-64-hosted build, which" _pmn_note " can't run on an arm64 machine. ARM publishes an AArch64-hosted build of the" _pmn_note " same toolchain (developer.arm.com, GNU toolchain downloads) to unpack by hand." fi ;; esac case "$PMN_PM" in dnf|pacman) if [ "$PMN_ARCH" = arm64 ]; then _pmn_note " x86-64 cross compiler (linux-x64-* built from arm64): apt ships one" _pmn_note " (g++-x86-64-linux-gnu); no dnf or pacman equivalent is known to exist yet." fi ;; esac } # ----------------------------------------------------------------------- # Does a cross compiler WORK, or does it merely exist? # # `command -v arm-linux-gnueabihf-g++` can't tell, and a half-installed # toolchain passes it: setup.sh used to report the 32-bit ARM compiler as # installed while its g++ couldn't assemble a single file -- the assembler # and C library on the machine came from a different package set than the # compiler (see _pmn_aur_arm32_build). So the cross-compiler rows compile # and LINK a small C++ program, which needs every piece a real build needs # (the target's assembler, headers, libstdc++, crt1.o and libc), and # anything that fails reads "partial" instead of "ok". What comes out is a # foreign-architecture executable, so nothing is run. # ----------------------------------------------------------------------- # _pmn_cross_probe : 0 if builds and links a C++ # program for its target. Otherwise 1, with the compiler's first error line # on stdout. _pmn_cross_probe() { local cxx="$1" dir out="" rc=0 line dir="$(mktemp -d 2>/dev/null)" || return 0 # can't probe: don't cry wolf printf '#include \nint main() { std::cout << 1; }\n' > "$dir/probe.cpp" if _pmn_have timeout; then out="$(timeout 120 "$cxx" "$dir/probe.cpp" -o "$dir/probe" 2>&1 &1 : the catalog status ("ok", "missing" or # "partial ") of -g++. _pmn_cross_status() { local cxx="$1-g++" why if ! _pmn_have "$cxx"; then echo missing; return 0; fi if why="$(_pmn_cross_probe "$cxx")"; then echo ok else echo "partial can't build a test program: $why" fi } # _pmn_cross_diagnose : the facts that explain a cross compiler # that doesn't work -- which target it was built for, which sysroot and # assembler it looks for, and which of its packages are installed. Readable # as it is, and complete enough to paste into a bug report. _pmn_cross_diagnose() { local cxx="$1" opt triplet triplet="${cxx%-g++}" for opt in -dumpmachine -print-sysroot -print-prog-name=as; do _pmn_note " $cxx $opt -> $("$cxx" "$opt" 2>&1 /dev/null | grep "^${triplet}-" | tr '\n' ' ')" fi } # ----------------------------------------------------------------------- # Never answered for you: the slow or third-party extras (today, the AUR # toolchain below). _pmn_confirm takes `--all`, and an earlier "a", as a # yes; these don't count here -- only --everything (which never reaches # this) or a y typed at this very question. 0 for yes. # ----------------------------------------------------------------------- _pmn_confirm_heavy() { local ans [ "$_PMN_ANS_QUIT" = 1 ] && return 1 [ "$_PMN_ANS_SKIP_OPT" = 1 ] && return 1 [ -t 0 ] || return 1 if ! IFS= read -r -p " $1 [y/N] " ans; then echo _PMN_ANS_QUIT=1 return 1 fi case "$ans" in y|Y|yes|YES|Yes) return 0 ;; esac return 1 } # After a failed AUR build or install: what the usual causes look like, and # what to do about each. This script can't tell them apart from an exit # status alone. _pmn_note_aur_hint() { _pmn_note " If the output above ends in a key error ('PGP keys need importing', 'problem" _pmn_note " importing keys', 'unknown public key'): import the key it names (gpg --recv-keys" _pmn_note " ; for a kernel.org address, gpg --auto-key-locate nodefault,wkd --locate-keys" _pmn_note "
) and run this again. If it ends in a conflict ('are in conflict'," _pmn_note " 'conflicting files', 'exists in filesystem'): remove the package that owns the" _pmn_note " file or that is named, and run this again. Either way the packages' pages say" _pmn_note " more: https://aur.archlinux.org/packages/arm-linux-gnueabihf-gcc-bin" } # ----------------------------------------------------------------------- # The 32-bit ARM cross compiler on Arch (x64 hosts). # # Arch has no official package for it. The AUR has two families: # # * arm-linux-gnueabihf-{binutils,linux-api-headers,gcc-stage1, # glibc-headers,gcc-stage2,glibc,gcc}: GCC bootstrapped from source, # stage by stage. Hours of compiling, and each stage conflicts with the # one before it. # * arm-linux-gnueabihf-{binutils,glibc-headers,glibc,gcc}-bin: ARM's own # x86-64-hosted GNU toolchain (arm-none-linux-gnueabihf, 15.2.rel1), # prebuilt and split into four packages that each `provide` the name of # their source-built twin. A download, not a compile -- and x86-64 only, # which is why arm64 hosts aren't offered it. # # `yay -S arm-linux-gnueabihf-gcc-bin` -- what this script used to run, in # three steps -- looks like the second family and builds the first. The # package's dependencies are written as the generic names (binutils, glibc, # glibc-headers); a helper looks for a package of exactly that name first, # finds the source-built one, and only then at what merely provides it. The # result was hours of compiling and a MIXED toolchain: ARM's prebuilt # compiler (which looks for its assembler and C library where ARM's layout # puts them) next to a source-built binutils and glibc (which put them # somewhere else). It "compiled okay" and then failed every use: # # linux-arm32-gcc "Fatal error: invalid -march= option: `armv7-a+simd'" # -- that is the host's x86 assembler: the compiler # couldn't find its own, so it fell back to PATH. # linux-arm32-clang "Scrt1.o: file not recognized" -- the sysroot the # compiler reports (/arm-none-linux-gnueabihf/libc) # isn't there, so the linker found the host's. # # So the four -bin packages are installed by name, as ONE pacman # transaction: build each from its AUR repository with makepkg (no # dependency resolution by an AUR helper to get wrong -- and none needed, # which is also why no helper has to be installed), then `pacman -U` them # together, so every dependency among them is satisfied by another of the # four and nothing is built from source. --ask 4 is pacman's pre-answer-yes # for "X and Y are in conflict. Remove Y?" (its default, which --noconfirm # takes, is No): the -bin packages conflict with their source-built twins, # and an earlier source build is exactly the state a repair starts from. It # answers yes for any conflicting package in that transaction, not only # those, and the prompt says so. # ----------------------------------------------------------------------- _PMN_ARM32_AUR_PACKAGES="arm-linux-gnueabihf-binutils-bin arm-linux-gnueabihf-glibc-headers-bin arm-linux-gnueabihf-glibc-bin arm-linux-gnueabihf-gcc-bin" # _pmn_aur_arm32_build: clones the four AUR repositories, builds them with # makepkg, and installs them together. The work directory is removed on # success; after a failure it stays, with the (shared) source download in it, # so running this again doesn't fetch ARM's toolchain a second time. _pmn_aur_arm32_build() { local work pkg dir stage staged=() files=() f if [ "$(id -u)" = 0 ]; then _pmn_say " ${_PMN_RED}can't build an AUR package as root${_PMN_RESET} -- makepkg refuses to." _pmn_say " Re-run as a normal user with sudo." return 1 fi if [ "$PMN_SUDO" = "?" ]; then _pmn_say " ${_PMN_RED}cannot install the packages:${_PMN_RESET} installing needs root, and sudo isn't available." return 1 fi if ! { _pmn_have git && _pmn_have makepkg && _pmn_have fakeroot; }; then _pmn_pkg_install "git base-devel" || return 1 fi # Under the cache directory, not /tmp: /tmp is RAM-backed on Arch, and # ARM's toolchain unpacks to well over a gigabyte. work="${XDG_CACHE_HOME:-$HOME/.cache}/pmn-aur-arm32" mkdir -p "$work/src" "$work/git" "$work/pkgs" || return 1 rm -f "$work"/pkgs/* # All four PKGBUILDs download the same ARM tarball: SRCDEST makes makepkg # fetch it once and reuse it (it reads SRCDEST/PKGDEST from the # environment, ahead of makepkg.conf). --clean drops each build's # unpacked copy as soon as its package exists, so only one is on disk at a # time; --nodeps because these only repackage a binary release -- the # dependency check that matters happens at install, below. for pkg in $_PMN_ARM32_AUR_PACKAGES; do dir="$work/git/$pkg" rm -rf "$dir" _pmn_run_shown git clone --depth 1 "https://aur.archlinux.org/${pkg}.git" "$dir" || return 1 # The AUR answers a clone of a name it doesn't have with an empty # repository and a warning, not an error -- which is what a package # that was renamed, merged into another or removed looks like. if [ ! -f "$dir/PKGBUILD" ]; then _pmn_say " ${_PMN_RED}the AUR repository ${pkg} has no PKGBUILD${_PMN_RESET} -- the package may have been renamed, merged into another or removed." _pmn_say " Look for what replaced it: https://aur.archlinux.org/packages?K=arm-linux-gnueabihf" return 1 fi (cd "$dir" && _pmn_run_shown env SRCDEST="$work/src" PKGDEST="$work/pkgs" \ makepkg --force --clean --nodeps --noconfirm) || return 1 done for f in "$work"/pkgs/*.pkg.tar.*; do [ -e "$f" ] || continue case "$f" in *.sig|*-debug-*) continue ;; esac files+=("$f") done if [ "${#files[@]}" -eq 0 ]; then _pmn_say " ${_PMN_RED}makepkg finished, but left no package in${_PMN_RESET} $work/pkgs" return 1 fi # The bootstrap compilers of an earlier source build install the same # /usr/bin/arm-linux-gnueabihf-* names as gcc-bin, with no conflict # declared between them, so they have to go first (plain -R, not -s, # which would also take what was installed for later). for stage in arm-linux-gnueabihf-gcc-stage2 arm-linux-gnueabihf-gcc-stage1; do if pacman -Qq "$stage" >/dev/null 2>&1; then staged+=("$stage"); fi done if [ "${#staged[@]}" -gt 0 ]; then _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} pacman -R --noconfirm "${staged[@]}" || return 1 fi _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} pacman -U --noconfirm --needed --ask 4 "${files[@]}" || return 1 rm -rf "$work" } # _pmn_report_arm32_install: after the install itself reported success -- # which says nothing about whether the compiler works. Says what it found. _pmn_report_arm32_install() { local why if ! _pmn_have arm-linux-gnueabihf-g++; then _pmn_say " ${_PMN_YELLOW}the packages installed, but arm-linux-gnueabihf-g++ still isn't on PATH${_PMN_RESET} -- check the output above." return 1 fi if why="$(_pmn_cross_probe arm-linux-gnueabihf-g++)"; then _pmn_say " ${_PMN_GREEN}installed${_PMN_RESET} -- and it compiles and links a test program" return 0 fi _pmn_say " ${_PMN_YELLOW}installed, but arm-linux-gnueabihf-g++ can't build a test program:${_PMN_RESET} $why" _pmn_cross_diagnose arm-linux-gnueabihf-g++ _pmn_note " That is what breeze/cmake/toolchains/ would hit too. Paste the lines above into an" _pmn_note " issue: they say which assembler and sysroot the compiler looks for." return 1 } # ----------------------------------------------------------------------- # _pmn_offer_aur_arm32: the pacman/x64 half of the situation # _pmn_arch_extra_notes describes. Offered when the 32-bit ARM compiler is # missing -- or present but unable to build anything, the state the old # three-step source build left behind; the same install repairs it. # # Never done without an answer from you: --all and an earlier "a" don't # count (see _pmn_confirm_heavy), only --everything or a y at this question. # It runs community PKGBUILDs as you and downloads ARM's toolchain, so it # is a separate decision from "install the usual tools". # # Uses bash's dynamic scoping deliberately: `offered` and `skipped_optional` # are the CALLER's (_pmn_tools_run's) `local` variables, still in scope # here since this is called from within that function, so accepting or # declining this folds into the SAME summary the catalog rows use instead # of printing a separate one. # ----------------------------------------------------------------------- _pmn_offer_aur_arm32() { [ "$PMN_PM" = pacman ] && [ "$PMN_ARCH" = x64 ] || return 0 [ "$_PMN_ANS_QUIT" = 1 ] && return 0 local state detail label="32-bit ARM cross compiler (AUR, prebuilt)" state="$(_pmn_tool_status xc-arm32)" [ "${state%% *}" = ok ] && return 0 detail="${state#* }"; [ "$detail" = "$state" ] && detail="" if ! _pmn_heavy_wanted; then if [ "$PMN_TOOLS_ALL" = 1 ] || [ "$_PMN_ANS_SKIP_OPT" = 1 ] || [ ! -t 0 ]; then skipped_optional="${skipped_optional:+$skipped_optional, }$label" return 0 fi fi offered=$((offered + 1)) _pmn_say _pmn_say " ${_PMN_BOLD}${label}${_PMN_RESET} -- the linux-arm32-* presets, built from this machine" if [ -n "$detail" ]; then _pmn_note " arm-linux-gnueabihf-g++ is already installed, but it $detail" _pmn_note " This install repairs that: it replaces the pieces that don't belong together." fi _pmn_note " No official Arch package exists for this (see the note above). The AUR has" _pmn_note " ARM's own GNU toolchain (15.2.rel1, x86-64 hosts) repackaged as four prebuilt" _pmn_note " packages -- arm-linux-gnueabihf-{binutils,glibc-headers,glibc,gcc}-bin: a" _pmn_note " download of a couple of hundred MB and a few minutes, no compiling. They are" _pmn_note " community-maintained, not reviewed by Arch, and their PKGBUILDs run as you." _pmn_note " Each is built from its AUR repository with makepkg (git and base-devel are" _pmn_note " installed first if missing), then all four are installed in ONE pacman" _pmn_note " transaction, with --ask 4: pacman's pre-answer-yes for \"X and Y are in" _pmn_note " conflict. Remove Y?\", whose default (and --noconfirm's) is No. That is how a" _pmn_note " source-built arm-linux-gnueabihf-binutils or -glibc from an earlier attempt" _pmn_note " gets replaced; it answers yes for any conflicting package in that transaction," _pmn_note " not only those. Leftover gcc-stage1/stage2 compilers are removed first." if _pmn_heavy_wanted || _pmn_confirm_heavy "Build and install these four packages now?"; then if _pmn_aur_arm32_build; then _pmn_report_arm32_install || true else _pmn_say " ${_PMN_RED}the install failed${_PMN_RESET} -- see the output above." _pmn_note_aur_hint fi else skipped_optional="${skipped_optional:+$skipped_optional, }$label" fi } # Field (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 ] } # Is "everything" mode on (setup.sh --everything)? A flag only, with no # matching answer at the prompts: what it unlocks is slow or third-party # enough that it shouldn't be one keystroke at a question about something # else. _pmn_heavy_wanted() { [ "$PMN_TOOLS_HEAVY" = 1 ]; } # _pmn_confirm # # 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 : 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" } # wine on Arch lives in the multilib repository, which is DISABLED by # default -- confirmed against the Arch Wiki and Arch Linux Forums, and # the standard symptom without it is `pacman -S wine` failing with # "error: target not found: wine", which would otherwise look like this # script's package name is simply wrong. `pacman-conf --repo-list` is the # reliable way to ask pacman which repositories are actually active, # rather than grep-ing /etc/pacman.conf by hand (which a comment, or an # include from a different file, can easily fool). _pmn_pacman_multilib_enabled() { pacman-conf --repo-list 2>/dev/null | grep -qx multilib } # Offers to enable it -- editing /etc/pacman.conf is a bigger step than a # normal package install, so it gets its own explicit confirmation, and # says exactly what will change, before wine's own "Install wine?" # question ever appears. # # Two ways it can be missing, tried in order: most installs ship the # standard commented-out block, which the sed uncomments in place; some # minimal/container base images (confirmed against an Arch distrobox # image, which is how most people meet this path in practice) strip that # block out of /etc/pacman.conf entirely rather than just commenting it, # in which case the sed is a same-file no-op (a real edit either finds # something to change or it doesn't -- there's no way to tell those apart # except checking after) and the same two lines the Arch Wiki documents # get appended instead. Only the first method that actually results in # multilib being enabled runs pacman -Sy, so a real config error still # ends in the honest "enable by hand" message rather than silently # appending a second, conflicting [multilib] section. _pmn_pacman_enable_multilib() { if [ "$PMN_SUDO" = "?" ]; then _pmn_say " ${_PMN_RED}cannot enable multilib:${_PMN_RESET} editing /etc/pacman.conf needs root, and sudo isn't available." return 1 fi _pmn_say " wine is in Arch's multilib repository, which isn't enabled on this machine." _pmn_note " That needs a one-time change to /etc/pacman.conf: uncommenting (or adding)" _pmn_note " [multilib]" _pmn_note " Include = /etc/pacman.d/mirrorlist" if ! _pmn_confirm optional "Enable the multilib repository now?"; then return 1 fi _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} sed -i \ -e '/^#\[multilib\]/s/^#//' \ -e '/^\[multilib\]/{n;s/^#//}' \ /etc/pacman.conf || return 1 if ! _pmn_pacman_multilib_enabled && ! grep -qx '\[multilib\]' /etc/pacman.conf 2>/dev/null; then _pmn_say " ${_PMN_YELLOW}no [multilib] block found to uncomment${_PMN_RESET} -- appending one instead." _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} sh -c \ "printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf" || return 1 fi if ! _pmn_pacman_multilib_enabled; then _pmn_say " ${_PMN_YELLOW}/etc/pacman.conf doesn't look like the standard layout this edits${_PMN_RESET} -- enable [multilib] by hand, then re-run." return 1 fi _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} pacman -Sy || return 1 } # The Fedora-targeting cross toolchains that need a COPR instead of a # Fedora-native package (see _pmn_arch_extra_notes for why): prints # "/" for the ids that need one, empty for anything else. # One place the confirmation prompt and the install step both read from, so # they can't drift apart. lantw44's toolchains are crosstool-NG builds of a # full GCC+glibc (C++ included, unlike Fedora's own cross-gcc) -- see # https://copr.fedorainfracloud.org/coprs/lantw44/. _pmn_dnf_copr_for() { case "$1" in xc-arm64) echo "lantw44/aarch64-linux-gnu-toolchain" ;; xc-arm32) echo "lantw44/arm-linux-gnueabihf-toolchain" ;; esac } # Is / already enabled? Asking dnf what's enabled, rather # than guessing the generated .repo filename on disk, is the same approach # _pmn_pacman_multilib_enabled takes with `pacman-conf --repo-list`. Best # effort: "no" here on a dnf too old for the copr plugin just means # _pmn_dnf_enable_copr asks again next run instead of remembering -- annoying, # never unsafe, since enabling an already-enabled COPR is a harmless no-op. _pmn_dnf_copr_enabled() { dnf -q repolist --enabled 2>/dev/null | grep -qi "copr.*${1#*/}" } # Offers to enable it -- adding a repository is a bigger step than a normal # package install, so like wine's multilib prompt above, it gets its own # explicit confirmation, separate from the "install this tool?" question # that precedes it. COPR builds happen on Fedora's own servers (nothing # compiles on your machine, unlike the AUR case below) but a COPR project is # run by its owner, not reviewed by Fedora -- said plainly, once, before # anything is added. _pmn_dnf_enable_copr() { local repo="$1" if [ "$PMN_SUDO" = "?" ]; then _pmn_say " ${_PMN_RED}cannot enable this repository:${_PMN_RESET} needs root, and sudo isn't available." return 1 fi _pmn_say " Fedora's own repos have no working cross compiler for this (see above) --" _pmn_say " this one comes from ${repo}, a community COPR repository:" _pmn_note " https://copr.fedorainfracloud.org/coprs/${repo}/" _pmn_note " (built on Fedora's own servers, but not reviewed by Fedora)" if ! _pmn_confirm optional "Enable the ${repo} COPR repository now?"; then return 1 fi _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} dnf -y copr enable "$repo" || return 1 } # Running a 32-bit Windows binary (the windows-x86-mingw32 preset) under # Wine on an x64 Debian/Ubuntu host needs wine32:i386 -- a SEPARATE # package from wine/wine64, on a SEPARATE (i386) architecture that apt # doesn't register by default. Confirmed against Debian's own wiki and # several Debian/Ubuntu bug reports: without this, `wine` installs and # runs fine on 64-bit binaries, and only announces the problem the first # time it's actually asked to run a 32-bit one -- "it looks like wine32 # is missing ... multiarch needs to be enabled first" -- which is Wine's # own message, not this script's, and by then setup.sh already exited # successfully long before, having had no reason to think anything was # left to do; there's nothing to detect this from wine's own status. So # the apt package list for wine (below) includes wine32:i386 unconditionally # whenever wine is offered at all (x64 hosts only -- see _pmn_tools_ids), # and this runs first to make that installable, the same two-step # separate-confirmation shape as pacman's multilib above. _pmn_apt_multiarch_i386_enabled() { dpkg --print-foreign-architectures 2>/dev/null | grep -qx i386 } # Is wine32:i386 actually installed? Wine itself (64-bit) runs without it and # prints its "it looks like wine32 is missing" complaint on EVERY run, so a # machine where wine was installed before wine32 was part of this script's # package list would otherwise look finished: wine present -> nothing left # to offer. Reported as "partial" (see the report loop) so it gets offered # again, and the install is idempotent -- wine is already there, only # wine32:i386 (and the i386 architecture it needs) is added. _pmn_apt_wine32_installed() { # shellcheck disable=SC2016 # ${Status} is dpkg-query's format syntax, not ours dpkg-query -W -f='${Status}' wine32:i386 2>/dev/null | grep -q '^install ok installed' } # The mirror image of the above, and the one that bit: is wine64 installed? # `wine` itself needs only one of wine64 and wine32, so a machine set up with # `apt-get install wine wine32:i386` has the 32-bit wine alone -- `wine` is # there, wine32 is there, every check above passes -- and can run no 64-bit # Windows program: the 32-bit loader can't, and creates a 32-bit prefix even # under WINEARCH=win64, then refuses it ("WINEARCH set to win64 but '...' is # a 32-bit installation"). _pmn_apt_wine64_installed() { # shellcheck disable=SC2016 # ${Status} is dpkg-query's format syntax, not ours dpkg-query -W -f='${Status}' wine64 2>/dev/null | grep -q '^install ok installed' } _pmn_apt_enable_multiarch_i386() { if [ "$PMN_SUDO" = "?" ]; then _pmn_say " ${_PMN_RED}cannot enable the i386 architecture:${_PMN_RESET} needs root, and sudo isn't available." return 1 fi _pmn_say " wine32:i386 -- needed to run 32-bit Windows binaries (the" _pmn_say " windows-x86-mingw32 preset) under Wine -- needs the i386 foreign" _pmn_say " architecture registered first (dpkg --add-architecture i386)." if ! _pmn_confirm optional "Register the i386 architecture now?"; then return 1 fi _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} dpkg --add-architecture i386 || return 1 # A fresh update is needed here specifically: i386's package lists # don't exist yet even if apt-get update already ran this session for # the native architecture, and _pmn_pkg_install's own update only runs # once (_PMN_APT_UPDATED) -- set the flag after so it doesn't run again # right behind this one for nothing. _pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} apt-get update || return 1 _PMN_APT_UPDATED=1 } # _pmn_install_row : install one catalog row; returns its status. _pmn_install_row() { local row="$1" id pkgs copr id="$(_pmn_field "$row" 1)" case "$id" in clt) _pmn_install_clt; return $? ;; brew) _pmn_install_brew; return $? ;; esac if [ "$id" = wine ] && [ "$PMN_PM" = pacman ] && ! _pmn_pacman_multilib_enabled; then _pmn_pacman_enable_multilib || return 1 fi if [ "$id" = wine ] && [ "$PMN_PM" = apt ] && ! _pmn_apt_multiarch_i386_enabled; then _pmn_apt_enable_multiarch_i386 || return 1 fi if [ "$PMN_PM" = dnf ]; then copr="$(_pmn_dnf_copr_for "$id")" if [ -n "$copr" ] && ! _pmn_dnf_copr_enabled "$copr"; then _pmn_dnf_enable_copr "$copr" || return 1 fi fi 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 breeze\\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" ;; partial) printf ' %s[part]%s %-30s %s%s%s\n' "$_PMN_YELLOW" "$_PMN_RESET" "$label" "$_PMN_DIM" "$detail" "$_PMN_RESET" ;; *) 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 _pmn_arch_extra_notes 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)" elif [ "${status%% *}" = partial ]; then _pmn_note " (already installed, but ${status#* })" 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" ;; partial) _pmn_say " ${_PMN_YELLOW}the install finished, but ${status#* }${_PMN_RESET} -- check the output above." 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 _pmn_offer_aur_arm32 if [ "$offered" -eq 0 ] && [ -z "$skipped_optional" ] && [ "$_PMN_ANS_QUIT" != 1 ]; then _pmn_say " Nothing to install." fi if [ -n "$skipped_optional" ]; then local install_hint="--all installs them all" [ "$PMN_PM" = pacman ] && install_hint="$install_hint; --everything also installs the AUR extras" _pmn_say _pmn_note " Optional, not installed: ${skipped_optional}" _pmn_note " (breeze/scripts/setup.sh, run interactively, offers each one; ${install_hint}.)" 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 : 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 }