joeylib2/installer/lib/common.sh
2026-10-13 14:01:46 -05:00

358 lines
11 KiB
Bash

# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# Shared helpers for install.sh and the component files in
# installer/components/. Sourced, not executed.
#
# Tracked (the recipes):
# install.sh the command line
# installer/lib/ these helpers
# installer/components/<id> one recipe per component
# installer/patches/ patches applied to sources the installer builds
# make/support/ build helper scripts the makefiles call
# Generated (everything under toolchains/, which can be deleted at any time
# and re-created by install.sh):
# toolchains/cache/downloads/ sha256-verified downloads
# toolchains/cache/build/<id>/ scratch build trees, deleted after install
# toolchains/opt/<id>/ installed components
# toolchains/firmware/ ROM / OS images for the emulators
# toolchains/state/ stamps and settings
# toolchains/env.sh, config.mk the tool paths, for shells and for make
JOEY_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
JOEY_INSTALLER="$JOEY_REPO/installer"
JOEY_COMPONENTS="$JOEY_INSTALLER/components"
JOEY_PATCHES="$JOEY_INSTALLER/patches"
JOEY_BUILD_SUPPORT="$JOEY_REPO/make/support"
JOEY_TOOLCHAINS="$JOEY_REPO/toolchains"
JOEY_CACHE="$JOEY_TOOLCHAINS/cache/downloads"
JOEY_BUILD_ROOT="$JOEY_TOOLCHAINS/cache/build"
JOEY_OPT="$JOEY_TOOLCHAINS/opt"
JOEY_FIRMWARE="$JOEY_TOOLCHAINS/firmware"
JOEY_STATE="$JOEY_TOOLCHAINS/state"
JOEY_STAMPS="$JOEY_STATE/stamps"
JOEY_JOBS="${JOEY_JOBS:-$(nproc 2>/dev/null || echo 2)}"
JOEY_KEEP_BUILD="${JOEY_KEEP_BUILD:-0}"
JOEY_ASSUME_YES="${JOEY_ASSUME_YES:-0}"
JOEY_FORCE="${JOEY_FORCE:-0}"
# Never prompt for credentials: a missing or private repo must fail fast
# with a clear message instead of hanging on a username prompt.
export GIT_TERMINAL_PROMPT=0
if [ -t 1 ]; then
JOEY_C_RESET=$'\033[0m'
JOEY_C_BOLD=$'\033[1m'
JOEY_C_RED=$'\033[31m'
JOEY_C_GREEN=$'\033[32m'
JOEY_C_YELLOW=$'\033[33m'
JOEY_C_CYAN=$'\033[36m'
else
JOEY_C_RESET=""
JOEY_C_BOLD=""
JOEY_C_RED=""
JOEY_C_GREEN=""
JOEY_C_YELLOW=""
JOEY_C_CYAN=""
fi
info() {
printf '%s[*]%s %s\n' "$JOEY_C_CYAN" "$JOEY_C_RESET" "$*"
}
ok() {
printf '%s[ok]%s %s\n' "$JOEY_C_GREEN" "$JOEY_C_RESET" "$*"
}
warn() {
printf '%s[!!]%s %s\n' "$JOEY_C_YELLOW" "$JOEY_C_RESET" "$*" >&2
}
die() {
printf '%s[xx]%s %s\n' "$JOEY_C_RED" "$JOEY_C_RESET" "$*" >&2
exit 1
}
header() {
printf '\n%s== %s ==%s\n' "$JOEY_C_BOLD" "$*" "$JOEY_C_RESET"
}
sha256Of() {
sha256sum "$1" | cut -d' ' -f1
}
# fetchFile URL SHA256 [NAME [HEADER]] -- download URL into the cache as
# NAME (default: the URL's basename), verify it, and print its path. A
# cached file is reused only if its checksum still matches; downloads go to
# a .part file first, so an interrupted fetch is never mistaken for a
# complete one. HEADER is sent with the request (an authorization).
fetchFile() {
local url="$1"
local want="$2"
local name="${3:-$(basename "$url")}"
local header="${4:-}"
local dest="$JOEY_CACHE/$name"
local got
mkdir -p "$JOEY_CACHE"
if [ -f "$dest" ]; then
got="$(sha256Of "$dest")"
if [ "$got" = "$want" ]; then
printf '%s\n' "$dest"
return 0
fi
warn "cached $name has the wrong checksum; downloading again"
rm -f "$dest"
fi
info "downloading $name" >&2
curl -fL --retry 3 --connect-timeout 30 --progress-bar ${header:+-H "$header"} -o "$dest.part" "$url" \
|| die "download failed: $url"
got="$(sha256Of "$dest.part")"
if [ "$got" != "$want" ]; then
rm -f "$dest.part"
die "checksum mismatch for $url
expected $want
got $got
The upstream file changed. Check it, then update the pin in the component file."
fi
mv "$dest.part" "$dest"
printf '%s\n' "$dest"
}
# fetchOciBlob REPOSITORY DIGEST NAME -- fetchFile for one blob (an image
# layer) of a public image on the GitHub container registry, by its sha256
# digest, which is also its checksum. The registry wants an anonymous token
# even for public images; none is asked for when the blob is cached.
fetchOciBlob() {
local token=""
if [ "$(sha256Of "$JOEY_CACHE/$3" 2>/dev/null || true)" != "$2" ]; then
token="$(curl -fsSL "https://ghcr.io/token?scope=repository:$1:pull" | python3 -c 'import json, sys; print(json.load(sys.stdin)["token"])')" \
|| die "cannot get a registry token for $1"
fi
fetchFile "https://ghcr.io/v2/$1/blobs/sha256:$2" "$2" "$3" "Authorization: Bearer $token"
}
# gitFetchCommit URL SHA DIR -- make DIR a shallow checkout of exactly SHA.
# Reuses an existing clone when it can.
gitFetchCommit() {
local url="$1"
local sha="$2"
local dir="$3"
if [ -d "$dir/.git" ] && [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$sha" ]; then
return 0
fi
if [ ! -d "$dir/.git" ]; then
rm -rf "$dir"
mkdir -p "$dir"
git -C "$dir" init -q
git -C "$dir" remote add origin "$url"
else
git -C "$dir" remote set-url origin "$url"
fi
git -C "$dir" fetch -q --depth=1 origin "$sha" || die "could not fetch $sha from $url"
git -C "$dir" checkout -q -f --detach FETCH_HEAD
}
# lhaCommand -- the LHA extractor: the lhasa package installs it as lhasa
# on Debian and Ubuntu but as lha on Fedora.
lhaCommand() {
command -v lhasa || command -v lha || die "no lhasa or lha command (install the lhasa package)"
}
# extractArchive ARCHIVE DEST [STRIP] -- unpack ARCHIVE into DEST,
# dropping STRIP leading path components (tar formats only).
extractArchive() {
local archive="$1"
local dest="$2"
local strip="${3:-0}"
mkdir -p "$dest"
case "$archive" in
*.tar.gz|*.tgz) tar -xzf "$archive" -C "$dest" --strip-components="$strip" ;;
*.tar.bz2) tar -xjf "$archive" -C "$dest" --strip-components="$strip" ;;
*.tar.xz) tar -xJf "$archive" -C "$dest" --strip-components="$strip" ;;
*.zip) unzip -oq "$archive" -d "$dest" ;;
*.lha|*.lzh|*.LZH) (cd "$dest" && "$(lhaCommand)" xq "$archive") ;;
*) die "don't know how to extract $archive" ;;
esac
}
# freshBuildDir ID -- print an empty scratch directory for building ID.
freshBuildDir() {
local dir="$JOEY_BUILD_ROOT/$1"
rm -rf "$dir"
mkdir -p "$dir"
printf '%s\n' "$dir"
}
# dropBuildDir ID -- delete ID's scratch tree unless --keep-build.
dropBuildDir() {
if [ "$JOEY_KEEP_BUILD" = "1" ]; then
info "keeping build tree $JOEY_BUILD_ROOT/$1"
else
rm -rf "${JOEY_BUILD_ROOT:?}/$1"
fi
}
# verifyCompiles CC [FLAGS...] -- compile a trivial C file to an object;
# the smoke test the cross-compiler components share.
verifyCompiles() {
local tmp
local status=1
tmp="$(mktemp -d)"
printf 'int answer(int x) { return x * 42; }\n' > "$tmp/t.c"
if "$@" -c "$tmp/t.c" -o "$tmp/t.o" && [ -s "$tmp/t.o" ]; then
status=0
fi
rm -rf "$tmp"
return $status
}
# verifyLinked DIR... -- every dynamically linked executable directly in
# the DIRs finds its shared libraries. A prebuilt toolchain can need one the
# host lacks (djgpp's ar wants the flex runtime, libfl.so.2), and a
# compile-only smoke test never runs ar.
verifyLinked() {
local dir
local f
local out
local line
local status=0
for dir in "$@"; do
for f in "$dir"/*; do
[ -f "$f" ] && [ -x "$f" ] || continue
# Scripts and static binaries are not dynamic executables.
out="$(ldd "$f" 2>/dev/null)" || continue
while IFS= read -r line; do
if [[ "$line" == *"not found"* ]]; then
warn "${f#"$JOEY_REPO"/} needs ${line#"${line%%[![:space:]]*}"}"
status=1
fi
done <<< "$out"
done
done
return $status
}
# ---- stamps ---------------------------------------------------------------
# A stamp records the pin a component was installed from, so moving the pin
# in the component file makes the component out of date.
stampRead() {
cat "$JOEY_STAMPS/$1" 2>/dev/null || true
}
stampWrite() {
mkdir -p "$JOEY_STAMPS"
printf '%s\n' "$2" > "$JOEY_STAMPS/$1"
}
stampClear() {
rm -f "$JOEY_STAMPS/$1"
}
# ---- components -------------------------------------------------------------
# Every component file defines:
# compDescription one line for `list`
# compGroups groups it belongs to, e.g. "toolchain:amiga"
# compRequires components that must be installed first
# compHostDeps logical host package names (see distroPackages.txt)
# compPin anything that changes when the component must be rebuilt
# compInstall installs into $compDir
# compInstalled cheap check that the install is present
# compVerify smoke test for `doctor`
# compEnv emits the variables it provides (envVar / envPath)
# and a private-file component (lib/bundle.sh) also sets compPrivateFile.
# The file is always sourced in a subshell, so components can use the same
# function names without colliding.
componentIds() {
local f
for f in "$JOEY_COMPONENTS"/*.sh; do
basename "$f" .sh
done
}
componentFile() {
local file="$JOEY_COMPONENTS/$1.sh"
[ -f "$file" ] || die "unknown component: $1"
printf '%s\n' "$file"
}
# compQuery ID VAR -- print a component file's metadata variable.
compQuery() {
local file
file="$(componentFile "$1")"
(
compId="$1"
compDir="$JOEY_OPT/$1"
# shellcheck disable=SC1090
source "$file"
printf '%s\n' "${!2:-}"
)
}
# compRun ID FUNCTION [ARGS...] -- run one of a component's functions.
# It runs in a separate bash PROCESS, not a subshell: callers check the
# result (compRun ... || die), and bash disables set -e inside anything
# whose status is being checked, subshells included -- a failing step in
# the middle of compInstall would then be skipped silently.
compRun() {
local file
file="$(componentFile "$1")"
JOEY_JOBS="$JOEY_JOBS" JOEY_KEEP_BUILD="$JOEY_KEEP_BUILD" \
JOEY_ASSUME_YES="$JOEY_ASSUME_YES" JOEY_FORCE="$JOEY_FORCE" \
bash -c '
set -euo pipefail
source "$1/common.sh"
source "$1/distro.sh"
source "$1/bundle.sh"
compId="$2"
compDir="$JOEY_OPT/$2"
mkdir -p "$JOEY_OPT"
source "$3"
shift 3
"$@"
' compRun "$JOEY_INSTALLER/lib" "$1" "$file" "${@:2}"
}
# compIsCurrent ID -- installed from the current pin and still present.
compIsCurrent() {
[ -n "$(stampRead "$1")" ] || return 1
[ "$(stampRead "$1")" = "$(compQuery "$1" compPin)" ] || return 1
compRun "$1" compInstalled >/dev/null 2>&1
}
# Used inside compEnv to publish a variable / a PATH directory.
envVar() {
printf 'var\t%s\t%s\n' "$1" "$2"
}
envPath() {
printf 'path\t%s\n' "$1"
}