Skip to content

Commit abcdaec

Browse files
authored
Merge pull request #155 from MoonModules/next-iteration
A slow client never stalls the lights; a restore lands; boards go by name
2 parents f030e3f + e76c803 commit abcdaec

213 files changed

Lines changed: 7348 additions & 4071 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/actions/ccache/action.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: ccache
2+
description: The compiler cache for one job, restored from that job's newest run and saved after this one.
3+
# The key rotates per commit and restores from the newest matching prefix, the standard shape for a
4+
# compiler cache. The directory lives in the workspace (gitignored, so the build id stays clean),
5+
# where a build container mounting the workspace reaches it too. The builds pick ccache up
6+
# themselves once it is installed: build-system.md, The compiler cache.
7+
inputs:
8+
key:
9+
description: The job's cache name, one per configuration, since one configuration's objects never hit another's.
10+
required: true
11+
install:
12+
description: Install ccache on the runner; 'false' when the build runs in a container that ships it.
13+
default: 'true'
14+
runs:
15+
using: composite
16+
steps:
17+
- if: inputs.install == 'true' && runner.os == 'Linux'
18+
shell: bash
19+
run: sudo apt-get update -qq && sudo apt-get install -y -qq ccache
20+
- if: inputs.install == 'true' && runner.os == 'macOS'
21+
shell: bash
22+
run: brew install ccache
23+
- uses: actions/cache@v4
24+
with:
25+
path: .ccache
26+
key: ccache-${{ inputs.key }}-${{ github.sha }}
27+
restore-keys: ccache-${{ inputs.key }}-
28+
# 500 MB a job keeps every job's cache inside the repository's 10 GB together.
29+
- shell: bash
30+
run: |
31+
mkdir -p .ccache
32+
echo "max_size = 500M" > .ccache/ccache.conf
33+
echo "CCACHE_DIR=$GITHUB_WORKSPACE/.ccache" >> "$GITHUB_ENV"

‎.github/workflows/findings.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ jobs:
2929
run: |
3030
curl -sSL https://github.com/errata-ai/vale/releases/download/v3.21.0/vale_3.21.0_Linux_64-bit.tar.gz | tar -xz -C "$RUNNER_TEMP" vale
3131
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
32-
# check_code reads a configured tree; check_docgen parses the headers, and one includes the build_info.h the build generates.
33-
- name: configure the desktop build, and generate build_info.h
32+
# check_code reads a configured tree; check_docgen parses the sources, and build_info.cpp includes the build_id.h the build generates.
33+
- name: configure the desktop build, and generate build_id.h
3434
run: |
3535
cmake -S . -B build/linux
3636
uv run python moondeck/build/generate_build_info.py

‎.github/workflows/release.yml‎

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)