Skip to content

Instantly share code, notes, and snippets.

@R3tr0BoiDX
Last active September 10, 2026 20:47
Show Gist options
  • Select an option

  • Save R3tr0BoiDX/cd5895027eb21b3076a3c021ca7ad4bd to your computer and use it in GitHub Desktop.

Select an option

Save R3tr0BoiDX/cd5895027eb21b3076a3c021ca7ad4bd to your computer and use it in GitHub Desktop.
WiiCompiled build environment provision

WiiCompiled on the Steam Deck

Builds Mario Kart Wii into a native Linux binary via the WiiCompiled setup AppImage.

SteamOS cannot run that AppImage's build as-is, so this folder carries a small amount of scaffolding. wiicompiled-provision sets the scaffolding up; wiicompiled is a generated wrapper you use in place of the AppImage.

Contents

Path What it is
WiiCompiled-Setup-x86_64.appimage The upstream installer. Never run it directly.
MarioKartWii.iso Your PAL game disc image.
wiicompiled-provision Builds sysroot/ and generates wiicompiled. Run after any update.
wiicompiled Generated wrapper — use this for every AppImage command. Don't hand-edit; provisioning overwrites it.
sysroot/ Private host toolchain (headers + GCC runtime + libxml2.so.2). ~184 MB. Don't delete.

The folder is self-contained and relocatable — the scripts locate everything relative to themselves.

How to use

First time / after any update

  1. Download the script by clicking "Download ZIP" here in the upper right corner
  2. Unzip that ZIP archive wherever you want, e.g. on the Desktop. Make sure the wiicompiled-provision.sh stays within that folder, as it will create more files!
  3. Optional, but recommended: Rename the folder to Wiicompiled
  4. Also download the latest WiiCompiled-Setup-x86_64.AppImage and put it into that folder
  5. Optional, but recommended: Put your ethnically sourced PAL RMCP01 Mario Kart Wii ISO in the folder as well and name it MarioKartWii.iso
  6. Open a terminal in that folder. Search for Konsole in the start menu and then cd into that new folder like cd ~/Desktop/Wiicompiled
  7. Give the script rights to run with chmod +x wiicompiled-provision.sh
  8. Run the wiicompiled-provision.sh with ./wiicompiled-provision.sh
  9. The wiicompiled-provision.sh will download and build the build environment
  10. Run wiicompiled install with
  • --game pointing to your ISO file
  • --install-dir pointing to where the game should be stored (like "/home/deck/Games/Linux/Wiicompiled")
  • --force-clean-build to force a clean build
  • Full command for example: ./wiicompiled install --game "./MarioKartWii.iso" --install-dir "~/Games/Linux/Wiicompiled" --force-clean-build
  1. The game will build then. This will take a few minutes
  2. Enjoy!

Commands For example:

mkdir -p ~/Desktop/Wiicompiled
# Then move the wiicompiled-provision.sh and the WiiCompiled Appimage into the new folder
cd ~/Desktop/Wiicompiled
chmod +x wiicompiled-provision.sh
./wiicompiled-provision.sh
./wiicompiled install --game "./MarioKartWii.iso" --install-dir "~/Games/Linux/Wiicompiled" --force-clean-build

Takes roughly 7 minutes.

Then:

./wiicompiled launch-base       # play
./wiicompiled check-products    # what's installed, and whether it's current
./wiicompiled uninstall

When to re-run ./wiicompiled-provision

  • After a SteamOS update if you want to compile again — glibc or gcc-libs may have moved, and the sysroot has to be re-pinned to match. Your installed game keeps working either way; this only affects the next build.
  • After dropping in a new AppImage — re-points the wrapper (any filename works, it picks the newest *.appimage) and re-checks the new bundled toolchain for host dependencies. The sysroot itself is usually reused.
  • Any time a build fails with missing headers, crtbeginS.o, or -lgcc.

It's idempotent and safe to run with the game installed — it touches only sysroot/ and wiicompiled, never the installed game. With nothing changed it verifies and exits in seconds. Add --force to rebuild the sysroot regardless.

Why the scaffolding exists

SteamOS has an immutable /usr and ships no development environment, so the AppImage's bundled clang/lld toolchain is missing two things it expects from the host. Nothing can be installed into /usr to fix that, so sysroot/ stands in and the wrapper points the toolchain at it through the environment.

  1. libxml2.so.2 — the bundled ld.lld links against soname 2; SteamOS ships only libxml2.so.16. Without it: ld.lld: error while loading shared libraries: libxml2.so.2
  2. A host dev sysroot — SteamOS has no /usr/include at all and no GCC installation, so clang finds neither the libc headers nor crtbegin/libgcc. (It does keep the .so link symlinks and crt*.o in /usr/lib.) Without it: cannot open crtbeginS.o, unable to find library -lgcc, fatal error: 'stdio.h' file not found

