Skip to content

Instantly share code, notes, and snippets.

@JoeMatt
Created August 26, 2026 23:04
Show Gist options
  • Select an option

  • Save JoeMatt/0684352196fd668206b994935556feec to your computer and use it in GitHub Desktop.

Select an option

Save JoeMatt/0684352196fd668206b994935556feec to your computer and use it in GitHub Desktop.
Virtual Jaguar core installer for RaspberryPi Emulation Station aka RetroPie
#!/usr/bin/env bash
#
# Install / update the Virtual Jaguar libretro core on RetroPie (Raspberry Pi).
#
# https://github.com/libretro/virtualjaguar-libretro
#
# Usage:
# chmod +x install-vj-retropie.sh
# ./install-vj-retropie.sh # install the latest release
# ./install-vj-retropie.sh --tag v3.5.1 # install a specific release
# ./install-vj-retropie.sh --dry-run # show what it would do, change nothing
#
# What it does:
# 1. Works out whether your RetroArch is 32-bit or 64-bit -- by reading the
# RetroArch binary itself, NOT `uname -m`. This is the step everyone gets
# wrong, see the note below.
# 2. Works out which Pi you are on.
# 3. Downloads the matching .so from the GitHub release.
# 4. Backs up your current core, installs the new one, and verifies it.
#
# Nothing is deleted: the old core is renamed with a .bak-<timestamp> suffix,
# and the script prints the exact command to undo the whole thing.
#
# ---------------------------------------------------------------------------
# WHY NOT `uname -m`
#
# Plenty of Pi setups run a 64-bit KERNEL with a 32-bit USERLAND -- Raspberry Pi
# OS did this by default for years. On those, `uname -m` says `aarch64` while
# every binary on the system is 32-bit `armhf`. Follow uname and you install a
# 64-bit core into a 32-bit RetroArch, which fails with a bare
# "Failed to open libretro core" and no hint as to why.
#
# The core must match RETROARCH, not the kernel. So this script reads the
# actual ELF class out of the RetroArch binary and uses that.
# ---------------------------------------------------------------------------
set -euo pipefail
REPO="libretro/virtualjaguar-libretro"
CORE_NAME="virtualjaguar_libretro"
TAG=""
DRY_RUN=0
RED=$'\033[31m'; GRN=$'\033[32m'; YLW=$'\033[33m'; BLD=$'\033[1m'; RST=$'\033[0m'
say() { printf '%s\n' "$*"; }
info() { printf '%s==>%s %s\n' "$BLD" "$RST" "$*"; }
ok() { printf '%s ok%s %s\n' "$GRN" "$RST" "$*"; }
warn() { printf '%s warn%s %s\n' "$YLW" "$RST" "$*"; }
die() { printf '%s FAIL%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
--tag) TAG="${2:-}"; [ -n "$TAG" ] || die "--tag needs a value, e.g. --tag v3.5.1"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) sed -n '3,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown option: $1 (try --help)" ;;
esac
done
command -v curl >/dev/null 2>&1 || die "curl is not installed. sudo apt install -y curl"
# ---------------------------------------------------------------------------
# 1. Find RetroArch and read its architecture
# ---------------------------------------------------------------------------
info "Looking for RetroArch"
RETROARCH=""
for c in /opt/retropie/emulators/retroarch/bin/retroarch \
/usr/bin/retroarch /usr/local/bin/retroarch; do
[ -x "$c" ] && { RETROARCH="$c"; break; }
done
[ -n "$RETROARCH" ] || RETROARCH="$(command -v retroarch 2>/dev/null || true)"
[ -n "$RETROARCH" ] || die "could not find a retroarch binary. Is RetroPie installed?"
ok "RetroArch: $RETROARCH"
# ELF class byte 5: 1 = 32-bit, 2 = 64-bit. `od` is in coreutils, always present;
# `file` is not installed on a stock RetroPie image.
elf_class=$(od -An -t u1 -j 4 -N 1 "$RETROARCH" | tr -d ' ')
case "$elf_class" in
1) RA_BITS=32 ;;
2) RA_BITS=64 ;;
*) die "could not read the ELF class of $RETROARCH (got '$elf_class')" ;;
esac
ok "RetroArch is ${RA_BITS}-bit"
kernel_arch=$(uname -m)
if { [ "$RA_BITS" = 32 ] && [ "$kernel_arch" = "aarch64" ]; }; then
warn "kernel reports '$kernel_arch' but RetroArch is 32-bit --"
warn "64-bit kernel with a 32-bit userland. Using the 32-bit core, which is correct."
warn "(This is the mismatch that makes hand-picked cores fail to load.)"
fi
# ---------------------------------------------------------------------------
# 2. Work out which Pi this is
# ---------------------------------------------------------------------------
info "Detecting the board"
model=""
[ -r /proc/device-tree/model ] && model=$(tr -d '\0' < /proc/device-tree/model)
[ -n "$model" ] || model=$(sed -n 's/^Model[[:space:]]*:[[:space:]]*//p' /proc/cpuinfo 2>/dev/null | head -1)
[ -n "$model" ] || model="unknown"
ok "Board: $model"
case "$model" in
*"Raspberry Pi 5"*) PI=rpi5; SUF=cortex-a76 ;;
*"Raspberry Pi 4"*|*"Pi 400"*) PI=rpi4; SUF=cortex-a72 ;;
*"Compute Module 4"*) PI=rpi4; SUF=cortex-a72 ;;
*"Raspberry Pi 3"*|*"Pi Zero 2"*) PI=rpi3; SUF=cortex-a53 ;;
*"Compute Module 3"*) PI=rpi3; SUF=cortex-a53 ;;
*"Raspberry Pi 2"*) PI=rpi2; SUF=cortex-a7 ;;
*"Raspberry Pi Model"*|*"Pi Zero"*) PI=rpi1; SUF=armv6 ;;
*)
warn "unrecognised board '$model'"
if [ "$RA_BITS" = 64 ]; then
warn "falling back to the generic aarch64 build"
ASSET="${CORE_NAME}-linux-aarch64.so"
else
die "no safe 32-bit fallback for an unknown board. Please open an issue at
https://github.com/$REPO/issues and include this line:
model=$model uname=$kernel_arch retroarch=${RA_BITS}-bit"
fi
;;
esac
if [ -z "${ASSET:-}" ]; then
# rpi1 and rpi2 are 32-bit-only parts; there is no _64 build for them.
if [ "$RA_BITS" = 64 ]; then
case "$PI" in
rpi1|rpi2) die "$model cannot run a 64-bit userland; this looks wrong.
Report it at https://github.com/$REPO/issues" ;;
*) ASSET="${CORE_NAME}-linux-${PI}_64-${SUF}.so" ;;
esac
else
ASSET="${CORE_NAME}-linux-${PI}-${SUF}.so"
fi
fi
ok "Core to install: $ASSET"
# ---------------------------------------------------------------------------
# 3. Where the core goes
# ---------------------------------------------------------------------------
CORE_DIR="/opt/retropie/libretrocores/lr-virtualjaguar"
CORE_PATH="$CORE_DIR/${CORE_NAME}.so"
# If an lr-virtualjaguar entry already exists in emulators.cfg, trust the path it
# names over the convention -- some setups relocate cores.
EMUCFG="/opt/retropie/configs/atarijaguar/emulators.cfg"
if [ -r "$EMUCFG" ]; then
existing=$(sed -n 's/.*-L[[:space:]]\{1,\}\([^[:space:]]*virtualjaguar[^[:space:]]*\.so\).*/\1/p' "$EMUCFG" | head -1)
if [ -n "$existing" ]; then
CORE_PATH="$existing"
CORE_DIR=$(dirname "$CORE_PATH")
ok "Found an existing entry in emulators.cfg, will update: $CORE_PATH"
fi
fi
if [ -f "$CORE_PATH" ]; then
info "Existing core found, it will be backed up"
else
info "No existing core at $CORE_PATH -- this will be a fresh install"
warn "If Virtual Jaguar has never been installed, run RetroPie-Setup once first:"
warn " sudo ~/RetroPie-Setup/retropie_setup.sh"
warn " Manage packages -> experimental -> lr-virtualjaguar -> Install"
warn "That creates the folder, the .info file and the emulators.cfg entry."
warn "Then re-run this script to swap in the newer build."
fi
# ---------------------------------------------------------------------------
# 4. Download
# ---------------------------------------------------------------------------
if [ -n "$TAG" ]; then
URL="https://github.com/$REPO/releases/download/$TAG/$ASSET"
else
URL="https://github.com/$REPO/releases/latest/download/$ASSET"
fi
info "Downloading"
say " $URL"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
if ! curl -fsSL --retry 3 -o "$TMP/core.so" "$URL"; then
die "download failed.
Most likely this release has no build named '$ASSET'.
Check what that release actually shipped:
https://github.com/$REPO/releases"
fi
size=$(wc -c < "$TMP/core.so")
[ "$size" -gt 100000 ] || die "downloaded file is only $size bytes -- that is not a core."
ok "Downloaded $(( size / 1024 )) KB"
# ---------------------------------------------------------------------------
# 5. Verify BEFORE installing -- catch a wrong-arch file while it is still
# harmless, rather than after it has replaced a working core.
# ---------------------------------------------------------------------------
info "Checking the download matches your RetroArch"
dl_class=$(od -An -t u1 -j 4 -N 1 "$TMP/core.so" | tr -d ' ')
case "$dl_class" in 1) DL_BITS=32 ;; 2) DL_BITS=64 ;; *) die "downloaded file is not an ELF shared object" ;; esac
[ "$DL_BITS" = "$RA_BITS" ] || die "arch mismatch -- downloaded a ${DL_BITS}-bit core but RetroArch is ${RA_BITS}-bit.
Nothing was changed. Please report this at https://github.com/$REPO/issues
and include: model=$model uname=$kernel_arch asset=$ASSET"
ok "Both ${RA_BITS}-bit -- match"
if [ "$DRY_RUN" = 1 ]; then
say ""
info "--dry-run: stopping here. Nothing was changed."
say " would install: $ASSET"
say " to: $CORE_PATH"
exit 0
fi
# ---------------------------------------------------------------------------
# 6. Install
# ---------------------------------------------------------------------------
info "Installing"
SUDO=""
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
$SUDO mkdir -p "$CORE_DIR"
BACKUP=""
if [ -f "$CORE_PATH" ]; then
BACKUP="$CORE_PATH.bak-$(date +%Y%m%d-%H%M%S)"
$SUDO cp -p "$CORE_PATH" "$BACKUP"
ok "Backed up old core -> $BACKUP"
fi
$SUDO install -m 0644 "$TMP/core.so" "$CORE_PATH"
ok "Installed -> $CORE_PATH"
# ---------------------------------------------------------------------------
# 7. Confirm RetroArch can actually load it. A core that installs but does not
# load is the failure mode worth catching here, not at the next game launch.
# ---------------------------------------------------------------------------
info "Asking RetroArch to load the core"
if out=$("$RETROARCH" -L "$CORE_PATH" --version 2>&1); then
ok "RetroArch loaded it"
printf '%s\n' "$out" | sed -n '1,3p' | sed 's/^/ /'
else
warn "RetroArch could not load the core (this check is not always conclusive"
warn "on a headless SSH session -- it may just be the lack of a display)."
printf '%s\n' "$out" | sed -n '1,6p' | sed 's/^/ /'
if [ -n "$BACKUP" ]; then
say ""
warn "To undo:"
say " sudo cp -p '$BACKUP' '$CORE_PATH'"
fi
fi
say ""
info "${BLD}Done.${RST}"
say " Core: $CORE_PATH"
say " Board: $model"
say " Arch: ${RA_BITS}-bit"
[ -n "$BACKUP" ] && say " Undo with: sudo cp -p '$BACKUP' '$CORE_PATH'"
say ""
say " Put Jaguar ROMs in ~/RetroPie/roms/atarijaguar, then restart EmulationStation"
say " (Start -> Quit -> Restart EmulationStation) so it rescans."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment