diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index 3e298fb..dd6033f 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -47,22 +47,59 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Install build dependencies + # In a manylinux_2_28 container rather than on the runner, for its glibc 2.28, which is + # RHEL 8's. A library records the glibc symbol versions of the headers it was compiled + # against, and the loader refuses it anywhere older. Built on the runner's own Ubuntu 24.04 + # it needed GLIBC_2.38, so the Linux head could not load it on Ubuntu 22.04, Debian 12, or + # RHEL 8 or 9, and BuildMonitor exited there without opening a window. + - name: Build if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - cmake ninja-build \ - libx11-dev libxrandr-dev libxi-dev libxcursor-dev libxinerama-dev \ - libgl1-mesa-dev libglu1-mesa-dev libwayland-dev libxkbcommon-dev + run: docker run --rm -v "$PWD:/src" -w /src "quay.io/pypa/manylinux_2_28_$(uname -m)" bash native/build-linux.sh - - name: Configure - if: matrix.rid != 'osx' - run: cmake -S native -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - - - name: Build - if: matrix.rid != 'osx' - run: cmake --build build --config Release + # The floor the container is there to hold, checked rather than trusted, since nothing else + # would notice until a user on an older distribution did. + - name: Check glibc floor + if: runner.os == 'Linux' + env: + GLIBC_FLOOR: GLIBC_2.28 + run: | + highest=$(objdump -T build/libbuildmonitor_ui.so | grep -o 'GLIBC_[0-9][0-9.]*' | sort -uV | tail -n 1) + echo "Highest glibc symbol version required: $highest" + if [ "$(printf '%s\n' "$GLIBC_FLOOR" "$highest" | sort -V | tail -n 1)" != "$GLIBC_FLOOR" ]; then + echo "::error::libbuildmonitor_ui.so requires $highest, above the $GLIBC_FLOOR floor" + exit 1 + fi + # libGL.so.1 is on every machine with a GL driver; libOpenGL.so.0 is not + if objdump -p build/libbuildmonitor_ui.so | grep -q 'NEEDED.*libOpenGL'; then + echo "::error::libbuildmonitor_ui.so links libOpenGL.so.0 rather than libGL.so.1" + exit 1 + fi + + # Everything recorded as needed has to be on a machine for the library to load at all, so + # the list is held to what any machine that runs .NET and has a display already has. The + # container's linker records every library it is handed, used or not, and raylib hands on + # libSM and libICE, which a stock Ubuntu lacks. native/CMakeLists.txt links with --as-needed + # so that neither is recorded, and this is what notices the list growing by any other route. + - name: Check needed libraries + if: runner.os == 'Linux' + env: + ALLOWED: >- + libc.so.6 libm.so.6 libdl.so.2 libpthread.so.0 librt.so.1 libstdc++.so.6 libgcc_s.so.1 + libX11.so.6 libGL.so.1 ld-linux-x86-64.so.2 ld-linux-aarch64.so.1 + run: | + needed=$(objdump -p build/libbuildmonitor_ui.so | awk '$1 == "NEEDED" { print $2 }') + echo "Needed:" $needed + failed=0 + for library in $needed; do + case " $ALLOWED " in + *" $library "*) ;; + *) + echo "::error::libbuildmonitor_ui.so needs $library, which is not in the list of libraries it may need" + failed=1 + ;; + esac + done + exit $failed # macOS draws with AppKit and Core Text rather than raylib and ImGui, so it is a Swift # package rather than a CMake project. Both --arch flags in one invocation produce a @@ -91,7 +128,7 @@ jobs: } case "${{ matrix.rid }}" in linux-*) - strip build/libbuildmonitor_ui.so + # Already stripped, inside the container that owns the build directory collect BuildMonitor.Native "${{ matrix.rid }}" build/libbuildmonitor_ui.so ;; osx) diff --git a/claude.md b/claude.md index be7a559..589d9cd 100644 --- a/claude.md +++ b/claude.md @@ -61,7 +61,7 @@ BuildMonitor is a tray app that polls CI services and shows one row per pipeline - `Auth/` — OAuth browser flow (PKCE + loopback listener), device flow, and per-OS secret stores. Secrets never go in settings.json. - `Protocol/` — the loopback socket the tray binds (single-instance gate) and the line protocol the launcher and the MCP server use to talk to it. - `src/BuildMonitor.Windows/` — WinForms head (`BuildMonitor.Tray.exe`): `NotifyIcon` tray, owner-drawn rows, real controls for form pages. -- `src/BuildMonitor.Native/` — macOS and Linux head (`BuildMonitor.Tray`) over the C ABI in `native/include/bm.h`; Swift/AppKit on macOS (`native/swift`), raylib + Dear ImGui on Linux (`native/`). Binaries are committed under `runtimes/{rid}/native` and regenerated by the `build-native` workflow, which opens a PR. The Linux tray is StatusNotifierItem over D-Bus in managed code. +- `src/BuildMonitor.Native/` — macOS and Linux head (`BuildMonitor.Tray`) over the C ABI in `native/include/bm.h`; Swift/AppKit on macOS (`native/swift`), raylib + Dear ImGui on Linux (`native/`). Binaries are committed under `runtimes/{rid}/native` and regenerated by the `build-native` workflow, which opens a PR. The Linux library is built in a `manylinux_2_28` container (`native/build-linux.sh`), so it needs no glibc newer than 2.28: built on the runner's own Ubuntu 24.04 it needed 2.38, and would not load on Ubuntu 22.04 or Debian 12. It is linked with `--as-needed`, since that container's linker otherwise records libSM and libICE, which a stock Ubuntu lacks, and `build-native` fails a build above the floor or one whose `NEEDED` entries are outside a short list. The Linux tray is StatusNotifierItem over D-Bus in managed code. - `src/BuildMonitor/` — the one package (`dotnet tool install -g BuildMonitor`). A launcher that starts the head for the current RID from `heads/{rid}/`, plus the `mcp` (stdio MCP server) and `status` verbs. It is `WinExe` so the tool shim is GUI subsystem and can sit in the Run-at-login key. Set `BuildMonitor_Head` to a head executable to run a local build through it. - Heads are bundled, never packaged on their own. diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 1c24e7e..2f78dd4 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -62,6 +62,16 @@ target_include_directories(buildmonitor_ui PRIVATE include ${imgui_SOURCE_DIR}) target_compile_definitions(buildmonitor_ui PRIVATE IMGUI_USE_WCHAR32) target_link_libraries(buildmonitor_ui PRIVATE raylib) +# Only the libraries something is actually called in are recorded as needed. raylib hands on +# everything CMake's FindX11 found, which includes libSM and libICE wherever their headers are +# installed, and nothing here or in raylib calls either. A library recorded as needed has to be +# on the machine for this one to load at all, and a stock Ubuntu has neither of those two. +# Ubuntu's own compiler passes this by default, so a library built on the runner never recorded +# them. The manylinux container build-native builds in does not pass it. +if(UNIX AND NOT APPLE) + target_link_options(buildmonitor_ui PRIVATE LINKER:--as-needed) +endif() + # The managed side probes for a bare name; keep it identical on every platform apart from the # platform's own prefix and extension. set_target_properties(buildmonitor_ui PROPERTIES diff --git a/native/build-linux.sh b/native/build-linux.sh new file mode 100644 index 0000000..df9e2f2 --- /dev/null +++ b/native/build-linux.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Builds build/libbuildmonitor_ui.so inside a manylinux_2_28 container, whose glibc 2.28 is the +# floor the library then needs - see the Linux build step in .github/workflows/build-native.yml. +# Runs from the repository root: +# +# docker run --rm -v "$PWD:/src" -w /src "quay.io/pypa/manylinux_2_28_$(uname -m)" bash native/build-linux.sh +set -euo pipefail + +# What raylib's bundled GLFW builds against, for both its X11 and Wayland backends: what the +# runner used to install for it, under this distribution's names. +dnf install -y \ + git pkgconfig \ + libX11-devel libXext-devel libXrandr-devel libXi-devel libXcursor-devel libXinerama-devel \ + mesa-libGL-devel wayland-devel libxkbcommon-devel + +# native/CMakeLists.txt needs CMake 3.24, newer than the distribution's own. The image carries +# current releases through pipx, so these are only installed where it does not. +export PATH="$HOME/.local/bin:$PATH" +for tool in cmake ninja; do + if ! command -v "$tool" > /dev/null 2>&1; then + pipx install "$tool" + fi +done + +# LEGACY links libGL.so.1, which every GL driver ships. This distribution's CMake otherwise +# prefers the GLVND split and links libOpenGL.so.0, which a machine with only libgl1 lacks. +cmake -S native -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOpenGL_GL_PREFERENCE=LEGACY +cmake --build build --config Release + +# Here rather than in the workflow's collect step: the build directory belongs to this +# container's root, and the runner cannot rewrite what is in it. +strip build/libbuildmonitor_ui.so diff --git a/src/BuildMonitor.Native/runtimes/linux-arm64/native/libbuildmonitor_ui.so b/src/BuildMonitor.Native/runtimes/linux-arm64/native/libbuildmonitor_ui.so index e782643..2100afe 100644 Binary files a/src/BuildMonitor.Native/runtimes/linux-arm64/native/libbuildmonitor_ui.so and b/src/BuildMonitor.Native/runtimes/linux-arm64/native/libbuildmonitor_ui.so differ diff --git a/src/BuildMonitor.Native/runtimes/linux-x64/native/libbuildmonitor_ui.so b/src/BuildMonitor.Native/runtimes/linux-x64/native/libbuildmonitor_ui.so index 5e7d792..d7e356b 100644 Binary files a/src/BuildMonitor.Native/runtimes/linux-x64/native/libbuildmonitor_ui.so and b/src/BuildMonitor.Native/runtimes/linux-x64/native/libbuildmonitor_ui.so differ