Created
August 26, 2026 23:04
-
-
Save JoeMatt/0684352196fd668206b994935556feec to your computer and use it in GitHub Desktop.
Virtual Jaguar core installer for RaspberryPi Emulation Station aka RetroPie
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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