|
#!/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 |