Provisioning pins the sysroot to the exact glibc / linux-api-headers / gcc versions pacman reports as installed, pulled from the SteamOS mirror (falling back to the Arch archive), so the compile-time ABI matches the libc.so.6 and libstdc++.so.6 the built game links against at runtime. It then proves the result by compiling, linking and running a C and a C++ test program through the real toolchain before reporting success.

The built game is self-contained: the scaffolding is build-time only, and /home/deck/Games/Linux/Wiicompiled/WiiCompiled runs with no special environment.

Three details that are easy to get wrong

  • libstdc++, not libc++. The toolchain bundles libc++ and it's the tempting shortcut, but the AppImage's native-prebuilt/*.a are compiled against the libstdc++ C++ ABI — libc++ links and then breaks at the ABI boundary.
  • Flags go through CFLAGS/CXXFLAGS/ASMFLAGS, not --cc/--cxx. local-build.sh derives llvm-ar/llvm-ranlib from dirname $cc_bin, so pointing --cc at a wrapper outside the toolchain's bin/ silently loses them.
  • -idirafter, not -I/-isystem. It puts the libc headers at the very end of the search chain so libstdc++'s own <stdio.h> wrappers still win and their include_next resolves correctly.

Troubleshooting

The wrapper fails fast if the sysroot is incomplete and tells you to run provisioning. Provisioning fails loudly rather than half-working.

One case it can't fix on its own: if a future AppImage needs a host library SteamOS lacks other than libxml2, it will name the binary and the missing library and stop. Find a build of that library, drop it into sysroot/usr/lib, and re-run.

#!/bin/bash
# wiicompiled-provision -- (re)build the host toolchain sysroot the WiiCompiled
# AppImage needs in order to compile anything on SteamOS, then regenerate the
# `wiicompiled` wrapper that feeds it to the build.
#
# WHY THIS EXISTS
# SteamOS has an immutable /usr with no /usr/include at all and no GCC install,
# and its libxml2 is soname 16 while the AppImage's bundled ld.lld wants soname 2.
# Nothing can be installed into /usr to fix that, so a private sysroot stands in.
#
# WHEN TO RUN IT
# - After a SteamOS update (glibc / gcc-libs may have moved -> re-pin the sysroot).
# - After replacing the AppImage with a new version (re-points the wrapper and
# re-checks that the new bundled toolchain still compiles and links here).
# - Any time the build starts failing with missing headers / crtbeginS.o / -lgcc.
# It is idempotent and cheap: with nothing changed it verifies and exits in seconds.
#
# It is safe to run with the game already installed; it touches only the sysroot
# and the wrapper, never the installed game.
set -euo pipefail
SELF_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
PROJECT_DIR="$SELF_DIR"
SYSROOT="$PROJECT_DIR/sysroot"
WRAPPER="$PROJECT_DIR/wiicompiled"
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/wiicompiled-provision"
ARCH=x86_64
TRIPLE=x86_64-pc-linux-gnu
# Last libxml2 release carrying soname 2 (2.14 bumped it to 16). The sysroot is
# rebuilt wholesale, so this exact build is what always ends up in it.
LIBXML2_VER=2.13.8-1
FORCE=0
for arg in "$@"; do
case "$arg" in
-f|--force) FORCE=1 ;;
-h|--help)
sed -n '2,19s/^# \?//p' "$0"
echo
echo "Usage: $(basename "$0") [--force]"
echo " --force rebuild the sysroot even if package versions are unchanged"
exit 0 ;;
*) echo "unknown argument: $arg (try --help)" >&2; exit 2 ;;
esac
done
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
info() { printf ' %s\n' "$*"; }
die() { printf '\033[1;31m==> error:\033[0m %s\n' "$*" >&2; exit 1; }
work=""; tdir=""; mnt_out=""; mnt_pid=""; MNT=""
cleanup() {
[ -n "$mnt_pid" ] && kill "$mnt_pid" 2>/dev/null || :
[ -n "$MNT" ] && { fusermount -u "$MNT" 2>/dev/null || :; rmdir "$MNT" 2>/dev/null || :; }
rm -rf "$work" "$tdir" "$mnt_out"
}
trap cleanup EXIT
# ---------------------------------------------------------------- locate AppImage
APPIMAGE="${WIICOMPILED_APPIMAGE:-}"
if [ -z "$APPIMAGE" ]; then
# Newest by mtime, so dropping in a new build just works without renaming.
APPIMAGE=$(find "$PROJECT_DIR" -maxdepth 1 -type f \
\( -iname '*.appimage' \) -printf '%T@ %p\n' 2>/dev/null |
sort -rn | head -1 | cut -d' ' -f2-)
fi
[ -n "$APPIMAGE" ] && [ -f "$APPIMAGE" ] || die "no AppImage found in $PROJECT_DIR (set WIICOMPILED_APPIMAGE)"
[ -x "$APPIMAGE" ] || { chmod +x "$APPIMAGE" && info "made $(basename "$APPIMAGE") executable"; }
say "AppImage: $APPIMAGE"
info "version: $("$APPIMAGE" --version 2>/dev/null || echo '(could not query)')"
# ------------------------------------------------------- resolve package versions
command -v pacman >/dev/null || die "pacman not found; cannot determine host package versions"
pkgver() { pacman -Q "$1" 2>/dev/null | awk '{print $2}'; }
GLIBC_VER=$(pkgver glibc)
LAH_VER=$(pkgver linux-api-headers)
GCC_VER=$(pkgver gcc-libs) # the `gcc` package shares gcc-libs' version string
[ -n "$GLIBC_VER" ] && [ -n "$LAH_VER" ] && [ -n "$GCC_VER" ] ||
die "could not read glibc / linux-api-headers / gcc-libs versions from pacman"
say "Host packages to match"
info "glibc $GLIBC_VER"
info "linux-api-headers $LAH_VER"
info "gcc / gcc-libs $GCC_VER"
STAMP="$SYSROOT/.provision-stamp"
WANT_STAMP="glibc=$GLIBC_VER
linux-api-headers=$LAH_VER
gcc=$GCC_VER"
# --------------------------------------------------------------- mirror discovery
# Prefer the SteamOS mirror: it is the authoritative source for the exact versions
# SteamOS actually installs, which the Arch archive does not always carry.
CORE_REPO=$(grep -oE '^\[core[^]]*\]' /etc/pacman.conf 2>/dev/null | head -1 | tr -d '[]')
MIRROR=$(grep -m1 -E '^[[:space:]]*Server[[:space:]]*=' /etc/pacman.d/mirrorlist 2>/dev/null |
sed 's/.*=[[:space:]]*//')
STEAMOS_BASE=""
if [ -n "$CORE_REPO" ] && [ -n "$MIRROR" ]; then
STEAMOS_BASE=${MIRROR//\$repo/$CORE_REPO}
STEAMOS_BASE=${STEAMOS_BASE//\$arch/$ARCH}
info "mirror: $STEAMOS_BASE"
fi
# fetch <pkgname> <version> -> echoes local path to the downloaded package
fetch_pkg() {
local name=$1 ver=$2
local file="$name-$ver-$ARCH.pkg.tar.zst"
local out="$CACHE/$file"
[ -s "$out" ] && { printf '%s' "$out"; return 0; }
mkdir -p "$CACHE"
local enc=${file//+/%2B}
local urls=()
[ -n "$STEAMOS_BASE" ] && urls+=("$STEAMOS_BASE/$enc")
urls+=("https://archive.archlinux.org/packages/${name:0:1}/$name/$enc")
local u
for u in "${urls[@]}"; do
if curl -fsSL --retry 3 --retry-delay 2 --remove-on-error -o "$out" "$u" 2>/dev/null; then
printf '%s' "$out"; return 0
fi
done
die "could not download $file from any mirror. Tried:$(printf '\n %s' "${urls[@]}")"
}
# unpack <pkgfile> <destdir> [paths...] (bsdtar ships with libarchive, pacman's own dep)
unpack() {
local pkg=$1 dest=$2; shift 2
mkdir -p "$dest"
bsdtar -xf "$pkg" -C "$dest" "$@"
}
# ------------------------------------------------------------------ sysroot build
needs_build=1
if [ "$FORCE" -eq 0 ] && [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$WANT_STAMP" ] \
&& [ -e "$SYSROOT/usr/include/stdio.h" ] && [ -e "$SYSROOT/usr/lib/libxml2.so.2" ]; then
needs_build=0
fi
if [ "$needs_build" -eq 0 ]; then
say "Sysroot already matches the installed packages - skipping rebuild (--force to override)"
else
say "Building sysroot at $SYSROOT"
work=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-sysroot.XXXXXX")
stage="$work/stage"; new="$work/sysroot"
mkdir -p "$stage" "$new/usr/lib"
info "fetching glibc $GLIBC_VER"; p=$(fetch_pkg glibc "$GLIBC_VER"); unpack "$p" "$stage" usr/include
info "fetching linux-api-headers $LAH_VER"; p=$(fetch_pkg linux-api-headers "$LAH_VER"); unpack "$p" "$stage" usr/include
info "fetching gcc $GCC_VER"; p=$(fetch_pkg gcc "$GCC_VER"); unpack "$p" "$stage" usr/include/c++ usr/lib/gcc
cp -a "$stage/usr/include" "$new/usr/include"
cp -a "$stage/usr/lib/gcc" "$new/usr/lib/gcc"
# Read the GCC directory version back off disk rather than assuming it equals the
# package version -- Arch's pkgver carries an extra +rN+g<sha> suffix that the
# on-disk tree does not use.
gcc_dir=$(find "$new/usr/lib/gcc/$TRIPLE" -maxdepth 1 -mindepth 1 -type d | head -1)
[ -n "$gcc_dir" ] || die "gcc package did not contain usr/lib/gcc/$TRIPLE/<version>"
GCC_DIRVER=$(basename "$gcc_dir")
[ -e "$gcc_dir/crtbeginS.o" ] || die "gcc package is missing crtbeginS.o"
[ -d "$new/usr/include/c++/$GCC_DIRVER" ] || die "gcc package is missing include/c++/$GCC_DIRVER"
info "gcc install dir version: $GCC_DIRVER"
info "fetching libxml2 $LIBXML2_VER"
p=$(fetch_pkg libxml2 "$LIBXML2_VER")
unpack "$p" "$stage" usr/lib
real=$(find "$stage/usr/lib" -maxdepth 1 -name 'libxml2.so.2*' -type f | head -1)
[ -n "$real" ] || die "libxml2 $LIBXML2_VER did not contain a libxml2.so.2"
cp "$real" "$new/usr/lib/libxml2.so.2"
chmod 0755 "$new/usr/lib/libxml2.so.2"
printf '%s\n' "$WANT_STAMP" > "$new/.provision-stamp"
rm -rf "$SYSROOT"
mv "$new" "$SYSROOT" || die "failed to install new sysroot (re-run to rebuild)"
info "sysroot installed ($(du -sh "$SYSROOT" | cut -f1))"
fi
GCC_DIRVER=$(basename "$(find "$SYSROOT/usr/lib/gcc/$TRIPLE" -maxdepth 1 -mindepth 1 -type d | head -1)")
GCC_INSTALL_DIR="$SYSROOT/usr/lib/gcc/$TRIPLE/$GCC_DIRVER"
# ----------------------------------------------------------------- write wrapper
say "Writing wrapper: $WRAPPER"
cat > "$WRAPPER" <<WRAPPER_EOF
#!/bin/bash
# GENERATED by wiicompiled-provision -- re-run that to regenerate; edits here are lost.
#
# SteamOS is missing two things the AppImage's bundled clang/lld toolchain expects
# from the host, and neither can be installed into the read-only /usr. Both are
# supplied out of \$SYSROOT, which mirrors a normal host layout. AppRun passes the
# environment straight down to wiicompiled-setup -> local-build.sh -> cmake ->
# clang -> ld.lld, so exporting it here is enough.
#
# 1. libxml2.so.2 -- the bundled ld.lld links against soname 2, SteamOS ships only
# libxml2.so.16. Without it: "ld.lld: error while loading shared libraries".
# 2. A host dev sysroot -- SteamOS has no /usr/include and no GCC, so clang finds
# neither libc headers nor crtbegin/libgcc. Without it: "cannot open
# crtbeginS.o", "unable to find library -lgcc", "'stdio.h' file not found".
#
# libstdc++ (not the toolchain's bundled libc++) is required: the AppImage's
# native-prebuilt/*.a static libraries are built against the libstdc++ C++ ABI.
#
# Flags ride in via CFLAGS/CXXFLAGS/ASMFLAGS, which CMake folds into
# CMAKE_{C,CXX,ASM}_FLAGS on a fresh configure. That is deliberate: local-build.sh
# derives llvm-ar/llvm-ranlib from dirname of --cc, so passing a --cc wrapper script
# outside the toolchain's bin directory would silently lose them.
set -euo pipefail
# Self-locating: keeps this folder relocatable as a unit.
SELF_DIR=\$(cd "\$(dirname "\$(readlink -f "\$0")")" && pwd)
APPIMAGE="\${WIICOMPILED_APPIMAGE:-\$SELF_DIR/$(basename "$APPIMAGE")}"
SYSROOT="\${WIICOMPILED_SYSROOT:-\$SELF_DIR/sysroot}"
SYSROOT_LIB="\$SYSROOT/usr/lib"
GCC_INSTALL_DIR="\$SYSROOT_LIB/gcc/$TRIPLE/$GCC_DIRVER"
die() { echo "wiicompiled: \$*" >&2; echo "wiicompiled: try running \$SELF_DIR/wiicompiled-provision" >&2; exit 1; }
[ -x "\$APPIMAGE" ] || die "AppImage not found or not executable: \$APPIMAGE"
[ -e "\$SYSROOT_LIB/libxml2.so.2" ] || die "missing \$SYSROOT_LIB/libxml2.so.2"
[ -e "\$SYSROOT/usr/include/stdio.h" ] || die "missing libc headers in \$SYSROOT/usr/include"
[ -e "\$GCC_INSTALL_DIR/crtbeginS.o" ] || die "missing GCC runtime in \$GCC_INSTALL_DIR"
# Only libxml2.so.2 is a bare runtime .so in there (the rest is the gcc/ tree), so
# this exposes exactly one library to the loader and nothing else.
export LD_LIBRARY_PATH="\$SYSROOT_LIB\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}"
# -idirafter (not -I/-isystem) puts the libc headers at the very END of the search
# chain, so libstdc++'s own <stdio.h> etc. wrappers still win and their include_next
# resolves correctly.
toolchain_flags="--gcc-install-dir=\$GCC_INSTALL_DIR -idirafter \$SYSROOT/usr/include"
export CFLAGS="\$toolchain_flags\${CFLAGS:+ \$CFLAGS}"
export CXXFLAGS="\$toolchain_flags\${CXXFLAGS:+ \$CXXFLAGS}"
export ASMFLAGS="\$toolchain_flags\${ASMFLAGS:+ \$ASMFLAGS}"
exec "\$APPIMAGE" "\$@"
WRAPPER_EOF
chmod +x "$WRAPPER"
bash -n "$WRAPPER" || die "generated wrapper is not valid bash"
# ---------------------------------------------------------------------- verify
# The toolchain must actually compile and link, for both C and C++.
say "Verifying against the bundled toolchain"
mnt_out=$(mktemp)
"$APPIMAGE" --appimage-mount > "$mnt_out" 2>&1 &
mnt_pid=$!
for _ in $(seq 1 50); do
MNT=$(cat "$mnt_out" 2>/dev/null); [ -n "$MNT" ] && [ -d "$MNT" ] && break
sleep 0.2
done
[ -n "$MNT" ] && [ -d "$MNT" ] || die "could not mount the AppImage to verify"
export LD_LIBRARY_PATH="$SYSROOT/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
FLAGS=(--gcc-install-dir="$GCC_INSTALL_DIR" -idirafter "$SYSROOT/usr/include")
tdir=$(mktemp -d)
printf '#include <stdio.h>\nint main(void){printf("ok\\n");return 0;}\n' > "$tdir/t.c"
printf '#include <cstdio>\n#include <string>\n#include <vector>\n#include <thread>\nint main(){std::vector<std::string> v{"ok"};std::thread t([&]{v.push_back("x");});t.join();std::printf("%%s\\n",v[0].c_str());return 0;}\n' > "$tdir/t.cpp"
"$MNT/usr/toolchain/bin/clang" "${FLAGS[@]}" -fuse-ld=lld "$tdir/t.c" -o "$tdir/tc" || die "C compile/link failed"
[ "$("$tdir/tc")" = ok ] || die "C test binary did not run"
info "C compile + link + run OK"
"$MNT/usr/toolchain/bin/clang++" "${FLAGS[@]}" -fuse-ld=lld -std=c++20 "$tdir/t.cpp" -o "$tdir/tcpp" || die "C++ compile/link failed"
[ "$("$tdir/tcpp")" = ok ] || die "C++ test binary did not run"
info "C++ compile + link + run OK"
say "Ready."
echo
echo " Rebuild the game with for example:"
echo " $WRAPPER install --game \"$PROJECT_DIR/<game>.iso\" \\"
echo " --install-dir \"/home/deck/Games/Linux/Wiicompiled\" --force-clean-build"
echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment