mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
download.py authenticated its release lookup and download.sh did not, so past the anonymous quota the shell twin failed with 403. The header goes to api.github.com only and is read from stdin, out of the process list.
245 lines
7.0 KiB
Bash
Executable File
245 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Download BIOS pack from GitHub Releases (Linux/macOS one-liner compatible)
|
|
#
|
|
# A pack over 2 GB is published in several parts. Each is a ZIP
|
|
# (.part1of2.zip), checked and extracted in turn. Releases up to v2026.09.04
|
|
# carry byte ranges of one archive instead (.zip.001), which are joined first.
|
|
#
|
|
# Usage:
|
|
# bash scripts/download.sh retroarch ~/RetroArch/system/
|
|
# bash scripts/download.sh --list
|
|
#
|
|
# Requires: curl, unzip
|
|
|
|
set -euo pipefail
|
|
|
|
REPO="Abdess/retrobios"
|
|
API_BASE="${RETROBIOS_API:-https://api.github.com}"
|
|
|
|
# This endpoint names the assets and so the bytes that land in the BIOS
|
|
# directory. Plain HTTP stays allowed to loopback, which is how the script
|
|
# is exercised end to end.
|
|
case "$API_BASE" in
|
|
https://*) ;;
|
|
http://127.0.0.1[:/]*|http://localhost[:/]*|"http://[::1]"[:/]*) ;;
|
|
*)
|
|
echo "Error: RETROBIOS_API must use HTTPS, got '$API_BASE'" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
API="${API_BASE%/}/repos/${REPO}/releases/latest"
|
|
|
|
usage() {
|
|
echo "Usage: $0 <platform> <destination>"
|
|
echo " $0 --list"
|
|
echo ""
|
|
echo "Download BIOS packs from GitHub Releases."
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 retroarch ~/RetroArch/system/"
|
|
echo " $0 batocera /userdata/bios/"
|
|
echo " $0 --list"
|
|
exit 1
|
|
}
|
|
|
|
# One asset URL per line, in the order the release lists them.
|
|
asset_urls() {
|
|
printf '%s' "$1" | tr ',' '\n' |
|
|
sed -n 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
|
|
}
|
|
|
|
# The archive name behind each asset, the files of one pack folded together:
|
|
# Pack.part1of2.zip and Pack.zip.001 both belong to Pack.zip.
|
|
pack_names() {
|
|
asset_urls "$1" | sed 's#.*/##' |
|
|
sed 's/\(_BIOS_Pack\)\.part[0-9][0-9]*of[0-9][0-9]*\.zip$/\1.zip/' |
|
|
sed -n 's/\(.*_BIOS_Pack\.zip\)\(\.[0-9][0-9]*\)\{0,1\}$/\1/p' |
|
|
LC_ALL=C sort -u
|
|
}
|
|
|
|
platform_names() {
|
|
pack_names "$1" | sed 's/_BIOS_Pack\.zip$//' | tr '_' ' '
|
|
}
|
|
|
|
# Letters and digits only: the platform is `misterfpga`, the asset MiSTer_FPGA.
|
|
normalize() {
|
|
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9'
|
|
}
|
|
|
|
# The anonymous quota is 60 calls an hour per address. The token goes to
|
|
# GitHub's API only: RETROBIOS_API can name any host. The header is read
|
|
# from stdin so the token stays out of the process list.
|
|
fetch_release() {
|
|
case "$API_BASE" in
|
|
https://api.github.com|https://api.github.com/)
|
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
|
printf 'Authorization: Bearer %s\n' "$GITHUB_TOKEN" |
|
|
curl -fsSL -H @- "$API"
|
|
return
|
|
fi
|
|
;;
|
|
esac
|
|
curl -fsSL "$API"
|
|
}
|
|
|
|
list_platforms() {
|
|
echo "Fetching available platforms..."
|
|
platform_names "$(fetch_release)"
|
|
}
|
|
|
|
download_pack() {
|
|
local platform="$1"
|
|
local dest="$2"
|
|
local normalized
|
|
normalized=$(normalize "$platform")
|
|
|
|
echo "Fetching release info..."
|
|
local release_json
|
|
release_json=$(fetch_release)
|
|
|
|
local archive=""
|
|
local candidate
|
|
while read -r candidate; do
|
|
[ -n "$candidate" ] || continue
|
|
case "$(normalize "$candidate")" in
|
|
*"$normalized"*)
|
|
archive="$candidate"
|
|
break
|
|
;;
|
|
esac
|
|
done <<EOF
|
|
$(pack_names "$release_json")
|
|
EOF
|
|
|
|
if [ -z "$archive" ]; then
|
|
echo "Error: Platform '$platform' not found in latest release."
|
|
echo "Available platforms:"
|
|
platform_names "$release_json"
|
|
exit 1
|
|
fi
|
|
|
|
local stem="${archive%.zip}"
|
|
local volumes
|
|
volumes=$(asset_urls "$release_json" |
|
|
grep -E "/${stem//./\\.}(\.zip(\.[0-9]+)?|\.part[0-9]+of[0-9]+\.zip)$" |
|
|
LC_ALL=C sort)
|
|
if [ -z "$volumes" ]; then
|
|
echo "Error: no asset found for ${archive}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
local count
|
|
count=$(printf '%s\n' "$volumes" | wc -l | tr -d ' ')
|
|
|
|
# Parts that are each a ZIP say in their name how many there are.
|
|
local declared
|
|
declared=$(printf '%s\n' "$volumes" |
|
|
sed -n 's/.*\.part[0-9][0-9]*of\([0-9][0-9]*\)\.zip$/\1/p' | head -1)
|
|
if [ -n "$declared" ] && [ "$declared" != "$count" ]; then
|
|
echo "Error: the release lists ${count} of ${declared} parts of ${archive}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Staged inside the destination: a pack is gigabytes, and /tmp is a RAM
|
|
# disk on the appliances these packs target.
|
|
mkdir -p "$dest"
|
|
local staging="${dest%/}/.retrobios-download"
|
|
rm -rf "$staging"
|
|
mkdir -p "$staging"
|
|
trap 'rm -rf "$staging"' EXIT
|
|
|
|
local index=0
|
|
local url name
|
|
while read -r url; do
|
|
[ -n "$url" ] || continue
|
|
index=$((index + 1))
|
|
name=$(basename "$url")
|
|
if [ "$count" -gt 1 ]; then
|
|
echo "Downloading ${name} (part ${index}/${count})..."
|
|
else
|
|
echo "Downloading ${name}..."
|
|
fi
|
|
curl -fL --progress-bar -o "${staging}/${name}" "$url"
|
|
done <<EOF
|
|
$volumes
|
|
EOF
|
|
|
|
if [ -z "$declared" ] && [ "$count" -gt 1 ]; then
|
|
echo "Joining ${count} parts into ${archive}..."
|
|
# Releases up to v2026.09.04: split(1) wrote plain byte ranges, so
|
|
# concatenation rebuilds the ZIP.
|
|
cat "${staging}/${archive}".[0-9][0-9][0-9] > "${staging}/${archive}"
|
|
rm -f "${staging}/${archive}".[0-9][0-9][0-9]
|
|
fi
|
|
|
|
local file
|
|
for file in "${staging}"/*.zip; do
|
|
verify_checksum "$release_json" "$staging" "$(basename "$file")"
|
|
done
|
|
|
|
echo "Extracting to ${dest}/..."
|
|
for file in "${staging}"/*.zip; do
|
|
unzip -o -q "$file" -d "$dest"
|
|
done
|
|
|
|
rm -rf "$staging"
|
|
trap - EXIT
|
|
echo "Done! BIOS files extracted to ${dest}/"
|
|
}
|
|
|
|
verify_checksum() {
|
|
local release_json="$1" staging="$2" archive="$3"
|
|
|
|
local sums_url
|
|
sums_url=$(asset_urls "$release_json" | grep -E '/SHA256SUMS\.txt$' | head -1)
|
|
if [ -z "$sums_url" ]; then
|
|
echo "No SHA256SUMS.txt in the release, skipping the checksum."
|
|
return 0
|
|
fi
|
|
|
|
local hasher
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
hasher="sha256sum"
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
hasher="shasum -a 256"
|
|
else
|
|
echo "No sha256sum available, skipping the checksum."
|
|
return 0
|
|
fi
|
|
|
|
local expected
|
|
expected=$(curl -fsSL "$sums_url" | awk -v name="$archive" '$2 == name {print $1}')
|
|
if [ -z "$expected" ]; then
|
|
echo "No checksum published for ${archive}, skipping."
|
|
return 0
|
|
fi
|
|
|
|
echo "Checking ${archive}..."
|
|
local actual
|
|
actual=$($hasher "${staging}/${archive}" | cut -d' ' -f1)
|
|
if [ "$actual" != "$expected" ]; then
|
|
echo "Error: checksum mismatch for ${archive}" >&2
|
|
echo " expected ${expected}" >&2
|
|
echo " got ${actual}" >&2
|
|
echo "Download the parts again: a truncated part gives this." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Main
|
|
case "${1:-}" in
|
|
--list|-l)
|
|
list_platforms
|
|
;;
|
|
--help|-h|"")
|
|
usage
|
|
;;
|
|
*)
|
|
if [[ -z "${2:-}" ]]; then
|
|
echo "Error: Destination directory required."
|
|
usage
|
|
fi
|
|
download_pack "$1" "$2"
|
|
;;
|
|
esac
|