diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2619e4..a203bfd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,9 +37,29 @@ jobs: - { arch: arm64, runner: ubuntu-24.04-arm, extra: "" } runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + FRAME_USE_CCACHE: "1" + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + CCACHE_COMPILERCHECK: content + APT_CACHE_DIR: ${{ github.workspace }}/.apt-cache steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Restore compiler cache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-studio-linux-${{ matrix.arch }}-${{ github.sha }} + restore-keys: ccache-studio-linux-${{ matrix.arch }}- + - name: Restore apt package cache + uses: actions/cache@v5 + with: + path: .apt-cache/archives + key: apt-ubuntu-24.04-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: apt-ubuntu-24.04-${{ matrix.arch }}- + - name: Download Frame Home payload + uses: actions/download-artifact@v4 with: name: FrameHome-payload path: build/frame-home/payload @@ -47,14 +67,20 @@ jobs: run: | # x86 links against 32-bit X11, so apt needs the i386 packages too if [ "${{ matrix.i386 }}" = "true" ]; then sudo dpkg --add-architecture i386; fi + sudo mkdir -p "$APT_CACHE_DIR/archives/partial" + sudo chown _apt:root "$APT_CACHE_DIR/archives/partial" sudo apt-get update - sudo apt-get install -y ninja-build cmake libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxkbcommon-dev ${{ matrix.extra }} + sudo apt-get -o Dir::Cache::archives="$APT_CACHE_DIR/archives" install -y ccache ninja-build cmake libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxkbcommon-dev ${{ matrix.extra }} + sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives" - name: Build run: sh scripts/build-studio.sh linux-${{ matrix.arch }} - - uses: actions/upload-artifact@v4 + - name: Show compiler cache statistics + run: ccache --show-stats + - name: Upload Linux build + uses: actions/upload-artifact@v4 with: name: FrameStudio-linux-${{ matrix.arch }} - path: dist/FrameStudio-linux-${{ matrix.arch }}.tar.gz + path: build/stage/FrameStudio-linux-${{ matrix.arch }} windows: name: Windows ${{ matrix.arch }} @@ -66,8 +92,10 @@ jobs: runs-on: windows-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Download Frame Home payload + uses: actions/download-artifact@v4 with: name: FrameHome-payload path: build/frame-home/payload @@ -83,12 +111,11 @@ jobs: Copy-Item "build/windows-${{ matrix.arch }}/Frame Studio.exe" $stage Copy-Item -Recurse "build/windows-${{ matrix.arch }}/frame" "$stage/frame" Copy-Item LICENSE "$stage/LICENSE.txt" - New-Item -ItemType Directory -Force dist | Out-Null - Compress-Archive -Path $stage -DestinationPath "dist/$name.zip" -Force - - uses: actions/upload-artifact@v4 + - name: Upload Windows build + uses: actions/upload-artifact@v4 with: name: FrameStudio-windows-${{ matrix.arch }} - path: dist/FrameStudio-windows-${{ matrix.arch }}.zip + path: build/stage/FrameStudio-windows-${{ matrix.arch }} macos: name: macOS ${{ matrix.arch }} @@ -103,47 +130,89 @@ jobs: - { arch: x64, runner: macos-15-intel } runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + FRAME_USE_CCACHE: "1" + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + CCACHE_COMPILERCHECK: content steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Restore compiler cache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-studio-macos-${{ matrix.arch }}-${{ github.sha }} + restore-keys: ccache-studio-macos-${{ matrix.arch }}- + - name: Restore Homebrew package cache + uses: actions/cache@v5 + with: + path: ~/Library/Caches/Homebrew/downloads + key: brew-macos-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: brew-macos-${{ matrix.arch }}- + - name: Download Frame Home payload + uses: actions/download-artifact@v4 with: name: FrameHome-payload path: build/frame-home/payload - name: Tools and MoltenVK (Vulkan on Metal, bundled into the app) - run: brew install ninja molten-vk + run: brew install ccache ninja molten-vk - name: Build run: sh scripts/build-studio.sh macos-${{ matrix.arch }} - - uses: actions/upload-artifact@v4 + - name: Show compiler cache statistics + run: ccache --show-stats + - name: Upload macOS build + uses: actions/upload-artifact@v4 with: name: FrameStudio-macos-${{ matrix.arch }} - path: dist/FrameStudio-macos-${{ matrix.arch }}.zip + path: build/stage/FrameStudio-macos-${{ matrix.arch }} frame-home: name: Build Frame Home payload runs-on: ubuntu-24.04-arm timeout-minutes: 45 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + CCACHE_COMPILERCHECK: content + APT_CACHE_DIR: ${{ github.workspace }}/.apt-cache steps: - - uses: actions/checkout@v4 + - name: Check out source + uses: actions/checkout@v4 + - name: Restore compiler cache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-frame-home-arm64-${{ github.sha }} + restore-keys: ccache-frame-home-arm64- + - name: Restore apt package cache + uses: actions/cache@v5 + with: + path: .apt-cache/archives + key: apt-ubuntu-24.04-arm64-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: apt-ubuntu-24.04-arm64- - name: Install Frame Home build dependencies run: | set -euo pipefail + sudo mkdir -p "$APT_CACHE_DIR/archives/partial" + sudo chown _apt:root "$APT_CACHE_DIR/archives/partial" sudo apt-get update - sudo apt-get install -y cmake ninja-build gcc-13 g++-13 glslc zip + sudo apt-get -o Dir::Cache::archives="$APT_CACHE_DIR/archives" install -y ccache cmake ninja-build gcc-13 g++-13 glslc zip + sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives" - name: Build payload and installer run: sh scripts/build-frame-home.sh --ci - - uses: actions/upload-artifact@v4 + - name: Show compiler cache statistics + run: ccache --show-stats + - name: Upload Frame Home payload + uses: actions/upload-artifact@v4 with: name: FrameHome-payload path: build/frame-home/payload - - uses: actions/upload-artifact@v4 + - name: Upload Frame Home installer + uses: actions/upload-artifact@v4 with: name: FrameHome-installer - path: build/frame-home/FrameHome-installer.zip - if-no-files-found: error - - uses: actions/upload-artifact@v4 - with: - name: FrameHome-build-info - path: build/frame-home/build-info.txt + path: build/frame-home/FrameHome-installer-stage/FrameHome-installer if-no-files-found: error release: name: Publish @@ -163,19 +232,60 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} steps: - - uses: actions/download-artifact@v4 + - name: Download Studio build artifacts + uses: actions/download-artifact@v4 with: pattern: FrameStudio-* path: dist - merge-multiple: true - - uses: actions/download-artifact@v4 + merge-multiple: false + - name: Download Frame Home payload + uses: actions/download-artifact@v4 with: name: FrameHome-payload path: ${{ runner.temp }}/frame-home-payload - - uses: actions/download-artifact@v4 + - name: Download Frame Home installer + uses: actions/download-artifact@v4 with: name: FrameHome-installer - path: dist + path: dist/FrameHome-installer + - name: Rebuild release archives + run: | + for package in dist/FrameStudio-linux-*; do + [ -d "$package" ] || continue + name=${package##*/} + if [ -d "$package/frame/bin" ]; then + chmod +x "$package/frame/install.sh" "$package/frame/bin/"* + fi + tar -czf "dist/$name.tar.gz" -C dist "$name" + rm -rf "$package" + done + for package in dist/FrameStudio-windows-* dist/FrameStudio-macos-*; do + [ -d "$package" ] || continue + name=${package##*/} + if [ -d "$package/frame/bin" ]; then + chmod +x "$package/frame/install.sh" "$package/frame/bin/"* + fi + if [[ "$name" == FrameStudio-macos-* ]]; then + chmod +x "$package/Frame Studio.app/Contents/MacOS/Frame Studio" + fi + (cd dist && zip -qr "$name.zip" "$name") + rm -rf "$package" + done + chmod +x dist/FrameHome-installer/install-frame-home.sh \\ + dist/FrameHome-installer/payload/install.sh \\ + dist/FrameHome-installer/payload/bin/* + (cd dist && zip -qr FrameHome-installer.zip FrameHome-installer) + rm -rf dist/FrameHome-installer + python3 - <<'PY' + from pathlib import Path + from zipfile import ZipFile + + for path in Path("dist").glob("*.zip"): + with ZipFile(path) as archive: + nested = [name for name in archive.namelist() if name.lower().endswith(".zip")] + if nested: + raise SystemExit(f"{path} contains nested ZIP files: {nested}") + PY # What Frame Studio (studio/updater.h) and the headset's Environments panel check for updates: this commit, # Frame Home's identity (a hash of the headset programs, the same one the panel works out for what's installed) # and the files in this release. @@ -185,10 +295,12 @@ jobs: files=$(ls dist | python3 -c 'import json, sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))') printf '{"build": "%s", "home": "%s", "date": "%s", "files": %s}\n' "$GITHUB_SHA" "$home" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$files" > dist/latest.json cat dist/latest.json - - uses: actions/attest-build-provenance@v2 + - name: Attest Studio release archives + uses: actions/attest-build-provenance@v2 with: subject-path: dist/FrameStudio-* - - uses: actions/attest-build-provenance@v2 + - name: Attest Frame Home installer + uses: actions/attest-build-provenance@v2 with: subject-path: dist/FrameHome-installer.zip - name: Version release (tag v*) diff --git a/docs/building.md b/docs/building.md index 6f84d58..8d72f3a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -122,6 +122,8 @@ rolling `latest` release and one shared `v*` tag namespace; each release contain | By hand | **Actions > build > Run workflow** | A newer push to the same branch cancels a run that is still going. +Linux, macOS, and Frame Home jobs cache compiler outputs between runs; Windows/MSVC builds are not cached. +Linux jobs also cache downloaded apt packages, and macOS caches Homebrew downloads; apt repository metadata is still refreshed each run. To publish versions: @@ -148,14 +150,15 @@ sh scripts/build-frame-home.sh This uses the headset's native OpenVR library and Vulkan SDK, normally at `/opt/steamvr/bin/linuxarm64`, and requires CMake, Ninja, a C/C++ compiler, `glslc`, `zip`, and Python 3. Set `OPENVR_LIB_DIR` if SteamVR is installed elsewhere. The -payload, installer ZIP, and build manifest are written under `build/frame-home/`. This mode accepts the headset's +payload, installer ZIP, and build manifest are written under `build/frame-home/`; the build manifest is included in the +payload artifact as `build/frame-home/payload/build-info.txt`. This mode accepts the headset's installed glibc version, including Preview's 2.43; those local outputs are for development and are not the release build for stable/beta. CI selects release mode explicitly with `sh scripts/build-frame-home.sh --ci` (or `FRAME_HOME_BUILD_MODE=ci`). It requires ARM64 Linux with glibc 2.39 and GCC 13, then checks the ELF architecture and GLIBC symbol ceiling before packaging. The build manifest records compiler/tool versions, dynamic dependencies, and -GLIBC/GLIBCXX/CXXABI requirements in `build/frame-home/build-info.txt`; CI retains it as a run artifact. Studio can +GLIBC/GLIBCXX/CXXABI requirements in `build/frame-home/payload/build-info.txt`; CI keeps it with the payload artifact. Studio can bundle a locally built or downloaded payload by setting `FRAME_HOME_PAYLOAD_DIR` to that payload directory. The installer ZIP is checked against a Frame Home-only file allowlist before it is published. diff --git a/scripts/build-frame-home.sh b/scripts/build-frame-home.sh index 5235061..5072822 100755 --- a/scripts/build-frame-home.sh +++ b/scripts/build-frame-home.sh @@ -31,6 +31,7 @@ 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 + command -v ccache >/dev/null 2>&1 || { echo "Release builds require ccache" >&2; exit 1; } "$CXX" --version | grep -q '13\.' || { echo "Release builds require GCC 13" >&2; exit 1; } fi @@ -38,14 +39,18 @@ 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 + cmake -S "$ROOT" -B "$BUILD" -G Ninja -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache 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 \ +compile_layer() { + if [ "$MODE" = ci ]; then ccache "$CXX" "$@"; else "$CXX" "$@"; fi +} +compile_layer -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" @@ -63,6 +68,24 @@ for binary in "$PAYLOAD/bin/"* "$PAYLOAD/layer/libframe_home_layer.so"; do fi done +{ + 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 +cp "$OUT/build-info.txt" "$PAYLOAD/build-info.txt" + STAGE="$OUT/FrameHome-installer-stage/FrameHome-installer" mkdir -p "$STAGE" cp -R "$PAYLOAD" "$STAGE/payload" @@ -71,13 +94,13 @@ 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/build-info.txt", "FrameHome-installer/payload/bin/frame-home", "FrameHome-installer/payload/bin/frame-switcher", "FrameHome-installer/payload/bin/fenv-tool", @@ -102,20 +125,4 @@ with zipfile.ZipFile(sys.argv[1]) as archive: 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" +echo "Built $OUT/payload (including build-info.txt), $OUT/FrameHome-installer.zip" diff --git a/scripts/build-studio.sh b/scripts/build-studio.sh index e4c8308..13c1cb7 100755 --- a/scripts/build-studio.sh +++ b/scripts/build-studio.sh @@ -37,6 +37,17 @@ mkdir -p "$DIST" "$BUILD" say() { echo "build-studio: $*"; } say "using Frame Home payload: $FRAME_PAYLOAD_DIR" +configure() { + source=$1; dir=$2; shift 2 + if [ "${FRAME_USE_CCACHE:-0}" = 1 ]; then + command -v ccache >/dev/null 2>&1 || { say "FRAME_USE_CCACHE is set but ccache is unavailable"; exit 1; } + "$CMAKE" -S "$source" -B "$dir" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache "$@" + else + "$CMAKE" -S "$source" -B "$dir" -DCMAKE_BUILD_TYPE=Release "$@" + fi +} + if [ "$TARGETS" = " all" ]; then case "$(uname -s)" in Darwin) TARGETS="macos-arm64 macos-x64" ;; @@ -53,8 +64,7 @@ build() { target=$1; shift dir="$BUILD/$target" say "building $target" - "$CMAKE" -S "$ROOT/studio" -B "$dir" -DCMAKE_BUILD_TYPE=Release \ - "-DFRAME_HOME_PAYLOAD_DIR=$FRAME_PAYLOAD_DIR" "$@" > "$dir.configure.log" 2>&1 || { + configure "$ROOT/studio" "$dir" "-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 } @@ -65,7 +75,7 @@ build_pc_home() { target=$1; shift dir="$BUILD/$target-home" say "building Frame Home for $target's SteamVR" - "$CMAKE" -S "$ROOT" -B "$dir" -DCMAKE_BUILD_TYPE=Release -DFRAME_HOME_PC=ON "$@" > "$dir.configure.log" 2>&1 || { + configure "$ROOT" "$dir" -DFRAME_HOME_PC=ON "$@" > "$dir.configure.log" 2>&1 || { tail -20 "$dir.configure.log"; say "$target: Frame Home configure failed (full log: $dir.configure.log)"; exit 1; } "$CMAKE" --build "$dir" --target frame-home frame-switcher --parallel }