Skip to content

Build & Release

Build & Release #310

Workflow file for this run

name: Build & Release
on:
push:
tags:
- 'v*'
schedule:
- cron: '0 4 * * *' # Daily at 4 AM UTC
workflow_dispatch:
inputs:
release_type:
description: 'Release type'
required: true
default: 'nightly'
type: choice
options:
- nightly
- release
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
# Cleanup nightly release if this is a scheduled/dispatch nightly build
cleanup-nightly:
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'nightly')
steps:
- name: Delete existing nightly release
run: |
gh release delete nightly --yes --repo ${{ github.repository }} || true
gh api -X DELETE repos/${{ github.repository }}/git/refs/tags/nightly || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
# 2026-07-03: moved off the shared bare-metal `splitux` fleet onto a
# dedicated ephemeral, per-repo container runner (Phase 2 of the
# org-wide containerization pilot - see ~/ssh-workspace/ci-runners/README.md
# on the lab). gamescope-splitux and gbe_fork-splitux's Linux jobs keep
# using the shared `splitux` label, completely unaffected.
runs-on: [self-hosted, Linux, X64, splitux-ci]
needs: [cleanup-nightly]
if: always() && (needs.cleanup-nightly.result == 'success' || needs.cleanup-nightly.result == 'skipped')
steps:
- name: Verify dependencies
run: |
# Self-hosted runner has no sudo; deps are pre-installed on the host.
# Fail fast with a clear message if anything is missing.
pacman -Q \
base-devel pkgconf \
sdl2-compat \
7zip unzip zip curl wget \
libx11 openssl \
bubblewrap slirp4netns \
fuse2 file patchelf
- name: Install Rust
run: |
if ! command -v rustup &> /dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
fi
# Idempotent: ensures a default exists even when rustup was pre-installed empty
rustup default stable
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: splitux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: splitux-cargo-
- name: Cache external deps
id: deps-cache
uses: actions/cache@v4
with:
path: /tmp/deps-cache
key: splitux-deps-v2
- name: Get version info
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
RELEASE_NAME="Splitux ${VERSION}"
IS_NIGHTLY="false"
PRERELEASE="false"
else
VERSION="nightly"
RELEASE_NAME="Splitux Nightly ($(date +%Y-%m-%d))"
IS_NIGHTLY="true"
PRERELEASE="true"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "is_nightly=$IS_NIGHTLY" >> $GITHUB_OUTPUT
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
- name: Fetch dependencies
run: |
mkdir -p res/goldberg res/gamescope-splitux res/bepinex res/facepunch
CACHE_DIR="/tmp/deps-cache"
mkdir -p "$CACHE_DIR"
# Clean stale extraction dirs (self-hosted runner reuses /tmp;
# BepInEx zips contain 000-permission files that block re-extraction)
chmod -R u+rwX /tmp/bepinex-il2cpp 2>/dev/null || true
rm -rf /tmp/bepinex-mono /tmp/bepinex-mono-linux /tmp/bepinex-il2cpp
rm -rf /tmp/gbe-win /tmp/release
# Gamescope-splitux (cached - stable version, canonical splitux-gg org)
if [ -f "$CACHE_DIR/gamescope-splitux-v1.0.5.zip" ]; then
echo "Using cached gamescope-splitux"
unzip -o -q "$CACHE_DIR/gamescope-splitux-v1.0.5.zip" -d res/gamescope-splitux
else
curl -fsSL https://github.com/splitux-gg/gamescope-splitux/releases/download/v1.0.5/gamescope-splitux-v1.0.5.zip -o "$CACHE_DIR/gamescope-splitux-v1.0.5.zip"
unzip -o -q "$CACHE_DIR/gamescope-splitux-v1.0.5.zip" -d res/gamescope-splitux
fi
# Goldberg emulator — collective release v1.1.0 (splitux-gg/gbe_fork-splitux dev HEAD):
# GSE_FORCE_LOG gate + CI build-loop fix + EOSPlus/Steam friend-join (b7430e00) +
# raw-UDP<->legacy-Steam-P2P bridge (95d65d05, GSE_IP_P2P_BRIDGE). Supersedes the old
# gabrielgad/release-p2p-autoaccept (linux) + eosplus-clean (win) split pins.
curl -fsSL https://github.com/splitux-gg/gbe_fork-splitux/releases/download/v1.1.0/emu-linux-release.tar.bz2 -o /tmp/gbe-linux.tar.bz2
tar -xjf /tmp/gbe-linux.tar.bz2 -C /tmp
mkdir -p res/goldberg/{linux64,linux32,win,steamnetworkingsockets/x64,steamnetworkingsockets/x32}
cp -f /tmp/release/regular/x64/*.so res/goldberg/linux64/ 2>/dev/null || true
cp -f /tmp/release/regular/x32/*.so res/goldberg/linux32/ 2>/dev/null || true
curl -fsSL https://github.com/splitux-gg/gbe_fork-splitux/releases/download/v1.1.0/emu-win-release.7z -o /tmp/gbe-win.7z
7z x -o/tmp/gbe-win /tmp/gbe-win.7z -y
cp -f /tmp/gbe-win/release/experimental/x64/steam_api64.dll res/goldberg/win/ 2>/dev/null || true
cp -f /tmp/gbe-win/release/experimental/x86/steam_api.dll res/goldberg/win/ 2>/dev/null || true
cp -f /tmp/gbe-win/release/steamnetworkingsockets/x64/*.dll res/goldberg/steamnetworkingsockets/x64/ 2>/dev/null || true
cp -f /tmp/gbe-win/release/steamnetworkingsockets/x86/*.dll res/goldberg/steamnetworkingsockets/x32/ 2>/dev/null || true
# Fail loud if the critical Windows goldberg DLL didn't land — the artifact's
# internal layout changed before (release/regular -> release/regular/x64) and the
# `|| true` guards silently shipped an empty res/goldberg/win/ for months.
test -s res/goldberg/win/steam_api64.dll || { echo "::error::goldberg steam_api64.dll missing from res/goldberg/win/ — check artifact layout"; exit 1; }
# BepInEx 5 Mono Windows (cached)
if [ -f "$CACHE_DIR/BepInEx_win_x64_5.4.23.2.zip" ]; then
echo "Using cached BepInEx Mono Windows"
cp "$CACHE_DIR/BepInEx_win_x64_5.4.23.2.zip" /tmp/bepinex-mono.zip
else
curl -fsSL https://github.com/BepInEx/BepInEx/releases/download/v5.4.23.2/BepInEx_win_x64_5.4.23.2.zip -o /tmp/bepinex-mono.zip
cp /tmp/bepinex-mono.zip "$CACHE_DIR/BepInEx_win_x64_5.4.23.2.zip"
fi
mkdir -p res/bepinex/mono/core
unzip -o -q /tmp/bepinex-mono.zip -d /tmp/bepinex-mono
cp -f /tmp/bepinex-mono/BepInEx/core/* res/bepinex/mono/core/ 2>/dev/null || true
cp -f /tmp/bepinex-mono/winhttp.dll res/bepinex/mono/ 2>/dev/null || true
cp -f /tmp/bepinex-mono/doorstop_config.ini res/bepinex/mono/ 2>/dev/null || true
# BepInEx 5 Mono Linux (cached)
if [ -f "$CACHE_DIR/BepInEx_linux_x64_5.4.23.2.zip" ]; then
echo "Using cached BepInEx Mono Linux"
cp "$CACHE_DIR/BepInEx_linux_x64_5.4.23.2.zip" /tmp/bepinex-mono-linux.zip
else
curl -fsSL https://github.com/BepInEx/BepInEx/releases/download/v5.4.23.2/BepInEx_linux_x64_5.4.23.2.zip -o /tmp/bepinex-mono-linux.zip
cp /tmp/bepinex-mono-linux.zip "$CACHE_DIR/BepInEx_linux_x64_5.4.23.2.zip"
fi
mkdir -p res/bepinex/mono-linux/core
unzip -o -q /tmp/bepinex-mono-linux.zip -d /tmp/bepinex-mono-linux
cp -f /tmp/bepinex-mono-linux/BepInEx/core/* res/bepinex/mono-linux/core/ 2>/dev/null || true
cp -f /tmp/bepinex-mono-linux/libdoorstop.so res/bepinex/mono-linux/ 2>/dev/null || true
cp -f /tmp/bepinex-mono-linux/run_bepinex.sh res/bepinex/mono-linux/ 2>/dev/null || true
# BepInEx 6 IL2CPP (cached)
if [ -f "$CACHE_DIR/BepInEx-Unity.IL2CPP-win-x64-6.0.0-pre.2.zip" ]; then
echo "Using cached BepInEx IL2CPP"
cp "$CACHE_DIR/BepInEx-Unity.IL2CPP-win-x64-6.0.0-pre.2.zip" /tmp/bepinex-il2cpp.zip
else
curl -fsSL https://github.com/BepInEx/BepInEx/releases/download/v6.0.0-pre.2/BepInEx-Unity.IL2CPP-win-x64-6.0.0-pre.2.zip -o /tmp/bepinex-il2cpp.zip
cp /tmp/bepinex-il2cpp.zip "$CACHE_DIR/BepInEx-Unity.IL2CPP-win-x64-6.0.0-pre.2.zip"
fi
mkdir -p res/bepinex/il2cpp/core
unzip -o -q /tmp/bepinex-il2cpp.zip -d /tmp/bepinex-il2cpp
cp -f /tmp/bepinex-il2cpp/BepInEx/core/* res/bepinex/il2cpp/core/ 2>/dev/null || true
cp -f /tmp/bepinex-il2cpp/winhttp.dll res/bepinex/il2cpp/ 2>/dev/null || true
cp -f /tmp/bepinex-il2cpp/doorstop_config.ini res/bepinex/il2cpp/ 2>/dev/null || true
# SplituxFacepunch (cached)
if [ -f "$CACHE_DIR/SplituxFacepunch-v2.1.0.dll" ]; then
echo "Using cached SplituxFacepunch"
cp "$CACHE_DIR/SplituxFacepunch-v2.1.0.dll" res/facepunch/SplituxFacepunch.dll
else
curl -fsSL https://github.com/splitux-gg/bepinex-facepunch-splitux/releases/download/v2.1.0/SplituxFacepunch.dll -o res/facepunch/SplituxFacepunch.dll
cp res/facepunch/SplituxFacepunch.dll "$CACHE_DIR/SplituxFacepunch-v2.1.0.dll"
fi
- name: Build Splitux
run: |
cargo build --release
- name: Package tarball
run: |
mkdir -p release/splitux/bin release/splitux/res
# Copy binary
cp target/release/splitux release/splitux/
chmod +x release/splitux/splitux
# Copy gamescope-splitux
cp res/gamescope-splitux/bin/* release/splitux/bin/
chmod +x release/splitux/bin/*
# Copy resources (downloaded deps)
cp -r res/* release/splitux/res/
# Copy app assets (fonts, icon)
cp -r assets release/splitux/
cp LICENSE release/splitux/ 2>/dev/null || true
# Copy scripts and desktop file
cp launch.sh release/splitux/
cp install.sh release/splitux/
cp splitux.sh release/splitux/
cp splitux.desktop release/splitux/
chmod +x release/splitux/launch.sh release/splitux/install.sh release/splitux/splitux.sh
# Create tarball
cd release
tar -czvf splitux-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz splitux/
# Create zip archive
zip -r splitux-${{ steps.version.outputs.version }}-linux-x86_64.zip splitux/
- name: Build AppImage
run: |
# Download appimagetool
curl -fsSL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -o /tmp/appimagetool
chmod +x /tmp/appimagetool
# Create AppDir structure
mkdir -p AppDir/usr/bin AppDir/usr/share/applications AppDir/usr/share/icons/hicolor/256x256/apps AppDir/usr/share/splitux
# Copy binary and resources
cp target/release/splitux AppDir/usr/bin/
cp -r res/* AppDir/usr/share/splitux/
cp -r assets/* AppDir/usr/share/splitux/
cp res/gamescope-splitux/bin/* AppDir/usr/bin/
chmod +x AppDir/usr/bin/*
# Create desktop file
cat > AppDir/usr/share/applications/splitux.desktop << 'DESKTOP'
[Desktop Entry]
Type=Application
Name=Splitux
Comment=Local split-screen multiplayer for Steam games
Exec=splitux
Icon=splitux
Categories=Game;
Terminal=false
DESKTOP
# Copy/create icon (use a placeholder if none exists)
if [ -f assets/icon.png ]; then
cp assets/icon.png AppDir/usr/share/icons/hicolor/256x256/apps/splitux.png
else
echo "No icon found, AppImage will use default"
fi
# Create AppRun script
cat > AppDir/AppRun << 'APPRUN'
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export PATH="${HERE}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
export XDG_DATA_DIRS="${HERE}/usr/share:${XDG_DATA_DIRS}"
export SPLITUX_RES_PATH="${HERE}/usr/share/splitux"
exec "${HERE}/usr/bin/splitux" "$@"
APPRUN
chmod +x AppDir/AppRun
# Symlinks for AppImage
ln -sf usr/share/applications/splitux.desktop AppDir/splitux.desktop
ln -sf usr/share/icons/hicolor/256x256/apps/splitux.png AppDir/splitux.png 2>/dev/null || true
# Build AppImage (extract and run since FUSE may not work in container)
/tmp/appimagetool --appimage-extract
ARCH=x86_64 ./squashfs-root/AppRun AppDir release/Splitux-${{ steps.version.outputs.version }}-x86_64.AppImage
- name: Upload to Release
uses: softprops/action-gh-release@v2
with:
name: ${{ steps.version.outputs.release_name }}
tag_name: ${{ steps.version.outputs.is_nightly == 'true' && 'nightly' || github.ref_name }}
prerelease: ${{ steps.version.outputs.prerelease }}
body: |
${{ steps.version.outputs.is_nightly == 'true' && 'Automated nightly build. May be unstable.' || '' }}
## Downloads
| Format | Description |
|--------|-------------|
| `.tar.gz` | Native Linux tarball (recommended for most users) |
| `.zip` | Zip archive |
| `.AppImage` | Portable AppImage (no installation required) |
| `.flatpak` | Flatpak bundle (sandboxed) |
### Installation
**Tarball/Zip:**
```bash
tar -xzf splitux-*.tar.gz
cd splitux
./install.sh
```
**AppImage:**
```bash
chmod +x Splitux-*.AppImage
./Splitux-*.AppImage
```
**Flatpak:**
```bash
flatpak install Splitux-*.flatpak
flatpak run gg.splitux.Splitux
```
files: |
release/splitux-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz
release/splitux-${{ steps.version.outputs.version }}-linux-x86_64.zip
release/Splitux-${{ steps.version.outputs.version }}-x86_64.AppImage
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-flatpak:
# Same dedicated ephemeral runner as `build` above. This job's
# flatpak-builder step runs a nested bubblewrap sandbox internally - the
# ephemeral container it runs in needs --cap-add SYS_ADMIN
# --security-opt unmask=ALL at launch (set in the poller's systemd unit
# on the lab) or bwrap's own proc mount is rejected.
runs-on: [self-hosted, Linux, X64, splitux-ci]
needs: [build]
if: always() && needs.build.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version info
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
IS_NIGHTLY="false"
else
VERSION="nightly"
IS_NIGHTLY="true"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_nightly=$IS_NIGHTLY" >> $GITHUB_OUTPUT
- name: Cache flatpak-builder
uses: actions/cache@v4
with:
path: .flatpak-builder
key: splitux-flatpak-builder-${{ hashFiles('Cargo.lock') }}
restore-keys: splitux-flatpak-builder-
- name: Install Flatpak
run: |
# Self-hosted runner has no sudo; flatpak/flatpak-builder are pre-installed.
# Runtime + remote installed per-user (no root needed).
pacman -Q flatpak flatpak-builder
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform//23.08 org.freedesktop.Sdk//23.08 || true
- name: Build Flatpak
run: |
# Create flatpak manifest
cat > gg.splitux.Splitux.yml << 'MANIFEST'
app-id: gg.splitux.Splitux
runtime: org.freedesktop.Platform
runtime-version: '23.08'
sdk: org.freedesktop.Sdk
command: splitux
finish-args:
- --share=ipc
- --share=network
- --socket=x11
- --socket=wayland
- --socket=pulseaudio
- --device=all
- --filesystem=home
- --filesystem=/run/media
- --filesystem=/media
- --filesystem=/mnt
- --filesystem=xdg-run/gamescope-0:create
- --talk-name=org.freedesktop.Flatpak
- --env=PATH=/app/bin:/usr/bin
modules:
- name: bubblewrap
buildsystem: meson
sources:
- type: archive
url: https://github.com/containers/bubblewrap/releases/download/v0.8.0/bubblewrap-0.8.0.tar.xz
sha256: 957ad1149db9033db88e988b12bcebe349a445e1efc8a9b59ad2939a113d333a
- name: fuse-overlayfs
buildsystem: simple
build-commands:
- install -Dm755 fuse-overlayfs /app/bin/fuse-overlayfs
sources:
- type: file
url: https://github.com/containers/fuse-overlayfs/releases/download/v1.14/fuse-overlayfs-x86_64
sha256: 4817a8896a9e6f0433080f88f5b71dec931e8829a89d64c71af94b0630ccb4a9
dest-filename: fuse-overlayfs
- name: splitux
buildsystem: simple
build-options:
build-args:
- --share=network
env:
CARGO_HOME: /run/build/splitux/cargo
RUSTUP_HOME: /run/build/splitux/rustup
build-commands:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
- . $CARGO_HOME/env && cargo build --release
- install -Dm755 target/release/splitux /app/bin/splitux
- mkdir -p /app/share/splitux
- cp -r res/* /app/share/splitux/ || true
sources:
- type: dir
path: .
MANIFEST
# Build flatpak
flatpak-builder --force-clean --repo=repo build-dir gg.splitux.Splitux.yml
# Create single-file bundle
mkdir -p release
flatpak build-bundle repo release/Splitux-${{ steps.version.outputs.version }}.flatpak gg.splitux.Splitux
- name: Upload Flatpak to Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.is_nightly == 'true' && 'nightly' || github.ref_name }}
files: release/Splitux-${{ steps.version.outputs.version }}.flatpak
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}