ci: Build Frame Home in CI

Build Frame Home payload in CI

Build the ARM64 headset payload against the glibc 2.39 baseline, validate its ABI,
and pass the generated artifact to Studio packaging.

Add provenance attestations and document CI and local build modes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
KevinandCopilot committed 2026-10-24 18:51:06 +02:00
1 parent 5e2b42d487
commit 8d7fdfd305
7 files changed
+257 -103

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
#!/bin/sh
# Build the Steam Frame payload and its standalone installer on an ARM64 glibc 2.39 runner.
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
OUT=${FRAME_HOME_OUTPUT_DIR:-"$ROOT/build/frame-home"}
PAYLOAD="$OUT/payload"
MODE=${1:-${FRAME_HOME_BUILD_MODE:-local}}
case "$MODE" in
ci|--ci)
MODE=ci
BUILD=${FRAME_HOME_BUILD_DIR:-"$ROOT/build/frame-home-cmake-ci"}
CC=${CC:-gcc-13}
CXX=${CXX:-g++-13}
;;
local|--local)
MODE=local
BUILD=${FRAME_HOME_BUILD_DIR:-"$ROOT/build/frame-home-cmake-local"}
CC=${CC:-gcc}
CXX=${CXX:-g++}
;;
*) echo "Usage: $0 [--local|--ci]" >&2; exit 2 ;;
esac
export CC CXX
[ "$(uname -m)" = aarch64 ] || { echo "Frame Home requires an ARM64 runner" >&2; exit 1; }
if [ "$MODE" = ci ] && [ "$(getconf GNU_LIBC_VERSION)" != "glibc 2.39" ]; then
echo "Release builds require glibc 2.39 (found: $(getconf GNU_LIBC_VERSION))" >&2
exit 1
fi
for tool in cmake ninja "$CC" "$CXX" glslc readelf zip python3; do
command -v "$tool" >/dev/null 2>&1 || { echo "Missing build tool: $tool" >&2; exit 1; }
done
if [ "$MODE" = ci ]; then
"$CXX" --version | grep -q '13\.' || { echo "Release builds require GCC 13" >&2; exit 1; }
fi
if [ "$MODE" = local ]; then
cmake -S "$ROOT" -B "$BUILD" -G Ninja -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=OFF \
"-DOPENVR_LIB_DIR=${OPENVR_LIB_DIR:-/opt/steamvr/bin/linuxarm64}"
else
cmake -S "$ROOT" -B "$BUILD" -G Ninja -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON
fi
cmake --build "$BUILD" --target frame-home frame-switcher fenv-tool --parallel
rm -rf "$PAYLOAD" "$OUT/FrameHome-installer-stage" "$OUT/FrameHome-installer.zip"
mkdir -p "$PAYLOAD/bin" "$PAYLOAD/layer"
cp "$BUILD/frame-home" "$BUILD/frame-switcher" "$BUILD/fenv-tool" "$PAYLOAD/bin/"
"$CXX" -O2 -shared -fPIC -std=c++17 -fvisibility=hidden \
-I"$ROOT/third_party/vulkan-headers/include" -I"$ROOT/src" \
"$ROOT/layer/frame_home_layer.cpp" -o "$PAYLOAD/layer/libframe_home_layer.so" -lpthread
glslc "$ROOT/layer/env_ps.frag" -o "$PAYLOAD/layer/env_ps.spv"
cp "$ROOT/frame/payload/install.sh" "$PAYLOAD/install.sh"
chmod +x "$PAYLOAD/install.sh" "$PAYLOAD/bin/"*
for binary in "$PAYLOAD/bin/"* "$PAYLOAD/layer/libframe_home_layer.so"; do
readelf -h "$binary" | grep -q 'Machine:.*AArch64' || {
echo "Not an AArch64 ELF: $binary" >&2; exit 1;
}
highest=$(readelf --version-info "$binary" | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' | sort -Vu | tail -n 1 || true)
if [ "$MODE" = ci ] && [ -n "$highest" ] && [ "$(printf '%s\n' "$highest" GLIBC_2.39 | sort -V | tail -n 1)" != GLIBC_2.39 ]; then
echo "$binary requires $highest, newer than the glibc 2.39 baseline" >&2
exit 1
fi
done
STAGE="$OUT/FrameHome-installer-stage/FrameHome-installer"
mkdir -p "$STAGE"
cp -R "$PAYLOAD" "$STAGE/payload"
cp "$ROOT/LICENSE" "$STAGE/LICENSE.txt"
cp "$ROOT/frame/install-frame-home.sh" "$ROOT/frame/install-frame-home.ps1" \
"$ROOT/frame/Install Frame Home.bat" "$ROOT/frame/README-frame-home.txt" "$STAGE/"
chmod +x "$STAGE/install-frame-home.sh"
(cd "$OUT/FrameHome-installer-stage" && zip -qr "$OUT/FrameHome-installer.zip" FrameHome-installer)
rm -rf "$OUT/FrameHome-installer-stage"
python3 - "$OUT/FrameHome-installer.zip" <<'PY'
import sys
import zipfile
required = {
"FrameHome-installer/payload/install.sh",
"FrameHome-installer/payload/bin/frame-home",
"FrameHome-installer/payload/bin/frame-switcher",
"FrameHome-installer/payload/bin/fenv-tool",
"FrameHome-installer/payload/layer/libframe_home_layer.so",
"FrameHome-installer/payload/layer/env_ps.spv",
}
allowed_files = {
"FrameHome-installer/LICENSE.txt",
"FrameHome-installer/Install Frame Home.bat",
"FrameHome-installer/install-frame-home.ps1",
"FrameHome-installer/install-frame-home.sh",
"FrameHome-installer/README-frame-home.txt",
}
with zipfile.ZipFile(sys.argv[1]) as archive:
names = {name.rstrip("/") for name in archive.namelist()}
missing = required - names
unexpected = {
name for name in names
if name != "FrameHome-installer"
and name not in allowed_files
and not name.startswith("FrameHome-installer/payload/")
}
if missing or unexpected:
raise SystemExit(f"Invalid Frame Home installer: missing={sorted(missing)}, unexpected={sorted(unexpected)}")
PY
{
printf 'mode=%s\n' "$MODE"
printf 'architecture=%s\n' "$(uname -m)"
printf 'libc=%s\n' "$(getconf GNU_LIBC_VERSION)"
printf 'c_compiler=%s\n' "$("$CC" --version | head -n 1)"
printf 'cxx_compiler=%s\n' "$("$CXX" --version | head -n 1)"
printf 'cmake=%s\n' "$(cmake --version | head -n 1)"
printf 'ninja=%s\n' "$(ninja --version)"
printf 'glslc=%s\n' "$(glslc --version | head -n 1)"
} > "$OUT/build-info.txt"
for binary in "$PAYLOAD/bin/"* "$PAYLOAD/layer/libframe_home_layer.so"; do
printf '\n[%s]\n' "${binary#"$PAYLOAD/"}" >> "$OUT/build-info.txt"
readelf -d "$binary" | grep -E 'NEEDED|RPATH|RUNPATH' >> "$OUT/build-info.txt" || true
readelf --version-info "$binary" | grep -oE '(GLIBC|GLIBCXX|CXXABI)_[0-9]+(\.[0-9]+)+' | sort -Vu | tr '\n' ' ' >> "$OUT/build-info.txt" || true
printf '\n' >> "$OUT/build-info.txt"
done
echo "Built $OUT/payload, $OUT/FrameHome-installer.zip, and $OUT/build-info.txt"
+9 -22
View File
@@ -10,14 +10,13 @@
# (on Windows itself: studio\build_windows.bat x64|x86|arm64)
# macos-arm64 macos-x64 on a Mac with Xcode's command line tools; MoltenVK is bundled into the
# app from $MOLTENVK (a libMoltenVK.dylib) or Homebrew's molten-vk
# frame-home the Steam Frame installer on its own (no Studio needed): the headset
# files from frame/payload and the install scripts
# all every target this computer can build
# Output: dist/FrameStudio-<target>.zip (.tar.gz on Linux) and dist/FrameHome-installer.zip.
# Output: dist/FrameStudio-<target>.zip (.tar.gz on Linux).
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
DIST="$ROOT/dist"
BUILD="$ROOT/build"
FRAME_PAYLOAD_DIR=${FRAME_HOME_PAYLOAD_DIR:-"$ROOT/frame/payload"}
CMAKE=${CMAKE:-cmake}
TARGETS=""
while [ $# -gt 0 ]; do
@@ -33,10 +32,10 @@ say() { echo "build-studio: $*"; }
if [ "$TARGETS" = " all" ]; then
case "$(uname -s)" in
Darwin) TARGETS="macos-arm64 macos-x64 frame-home" ;;
Darwin) TARGETS="macos-arm64 macos-x64" ;;
*) case "$(uname -m)" in
aarch64|arm64) TARGETS="linux-arm64 frame-home" ;;
*) TARGETS="linux-x64 linux-x86 frame-home" ;;
aarch64|arm64) TARGETS="linux-arm64" ;;
*) TARGETS="linux-x64 linux-x86" ;;
esac
[ -x "${LLVM_MINGW:-$HOME/.local/opt/llvm-mingw}/bin/x86_64-w64-mingw32-clang" ] && TARGETS="$TARGETS windows-x64 windows-x86 windows-arm64" ;;
esac
@@ -47,7 +46,8 @@ build() {
target=$1; shift
dir="$BUILD/$target"
say "building $target"
"$CMAKE" -S "$ROOT/studio" -B "$dir" -DCMAKE_BUILD_TYPE=Release "$@" > "$dir.configure.log" 2>&1 || {
"$CMAKE" -S "$ROOT/studio" -B "$dir" -DCMAKE_BUILD_TYPE=Release \
"-DFRAME_HOME_PAYLOAD_DIR=$FRAME_PAYLOAD_DIR" "$@" > "$dir.configure.log" 2>&1 || {
tail -20 "$dir.configure.log"; say "$target: configure failed (full log: $dir.configure.log)"; exit 1; }
"$CMAKE" --build "$dir" --target frame-studio --parallel
}
@@ -73,8 +73,8 @@ stage_pc() {
cp "$home/frame-home" "$home/frame-switcher" "$stage/pc/bin/"
${CXX:-g++} -O2 -shared -fPIC -std=c++17 -fvisibility=hidden -I"$ROOT/third_party/vulkan-headers/include" -I"$ROOT/src" \
"$ROOT/layer/frame_home_layer.cpp" -o "$stage/pc/layer/libframe_home_layer.so" -lpthread
cp "$ROOT/frame/payload/layer/env_ps.spv" "$stage/pc/layer/"
cp "$ROOT/frame/payload/install.sh" "$stage/pc/" && chmod +x "$stage/pc/install.sh" "$stage"/pc/bin/* ;;
cp "$FRAME_PAYLOAD_DIR/layer/env_ps.spv" "$stage/pc/layer/"
cp "$FRAME_PAYLOAD_DIR/install.sh" "$stage/pc/" && chmod +x "$stage/pc/install.sh" "$stage"/pc/bin/* ;;
esac
}
@@ -112,18 +112,6 @@ bundle_moltenvk() {
fi
}
# dist/FrameHome-installer.zip: Frame Home for the Steam Frame, installed without Frame Studio
package_frame_home() {
stage="$BUILD/stage/FrameHome-installer"
rm -rf "$stage" && mkdir -p "$stage"
cp -R "$ROOT/frame/payload" "$stage/payload"
cp "$ROOT/LICENSE" "$stage/LICENSE.txt"
cp "$ROOT/frame/install-frame-home.sh" "$ROOT/frame/install-frame-home.ps1" "$ROOT/frame/Install Frame Home.bat" "$ROOT/frame/README-frame-home.txt" "$stage/"
chmod +x "$stage/install-frame-home.sh" "$stage/payload/install.sh" "$stage"/payload/bin/* 2>/dev/null || true
(cd "$BUILD/stage" && rm -f "$DIST/FrameHome-installer.zip" && zip -qr "$DIST/FrameHome-installer.zip" FrameHome-installer)
say "frame-home -> dist/FrameHome-installer.zip"
}
for t in $TARGETS; do
case "$t" in
linux-x64) build "$t" -G Ninja && build_pc_home "$t" -G Ninja && package "$t" ;;
@@ -135,7 +123,6 @@ for t in $TARGETS; do
windows-arm64) build "$t" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/llvm-mingw.cmake" -DWINDOWS_ARCH=aarch64 && package "$t" ;;
macos-arm64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 && package "$t" ;;
macos-x64) build "$t" -G Ninja -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 && package "$t" ;;
frame-home) package_frame_home ;;
*) say "unknown target: $t (see --help)"; exit 1 ;;
esac
done