joeylib2/install.sh

462 lines
15 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# JoeyLib installer: everything needed to build (and, with the emulator
# components, run and test) JoeyLib programs, pinned and checksummed.
# Host packages come from the Linux distribution (apt or dnf); everything
# else is downloaded or built under toolchains/ and nowhere else, so that
# folder can be deleted at any time and re-created by running this again.
#
# Usage:
# ./install.sh [options] <command> [targets...]
#
# Commands:
# list every component, its groups and whether it is current
# install TARGETS... install (or update) components and groups
# deps TARGETS... show the host packages TARGETS need (--install: install them)
# doctor check the host and smoke-test every installed component
# import PATH... take private files (Kickstart, ...) from files, folders
# or .zip archives, matched by checksum
# (installer/files.sha1)
# bundle FILE.tar.gz pack the private files present here, for the owner
# to host (JOEY_BUNDLE_URL in installer/bundle.conf)
# help this text
#
# Targets are component names (see `list`) or groups:
# toolchain:PORT emulator:PORT PORT toolchains emulators all
# where PORT is iigs, amiga, atarist, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos. `all`, `toolchains`
# and `emulators` leave out optional components (dvx, which unpacks a DVX
# tree's dist/ zips you have); name those explicitly.
#
# Options:
# -y, --yes install missing host packages without asking
# --install with `deps`: install the missing host packages
# --force reinstall even when a component is current
# --jobs N parallel build jobs (default: every core)
# --keep-build keep the scratch build trees in cache/build
# --llvm816-dir PATH use an existing llvm816 checkout for the IIgs
# compiler (remembered; "--llvm816-dir clone" undoes it)
# --dvx-dir PATH the DVX tree the dvx component unpacks DVX and its SDK
# from (its dist/dvx.zip and dist/sdk.zip; remembered;
# default: dvxgui beside this repository)
#
# Examples:
# ./install.sh install toolchains every cross compiler
# ./install.sh install emulator:dos what a tester needs for DOS
# ./install.sh install amiga Amiga compiler + emulator
# ./install.sh deps all --install just the host packages
#
# make reads toolchains/config.mk by itself. For interactive shells,
# `source toolchains/env.sh` sets the same variables.
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
set -euo pipefail
# shellcheck source=installer/lib/common.sh
source "$(dirname "${BASH_SOURCE[0]}")/installer/lib/common.sh"
# shellcheck source=installer/lib/distro.sh
source "$JOEY_INSTALLER/lib/distro.sh"
# shellcheck source=installer/lib/config.sh
source "$JOEY_INSTALLER/lib/config.sh"
# shellcheck source=installer/lib/bundle.sh
source "$JOEY_INSTALLER/lib/bundle.sh"
JOEY_PORTS="iigs amiga atarist dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos"
depsInstall=0
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
}
# groupMembers GROUP -- print the components in GROUP.
groupMembers() {
local group="$1"
local id
local groups
for id in $(componentIds); do
groups=" $(compQuery "$id" compGroups) "
case "$group" in
all)
if [[ "$groups" != *" optional "* ]]; then
printf '%s\n' "$id"
fi
;;
toolchains)
if [[ "$groups" == *" toolchain:"* && "$groups" != *" optional "* ]]; then
printf '%s\n' "$id"
fi
;;
emulators)
if [[ "$groups" == *" emulator:"* && "$groups" != *" optional "* ]]; then
printf '%s\n' "$id"
fi
;;
iigs|amiga|atarist|dos|x68000|dvx|sdl3|web|macos|win32|win16|psp|haiku|haiku32|beos)
if [[ "$groups" == *" toolchain:$group "* || "$groups" == *" emulator:$group "* ]]; then
printf '%s\n' "$id"
fi
;;
*)
if [[ "$groups" == *" $group "* ]]; then
printf '%s\n' "$id"
fi
;;
esac
done
}
# resolveTargets TARGET... -- print component ids, each after the components
# it requires.
resolveTargets() {
local target
local id
local members
declare -A seen=()
local ordered=()
addWithRequires() {
local req
[ -z "${seen[$1]:-}" ] || return 0
seen[$1]=1
for req in $(compQuery "$1" compRequires); do
addWithRequires "$req"
done
ordered+=("$1")
}
[ $# -gt 0 ] || die "no targets given (try: toolchains, emulators, all, or a port name)"
for target in "$@"; do
if [ -f "$JOEY_COMPONENTS/$target.sh" ]; then
addWithRequires "$target"
continue
fi
members="$(groupMembers "$target")"
[ -n "$members" ] || die "unknown target: $target (see: ./install.sh list)"
for id in $members; do
addWithRequires "$id"
done
done
printf '%s\n' "${ordered[@]}"
}
# hostPackagesFor ID... -- the distribution packages the components need.
hostPackagesFor() {
local id
local logical=()
for id in "$@"; do
# shellcheck disable=SC2207
logical+=($(compQuery "$id" compHostDeps))
done
[ ${#logical[@]} -gt 0 ] || return 0
distroPackageNames $(printf '%s\n' "${logical[@]}" | sort -u) | sort -u
}
cmdDeps() {
local ids
local pkgs
local missing
# Command substitution (not < <(...)) so a failure -- an unknown target,
# an unsupported distribution -- stops the script.
ids="$(resolveTargets "$@")"
pkgs="$(hostPackagesFor $ids)"
missing="$(distroMissing $pkgs)"
info "host packages for $*: $(printf '%s ' $pkgs)"
if [ -z "$missing" ]; then
ok "all present"
return 0
fi
warn "missing: $(printf '%s ' $missing)"
if [ "$depsInstall" = "1" ]; then
distroInstall $missing
else
info "install them with: ./install.sh deps $* --install"
return 1
fi
}
cmdInstall() {
local ids
local pkgs
local missing
local id
local installed=0
local idsText
idsText="$(resolveTargets "$@")"
mapfile -t ids <<< "$idsText"
pkgs="$(hostPackagesFor "${ids[@]}")"
missing="$(distroMissing $pkgs)"
if [ -n "$missing" ]; then
if [ "$JOEY_ASSUME_YES" = "1" ]; then
distroInstall $missing
else
die "missing host packages: $(printf '%s ' $missing)
Install them with: ./install.sh deps $* --install
or re-run with --yes to install them as part of this command."
fi
fi
# A failed component does not stop the others (a tester without the
# private bundle still gets every other emulator); anything that
# requires a failed component is skipped. All of it is reported at the
# end, and the exit status is non-zero.
local failed=()
local req
local blocked
for id in "${ids[@]}"; do
blocked=""
for req in $(compQuery "$id" compRequires); do
if [[ " ${failed[*]:-} " == *" $req "* ]]; then
blocked="$req"
fi
done
if [ -n "$blocked" ]; then
warn "$id skipped: it needs $blocked, which failed"
failed+=("$id")
continue
fi
if [ "$JOEY_FORCE" != "1" ] && compIsCurrent "$id"; then
ok "$id is current"
continue
fi
header "$id: $(compQuery "$id" compDescription)"
stampClear "$id"
if ! compRun "$id" compInstall; then
warn "$id: install failed"
failed+=("$id")
continue
fi
if ! compRun "$id" compInstalled; then
warn "$id: install finished but the result is missing"
failed+=("$id")
continue
fi
if ! compRun "$id" compVerify >/dev/null; then
warn "$id: installed, but its smoke test failed"
failed+=("$id")
continue
fi
stampWrite "$id" "$(compQuery "$id" compPin)"
ok "$id installed"
installed=$((installed + 1))
done
writeConfig
if [ ${#failed[@]} -gt 0 ]; then
die "$(( ${#ids[@]} - ${#failed[@]} )) of ${#ids[@]} components current; FAILED: ${failed[*]}"
fi
ok "${#ids[@]} components current ($installed installed now)"
info "make picks the tools up by itself; for a shell: source toolchains/env.sh"
}
cmdImport() {
local installedText
local id
[ $# -gt 0 ] || die "import needs at least one file, folder or .zip archive"
installedText="$(importPaths "$@")"
if [ -z "$installedText" ]; then
warn "nothing matched: none of those files is in installer/files.sha1"
return 1
fi
printf '%s\n' "$installedText" | sort -u | while read -r dest; do
ok "imported $dest"
done
# Mark the private components whose file is now in place as installed.
for id in $(componentIds); do
if [ -n "$(compQuery "$id" compPrivateFile)" ] && compRun "$id" compInstalled; then
stampWrite "$id" "$(compQuery "$id" compPin)"
fi
done
writeConfig
}
cmdList() {
local id
local status
local stamp
printf '%-13s %-9s %-46s %s\n' "COMPONENT" "STATUS" "GROUPS" "DESCRIPTION"
for id in $(componentIds); do
stamp="$(stampRead "$id")"
if [ -z "$stamp" ] || ! compRun "$id" compInstalled >/dev/null 2>&1; then
status="missing"
elif [ "$stamp" = "$(compQuery "$id" compPin)" ]; then
status="current"
else
status="outdated"
fi
printf '%-13s %-9s %-46s %s\n' "$id" "$status" "$(compQuery "$id" compGroups)" "$(compQuery "$id" compDescription)"
done
printf '\ngroups: toolchains emulators all %s, and toolchain:PORT / emulator:PORT\n' "$JOEY_PORTS"
}
# portGroups ID -- ID's toolchain:PORT and emulator:PORT groups.
portGroups() {
local group
for group in $(compQuery "$1" compGroups); do
case "$group" in
toolchain:*|emulator:*) printf '%s\n' "$group" ;;
esac
done
}
# wantedGroups -- the port groups the user chose: those holding an installed
# component that belongs to that group alone. Shared members (headless,
# mame, cadius) do not count, or installing one port would make
# every port that shares them look half-installed.
wantedGroups() {
local id
local groups
for id in $(componentIds); do
[ -n "$(stampRead "$id")" ] || continue
groups="$(portGroups "$id")"
[ -n "$groups" ] && [ "$groups" = "${groups%%$'\n'*}" ] && printf '%s\n' "$groups"
done
return 0
}
# firstGroupIn ID " GROUP... " -- print ID's first port group in the list.
firstGroupIn() {
local group
for group in $(portGroups "$1"); do
if [[ "$2" == *" $group "* ]]; then
printf '%s\n' "$group"
return 0
fi
done
}
cmdDoctor() {
local id
local failures=0
local family
local wanted
local group
header "host"
family="$(distroFamily)"
if [ "$family" = "unknown" ]; then
warn "unrecognised distribution: host packages must be installed by hand"
failures=$((failures + 1))
else
ok "package manager: $family"
fi
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
warn "bash ${BASH_VERSION} is older than 4"
failures=$((failures + 1))
fi
header "components"
wanted=" $(printf '%s ' $(wantedGroups))"
for id in $(componentIds); do
if [ -z "$(stampRead "$id")" ]; then
group="$(firstGroupIn "$id" "$wanted")"
if [ -n "$group" ]; then
warn "$id: not installed, but the rest of $group is (run: ./install.sh install $id)"
failures=$((failures + 1))
fi
elif ! compRun "$id" compInstalled >/dev/null 2>&1; then
if [ -n "$(compQuery "$id" compPrivateFile)" ]; then
warn "$id: $(compQuery "$id" compPrivateFile) is gone; supply it with ./install.sh import PATH (or JOEY_BUNDLE_URL)"
else
warn "$id: its files are gone (run: ./install.sh install $id)"
fi
failures=$((failures + 1))
elif ! compIsCurrent "$id"; then
warn "$id: out of date, its pin changed (run: ./install.sh install $id)"
failures=$((failures + 1))
elif compRun "$id" compVerify >/dev/null; then
ok "$id"
else
warn "$id: smoke test failed"
failures=$((failures + 1))
fi
done
writeConfig
if [ "$failures" -gt 0 ]; then
die "$failures problem(s) found"
fi
ok "everything installed checks out"
}
# ---- argument parsing -----------------------------------------------------
command=""
targets=()
while [ $# -gt 0 ]; do
case "$1" in
-y|--yes)
JOEY_ASSUME_YES=1
;;
--install)
depsInstall=1
;;
--force)
JOEY_FORCE=1
;;
--keep-build)
JOEY_KEEP_BUILD=1
;;
--jobs)
[ $# -ge 2 ] || die "--jobs needs a number"
JOEY_JOBS="$2"
shift
;;
--llvm816-dir)
[ $# -ge 2 ] || die "--llvm816-dir needs a path (or 'clone')"
mkdir -p "$JOEY_STATE"
if [ "$2" = "clone" ]; then
rm -f "$JOEY_STATE/llvm816Dir"
else
[ -f "$2/setup.sh" ] || die "$2 does not look like an llvm816 checkout (no setup.sh)"
(cd "$2" && pwd) > "$JOEY_STATE/llvm816Dir"
fi
shift
;;
--dvx-dir)
[ $# -ge 2 ] || die "--dvx-dir needs a path"
[ -f "$2/dist/dvx.zip" ] || die "$2 does not look like a DVX tree with its distribution made (no dist/dvx.zip)"
mkdir -p "$JOEY_STATE"
(cd "$2" && pwd) > "$JOEY_STATE/dvxDir"
shift
;;
-h|--help|help)
usage
exit 0
;;
-*)
die "unknown option: $1 (see: ./install.sh help)"
;;
*)
if [ -z "$command" ]; then
command="$1"
else
targets+=("$1")
fi
;;
esac
shift
done
case "$command" in
install) cmdInstall "${targets[@]}" ;;
deps) cmdDeps "${targets[@]}" ;;
import) cmdImport "${targets[@]}" ;;
bundle) [ ${#targets[@]} -eq 1 ] || die "bundle needs one output file name"; bundleCreate "${targets[0]}" ;;
list) cmdList ;;
doctor) cmdDoctor ;;
"") usage; exit 2 ;;
*) die "unknown command: $command (see: ./install.sh help)" ;;
esac