358 lines
11 KiB
Bash
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"
|
|
}
|