@@ -173,6 +173,11 @@ jobs:
173173 V=$(uv run python moondeck/build/compute_version.py --tag "$TAG")
174174 echo "version=$V" >> "$GITHUB_OUTPUT"
175175
176+ # The espressif/idf image ships ccache and turns it on; the workspace mount carries the cache in and out.
177+ - uses : ./.github/actions/ccache
178+ with :
179+ key : esp32-${{ matrix.firmware }}
180+ install : ' false'
176181 - name : Build firmware
177182 uses : espressif/esp-idf-ci-action@v1
178183 with :
@@ -184,7 +189,8 @@ jobs:
184189 # We run our own builder (not the action's default `idf.py build`)
185190 # so the sdkconfig fragments and EXCLUDE_COMPONENTS go through the
186191 # same code path as local builds. --release burns the channel tag in.
187- command : python ../moondeck/build/build_esp32.py --firmware ${{ matrix.firmware }} --release "${{ steps.tag.outputs.tag }}" --version "${{ steps.ver.outputs.version }}"
192+ command : python ../moondeck/build/build_esp32.py --firmware ${{ matrix.firmware }} --release "${{ steps.tag.outputs.tag }}" --version "${{ steps.ver.outputs.version }}" && ccache --show-stats
193+ extra_docker_args : -e CCACHE_DIR=/app/${{ github.repository }}/.ccache
188194
189195 - name : Stage release artifacts
190196 run : |
@@ -277,28 +283,11 @@ jobs:
277283 set -euo pipefail
278284 V=$(uv run python moondeck/build/compute_version.py --tag "$TAG")
279285 echo "version=$V" >> "$GITHUB_OUTPUT"
280- # ccache: the macOS runner compiles the whole desktop tree from cold every run (the ESP32
281- # jobs get a prebuilt IDF container; this one gets nothing), which is why it was the slowest
282- # job in the workflow at ~10 min. CMake picks the launcher up from the environment, so the
283- # build script stays unchanged. The key rotates per run and restores from the newest
284- # matching prefix, the standard save-always cache shape for a compiler cache.
285- - name : Install ccache
286- run : brew install ccache
287- - name : Restore ccache
288- uses : actions/cache@v4
286+ - uses : ./.github/actions/ccache
289287 with :
290- path : ~/Library/Caches/ccache
291- key : ccache-macos-14-${{ github.sha }}
292- restore-keys : |
293- ccache-macos-14-
288+ key : macos-arm64
294289 - name : Build + package macOS arm64
295- env :
296- CMAKE_CXX_COMPILER_LAUNCHER : ccache
297- CMAKE_C_COMPILER_LAUNCHER : ccache
298- run : uv run moondeck/ci/package_desktop.py --version "${{ steps.ver.outputs.version }}"
299- - name : ccache stats
300- if : always()
301- run : ccache --show-stats
290+ run : uv run moondeck/ci/package_desktop.py --version "${{ steps.ver.outputs.version }}" && ccache --show-stats
302291 - uses : actions/upload-artifact@v4
303292 with :
304293 name : desktop-macos
@@ -378,9 +367,17 @@ jobs:
378367 name : desktop-windows
379368 path : dist/
380369
370+ # **ubuntu-22.04, pinned like build-linux-arm64, and this is load-bearing.** A binary carries the
371+ # glibc floor of the machine that built it, and `ubuntu-latest` moves: when it moved on, the amd64
372+ # package came to need glibc 2.43 and died at startup on Debian 13, Ubuntu 24.04 and in our own
373+ # container, while the arm64 one still needed 2.35. Both build on 22.04 now, with the same GCC 12,
374+ # and check-glibc-floor below fails the release if either needs more.
381375 build-linux-x64 :
382376 needs : verify-version
383- runs-on : ubuntu-latest
377+ runs-on : ubuntu-22.04
378+ env :
379+ CC : gcc-12 # 22.04's default is GCC 11; 12 is what build-linux-arm64 builds with
380+ CXX : g++-12
384381 steps :
385382 - uses : actions/checkout@v4
386383 with :
@@ -395,11 +392,10 @@ jobs:
395392 # should not abort a release on its own; CMake's own check is what decides, and it fails the
396393 # build loudly when the library really is absent.
397394 - name : Install libcurl (MoonCloud, only used once consent is given in the app)
398- run : sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev || true
399- # Builds with the runner's default GCC (13 on ubuntu-24.04). Its four false-positive
400- # warnings are handled in CMakeLists.txt, which demotes them to non-fatal on GCC below
401- # 16 rather than pinning a compiler here: a from-source build on Debian or Raspberry Pi
402- # OS meets the same GCC and deserves the same fix.
395+ run : sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev g++-12 || true
396+ # GCC 12's four false-positive warnings are handled in CMakeLists.txt, which demotes them to
397+ # non-fatal on GCC below 16: a from-source build on Debian or Raspberry Pi OS meets the same
398+ # GCC and deserves the same fix.
403399 # Same tag + version resolution as build-esp32 (see the comments there): the computed
404400 # semver must reach the DESKTOP binary too, or a desktop build reports library.json's bare
405401 # core and the update badge can never see a `latest` (-dev.N) build as newer. shell: bash
@@ -426,8 +422,13 @@ jobs:
426422 set -euo pipefail
427423 V=$(uv run python moondeck/build/compute_version.py --tag "$TAG")
428424 echo "version=$V" >> "$GITHUB_OUTPUT"
425+ - uses : ./.github/actions/ccache
426+ with :
427+ key : linux-x64
429428 - name : Build + package Linux x64
430- run : uv run moondeck/ci/package_desktop.py --version "${{ steps.ver.outputs.version }}"
429+ run : uv run moondeck/ci/package_desktop.py --version "${{ steps.ver.outputs.version }}" && ccache --show-stats
430+ - name : Check the glibc floor
431+ run : uv run moondeck/ci/check_glibc_floor.py dist/moonlight_*_amd64.deb
431432 - uses : actions/upload-artifact@v4
432433 with :
433434 name : desktop-linux
@@ -442,11 +443,14 @@ jobs:
442443 # cleanly and then dies at startup with "GLIBC_2.38 not found", the same failure the Dockerfile
443444 # records hitting on debian12. Building against the lower floor covers Bookworm (2.36), Debian 13
444445 # trixie (2.41) and Ubuntu 24.04 (2.39) alike, because glibc symbol versioning is backward
445- # compatible. Its GCC is 12.3 rather than 13 , which CMakeLists already covers: it demotes the four
446- # false-positive warnings for every GCC below 16.
446+ # compatible. Its GCC is 12, which CMakeLists already covers: it demotes the four false-positive
447+ # warnings for every GCC below 16.
447448 build-linux-arm64 :
448449 needs : verify-version
449450 runs-on : ubuntu-22.04-arm
451+ env :
452+ CC : gcc-12 # named, as in build-linux-x64, so both architectures build with one compiler
453+ CXX : g++-12
450454 steps :
451455 - uses : actions/checkout@v4
452456 with :
@@ -460,7 +464,7 @@ jobs:
460464 # should not abort a release on its own; CMake's own check is what decides, and it fails the
461465 # build loudly when the library really is absent.
462466 - name : Install libcurl (MoonCloud, only used once consent is given in the app)
463- run : sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev || true
467+ run : sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev g++-12 || true
464468 # Same tag + version resolution as every other build job; see build-linux-x64 for why.
465469 - name : Resolve release tag
466470 id : tag
@@ -483,8 +487,13 @@ jobs:
483487 set -euo pipefail
484488 V=$(uv run python moondeck/build/compute_version.py --tag "$TAG")
485489 echo "version=$V" >> "$GITHUB_OUTPUT"
490+ - uses : ./.github/actions/ccache
491+ with :
492+ key : linux-arm64
486493 - name : Build + package Linux arm64
487- run : uv run moondeck/ci/package_desktop.py --version "${{ steps.ver.outputs.version }}"
494+ run : uv run moondeck/ci/package_desktop.py --version "${{ steps.ver.outputs.version }}" && ccache --show-stats
495+ - name : Check the glibc floor
496+ run : uv run moondeck/ci/check_glibc_floor.py dist/moonlight_*_arm64.deb
488497 - uses : actions/upload-artifact@v4
489498 with :
490499 name : desktop-linux-arm64
@@ -507,6 +516,9 @@ jobs:
507516 with :
508517 path : ~/teensy/arm-gnu-toolchain-*
509518 key : teensy-gcc-${{ runner.os }}-${{ steps.gcc.outputs.version }}
519+ - uses : ./.github/actions/ccache
520+ with :
521+ key : teensy
510522 # Same tag and version resolution as build-esp32, so the HEX reports the version and channel its asset name carries.
511523 - name : Resolve release tag
512524 id : tag
@@ -653,7 +665,7 @@ jobs:
653665 BASE="https://github.com/${REPO}/releases/download/$TAG"
654666 # The shipping firmware list — the same mooninstaller/firmwares.json the
655667 # build matrix reads, so manifests and builds can't drift.
656- for F in $(jq -r '.firmwares[] | select(.ships) | .name' mooninstaller/firmwares.json); do
668+ for F in $(jq -r '.firmwares[] | select(.ships and .platform != "desktop" ) | .name' mooninstaller/firmwares.json); do
657669 # An ESP32 manifest takes its offsets from IDF's flasher args; a Teensy one is the flat image at 0.
658670 ARGS=()
659671 [ -f "dist/flasher-$F.json" ] && ARGS=(--flasher-args "dist/flasher-$F.json")
@@ -819,18 +831,18 @@ jobs:
819831 ARG TARGETARCH
820832 COPY moonlight_$TARGETARCH.deb /tmp/moonlight.deb
821833 # libcurl4t64 is installed because the binary LINKS it (the one outbound HTTPS call), and
822- # the ldd sweep then collects it plus everything it needs. Naming the libraries by hand
823- # would be wrong the first time that chain moved .
834+ # libpulse0 because its audio input LOADS it at runtime, which ldd cannot see; the sweep
835+ # then collects both plus everything they need. See ../../Dockerfile .
824836 RUN apt-get update \
825- && apt-get install -y --no-install-recommends libcurl4t64 \
837+ && apt-get install -y --no-install-recommends libcurl4t64 libpulse0 \
826838 && rm -rf /var/lib/apt/lists/* \
827839 && dpkg-deb -x /tmp/moonlight.deb /rootfs \
828840 && mkdir -p /deps \
829- && ldd /rootfs/ usr/bin/MoonLight | awk '/=> \//{print $3}' | sort -u | grep -vE '/(libc|libm|libstdc\+\+|libgcc_s)\ .so' | xargs -I{} cp -L {} /deps/
830- # debian13, NOT debian12: the amd64 binary is built on ubuntu-24.04 (glibc 2.39) and
831- # needs glibc >= 2.38, where bookworm ships 2.36 and it dies at startup. The arm64 binary
832- # is built on ubuntu- 22.04-arm and floors at 2.35, so it would also run on a bookworm
833- # base; one base for both keeps the two images the same runtime . See ../../Dockerfile.
841+ && pulse=$(find / usr/lib -name libpulse .so.0 | head -1) \
842+ && test -n "$pulse" \
843+ && { ldd /rootfs/usr/bin/MoonLight; ldd "$pulse"; echo "libpulse.so.0 => $pulse (0)"; } | awk '/=> \//{print $3}' | sort -u | grep -vE '/(libc|libm|libstdc\+\+|libgcc_s)\.so' | xargs -I{} cp -L {} /deps/
844+ # Both binaries are built on Ubuntu 22.04 and need glibc 2.35 at most, which
845+ # check_glibc_floor.py enforces, so debian13 (2.41) runs them . See ../../Dockerfile.
834846 # Pinned by digest, not by tag: `cc-debian13` is mutable, so an unpinned base means two
835847 # builds of the same commit can ship different runtimes. Re-pin deliberately when picking
836848 # up base updates (docker buildx imagetools inspect gcr.io/distroless/cc-debian13:latest).
@@ -839,15 +851,56 @@ jobs:
839851 FROM gcr.io/distroless/cc-debian13@sha256:9b615fff20e1a4fad29c2b30562580b212c7dd5e2225236735cca0070ed11c78
840852 COPY --from=fetch /rootfs/usr/bin/MoonLight /usr/bin/MoonLight
841853 # The libraries the loader resolved, collected in the fetch stage. Without them the
842- # container dies at startup on "libcurl.so.4: cannot open shared object file": this job
843- # builds the image but never runs it, so that failure passed CI and reached a user .
854+ # container dies at startup on "libcurl.so.4: cannot open shared object file", which the
855+ # run before publishing now catches .
844856 COPY --from=fetch /deps/ /usr/lib/
845857 ENV XDG_DATA_HOME=/data
846858 VOLUME /data
847859 EXPOSE 8080
848860 ENTRYPOINT ["/usr/bin/MoonLight"]
849861 DOCKERFILE
850862
863+ # Each architecture's image runs before any is published, since an image whose binary dies at
864+ # startup (a missing library, a glibc its base lacks) installs cleanly and fails on a user's board.
865+ # One load per platform, since a multi-platform build cannot load; the push below reuses the cache.
866+ - uses : docker/build-push-action@v6
867+ with :
868+ context : ctx
869+ platforms : linux/amd64
870+ load : true
871+ tags : moonlight-smoke:amd64
872+ - uses : docker/build-push-action@v6
873+ with :
874+ context : ctx
875+ platforms : linux/arm64
876+ load : true
877+ tags : moonlight-smoke:arm64
878+ # amd64 runs natively and must answer its API. arm64 runs under QEMU, where an emulated render
879+ # loop makes a response time a matter of the runner, so it must survive its start: the loader
880+ # resolves every library and glibc symbol in the first second or the process dies.
881+ - name : Run each image
882+ run : |
883+ set -euo pipefail
884+ docker run -d --name smoke-amd64 --platform linux/amd64 -p 18080:8080 moonlight-smoke:amd64
885+ ok=0
886+ for i in $(seq 1 60); do
887+ if curl -fs http://localhost:18080/api/system > /dev/null; then ok=1; break; fi
888+ sleep 1
889+ done
890+ docker logs smoke-amd64 | tail -20
891+ docker rm -f smoke-amd64
892+ test "$ok" = 1 || { echo "the amd64 image did not answer within 60 s" >&2; exit 1; }
893+ echo "the amd64 image answers"
894+ docker run -d --name smoke-arm64 --platform linux/arm64 moonlight-smoke:arm64
895+ sleep 15
896+ running=$(docker inspect -f '{{.State.Running}}' smoke-arm64)
897+ docker logs smoke-arm64 2>&1 | tail -20 | tee arm64.log
898+ docker rm -f smoke-arm64
899+ if [ "$running" != true ] || grep -qE "GLIBC_|cannot open shared object" arm64.log; then
900+ echo "the arm64 image did not survive its start" >&2; exit 1
901+ fi
902+ echo "the arm64 image runs"
903+
851904 # Two tags: the exact version, which never moves, and `latest`, which follows this workflow's
852905 # own notion of latest (the rolling prerelease from main, or a tagged release). A user pins
853906 # one or tracks the other.
0 commit comments