# shellcheck shell=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/ 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// scratch build trees, deleted after install # toolchains/opt// 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" }