feat(cursor): 第六个成员 libwayland-cursor——客户端自己的鼠标指针 (#2) #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build every member on BOTH toolchains, and check what a green compile would | |
| # not: that the generator is ours, that the sonames are canonical, that the two | |
| # libraries do not overlap, and that every module produced an interface. | |
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| pull_request: | |
| workflow_dispatch: | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Both, because the module wrappers are where the two compilers differ | |
| # most: a 300-entry `using ::name;` export block is exactly the shape | |
| # that finds a disagreement about what may be named in an export. | |
| toolchain: [default, llvm] | |
| name: build (${{ matrix.toolchain }}) | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Installed the way a user does, through xlings, rather than by | |
| # unpacking an mcpp release: these packages depend on the ECOSYSTEM | |
| # (`xim:mesa`'s GBM discovery row, the toolchains), and a pinned mcpp | |
| # tarball carries a frozen snapshot of it. Testing against the current | |
| # ecosystem is the point. | |
| # | |
| # XLINGS_NON_INTERACTIVE is what makes the installer usable here. Without | |
| # it the script takes its `curl | bash` branch — `xlings self install < | |
| # /dev/tty` — and a runner has a /dev/tty that is readable but not | |
| # connected, so it dies with "No such device or address" before anything | |
| # is installed. | |
| # | |
| # XLINGS_VERSION PINS THE INSTALLER. Unpinned, this step installed | |
| # whatever was newest that morning, so a red run could never be told | |
| # apart from an ecosystem change — the one thing a fork's CI exists to | |
| # rule out. The installer reads `${1:-${XLINGS_VERSION:-}}`, so the | |
| # environment form pins it without changing the `curl | bash` shape. | |
| # | |
| # mcpp itself stays UNPINNED on purpose, and that is not an oversight: | |
| # these packages depend on the ECOSYSTEM (`xim:mesa`'s GBM/EGL discovery | |
| # rows, the toolchains), and a pinned mcpp tarball carries a frozen | |
| # snapshot of it. Pinning the tool that installs, floating the ecosystem | |
| # under test, is the split that makes a failure attributable. | |
| - name: Install xlings + mcpp | |
| env: | |
| XLINGS_NON_INTERACTIVE: "1" | |
| XLINGS_VERSION: "2026.8.27.5" | |
| run: | | |
| curl -fsSL https://d2learn.org/xlings-install.sh | bash | |
| echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" | |
| "$HOME/.xlings/bin/xlings" --version | |
| "$HOME/.xlings/bin/xlings" install mcpp | |
| echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" | |
| - name: Select toolchain | |
| if: matrix.toolchain != 'default' | |
| run: | | |
| mcpp toolchain install ${{ matrix.toolchain }} | |
| mcpp toolchain default ${{ matrix.toolchain }} | |
| mcpp toolchain list | |
| - name: Build the workspace | |
| run: | | |
| mcpp --version | |
| mcpp build --workspace | |
| - name: The macro mappings actually run | |
| run: mcpp test -p mcpp/util | |
| # Artifacts are located by searching the tree rather than by assuming | |
| # `mcpp/<member>/target/...`: where a workspace build puts its output is | |
| # mcpp's business and has moved before. On a miss the step prints what it | |
| # DID find, so the next failure is diagnosable from the log alone. | |
| - name: wayland-scanner is ours, and is 1.26.0 | |
| run: | | |
| scanner=$(find . -name wayland-scanner -type f -perm -u+x | head -1) | |
| if [ -z "$scanner" ]; then | |
| echo "::error::no wayland-scanner binary anywhere in the tree" | |
| find . -path ./upstream -prune -o -name 'target' -type d -print | |
| exit 1 | |
| fi | |
| echo "found: $scanner" | |
| # --version goes to STDERR, so the pipe needs 2>&1 or grep reads nothing | |
| # and the check fails on a scanner that is perfectly fine. | |
| "$scanner" --version 2>&1 | |
| "$scanner" --version 2>&1 | grep -qx 'wayland-scanner 1.26.0' | |
| - name: the generated protocol code came from THIS scanner | |
| run: | | |
| scanner=$(find . -name wayland-scanner -type f -perm -u+x | head -1) | |
| "$scanner" -s public-code upstream/protocol/wayland.xml /tmp/wayland-protocol.c | |
| head -1 /tmp/wayland-protocol.c | |
| grep -q 'Generated by wayland-scanner 1.26.0' /tmp/wayland-protocol.c | |
| - name: every library carries its canonical SONAME | |
| run: | | |
| # `${want%.*}` strips ONE trailing version component, whatever it is: | |
| # the built file is the unversioned `lib*.so` and the SONAME is what | |
| # `readelf -d` reports. The earlier `${want%.0}` form only worked | |
| # because both sonames happened to end in `.0` — libwayland-egl's | |
| # ends in `.1` and would have been searched for by its full name. | |
| for pair in "client:libwayland-client.so.0" \ | |
| "server:libwayland-server.so.0" \ | |
| "egl:libwayland-egl.so.1" \ | |
| "cursor:libwayland-cursor.so.0"; do | |
| member=${pair%%:*}; want=${pair#*:} | |
| so=$(find . -name "${want%.*}" -type f | head -1) | |
| test -n "$so" || { echo "$member: no library built"; exit 1; } | |
| got=$(readelf -d "$so" | sed -n 's/.*SONAME.*\[\(.*\)\]/\1/p') | |
| echo "$member -> $got" | |
| test "$got" = "$want" || { echo "expected $want"; exit 1; } | |
| done | |
| - name: the two libraries do not overlap | |
| run: | | |
| # Mesa's libEGL_mesa has DT_NEEDED on both; if one carried the other's | |
| # API the process would bind to whichever loaded first. | |
| # | |
| # Written with `if`, not `grep -q … && { exit 1; }`: under `bash -e` | |
| # that compound returns non-zero exactly when grep finds NOTHING — | |
| # which is the outcome we want — and the step fails on success. | |
| c=$(find . -name libwayland-client.so -type f | head -1) | |
| s=$(find . -name libwayland-server.so -type f | head -1) | |
| test -n "$c" && test -n "$s" || { echo "::error::a library is missing"; exit 1; } | |
| has() { readelf --dyn-syms -W "$1" | grep -q " $2$"; } | |
| has "$c" wl_display_connect || { echo "::error::client lost wl_display_connect"; exit 1; } | |
| has "$s" wl_display_create || { echo "::error::server lost wl_display_create"; exit 1; } | |
| if has "$c" wl_display_create; then | |
| echo "::error::libwayland-client exports the SERVER api (wl_display_create)"; exit 1 | |
| fi | |
| if has "$s" wl_display_connect; then | |
| echo "::error::libwayland-server exports the CLIENT api (wl_display_connect)"; exit 1 | |
| fi | |
| echo "client and server APIs are disjoint" | |
| - name: libwayland-egl carries its four functions and nothing else's | |
| run: | | |
| # The whole public surface, named one by one. It is four entries, so | |
| # a table beats a count: a count would still pass if one function | |
| # were replaced by another. | |
| # | |
| # Mesa's libEGL dlopens nothing here — it carries DT_NEEDED on | |
| # `libwayland-egl.so.1` by name — so a missing symbol surfaces as a | |
| # link failure in a client, far from this repo. | |
| e=$(find . -name libwayland-egl.so -type f | head -1) | |
| test -n "$e" || { echo "::error::libwayland-egl was not built"; exit 1; } | |
| has() { readelf --dyn-syms -W "$1" | grep -q " $2$"; } | |
| for f in wl_egl_window_create wl_egl_window_destroy \ | |
| wl_egl_window_resize wl_egl_window_get_attached_size; do | |
| has "$e" "$f" || { echo "::error::libwayland-egl lost $f"; exit 1; } | |
| done | |
| # It must NOT carry the client's API. wayland-egl.c calls no client | |
| # function, so if `wl_display_connect` shows up here the member has | |
| # started linking libwayland-client's objects in — the same | |
| # overlap the check above forbids between client and server. | |
| if has "$e" wl_display_connect; then | |
| echo "::error::libwayland-egl exports the CLIENT api (wl_display_connect)"; exit 1 | |
| fi | |
| echo "libwayland-egl exports exactly its own four functions" | |
| - name: libwayland-cursor carries its six functions and no host paths | |
| run: | | |
| c=$(find . -name libwayland-cursor.so -type f | head -1) | |
| test -n "$c" || { echo "::error::libwayland-cursor was not built"; exit 1; } | |
| has() { readelf --dyn-syms -W "$1" | grep -q " $2$"; } | |
| for f in wl_cursor_theme_load wl_cursor_theme_destroy \ | |
| wl_cursor_theme_get_cursor wl_cursor_image_get_buffer \ | |
| wl_cursor_frame wl_cursor_frame_and_duration; do | |
| has "$c" "$f" || { echo "::error::libwayland-cursor lost $f"; exit 1; } | |
| done | |
| # It DOES link the client, unlike libwayland-egl: wayland-cursor.c | |
| # calls wl_shm_create_pool and friends. A missing DT_NEEDED here would | |
| # mean those calls were satisfied from somewhere else. | |
| readelf -d "$c" | grep -q 'libwayland-client.so.0' || { | |
| echo "::error::libwayland-cursor does not link libwayland-client"; exit 1; } | |
| # And the compiled-in HOST cursor paths are gone. xcursor.c:493 bakes | |
| # in "~/.icons:/usr/share/icons:/usr/share/pixmaps:..." unless | |
| # XCURSORPATH is defined, and after relocation those name the host's | |
| # themes. The recipe empties it so a theme comes from XCURSOR_PATH or | |
| # not at all — and grepping the binary is the only way to see that it | |
| # worked, because a wrong value here fails by SILENTLY WORKING on a | |
| # machine that happens to have themes in /usr/share/icons. | |
| if strings "$c" | grep -qE '/usr/share/icons|/usr/X11R6|xorg-x11'; then | |
| echo "::error::a host cursor path was compiled into libwayland-cursor" | |
| strings "$c" | grep -E '/usr/share/icons|/usr/X11R6|xorg-x11' | |
| exit 1 | |
| fi | |
| echo "libwayland-cursor: six functions, links the client, no host paths" | |
| - name: mcpp/generated/ matches what the scanner produces | |
| run: | | |
| # The generated protocol code is checked in because it is the | |
| # package's PUBLIC interface (wayland-client.h includes it), so a | |
| # build-time include dir cannot carry it. Checked in means it can | |
| # drift; this is what stops it. | |
| scanner=$(find . -name wayland-scanner -type f -perm -u+x | head -1) | |
| X=upstream/protocol/wayland.xml | |
| mkdir -p /tmp/regen | |
| "$scanner" -s public-code "$X" /tmp/regen/wayland-protocol.c | |
| for side in client server; do | |
| "$scanner" -s $side-header "$X" /tmp/regen/wayland-$side-protocol.h | |
| "$scanner" -s $side-header -c "$X" /tmp/regen/wayland-$side-protocol-core.h | |
| done | |
| for f in /tmp/regen/*; do | |
| n=$(basename "$f") | |
| if ! diff -q "$f" "mcpp/generated/$n" >/dev/null; then | |
| echo "::error file=mcpp/generated/$n::out of date — regenerate it (see mcpp/generated/README.md)" | |
| diff "$f" "mcpp/generated/$n" | head -20 | |
| exit 1 | |
| fi | |
| done | |
| echo "generated code matches wayland.xml and this scanner" | |
| - name: the module wrappers match the generator | |
| run: | | |
| # The .cppm files and the -module.h copies are generated from the | |
| # public headers. Checked in, so they can drift; this stops it. | |
| python3 mcpp/tools/genmod.py . | |
| if ! git diff --quiet; then | |
| echo "::error::module wrappers are out of date — run mcpp/tools/genmod.py ." | |
| git diff --stat | |
| exit 1 | |
| fi | |
| echo "module wrappers match" | |
| - name: every module produced an interface | |
| run: | | |
| for m in freedesktop.wayland.client freedesktop.wayland.server freedesktop.wayland.util; do | |
| find . \( -name "$m.gcm" -o -name "$m.pcm" \) | head -1 | grep -q . \ | |
| || { echo "::error::$m has no module interface"; find . -name '*.gcm' -o -name '*.pcm'; exit 1; } | |
| echo "$m ok" | |
| done | |
| echo "freedesktop.wayland.client, freedesktop.wayland.server and freedesktop.wayland.util all have interfaces" | |
| upstream: | |
| # `upstream/` must be the release tarball, byte for byte. | |
| # | |
| # Checked by COMPARING, not by building: this repository builds one way, | |
| # through mcpp, and keeping a second build system alive just to assert | |
| # something would be a second thing to maintain and a second thing to | |
| # disagree with the first. Comparing is also the stronger check — meson | |
| # succeeding proves the tree still builds, not that nothing was edited. | |
| name: upstream/ is the release tarball, unmodified | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: diff upstream/ against wayland 1.26.0 | |
| run: | | |
| curl -L -fsS -o wayland.tar.xz \ | |
| "https://gitlab.freedesktop.org/wayland/wayland/-/releases/1.26.0/downloads/wayland-1.26.0.tar.xz" | |
| echo "64176eaa46e4969903e286f8e5ef8331affc17fdf03ac9b58381d2b23162b7a3 wayland.tar.xz" | sha256sum -c - | |
| mkdir -p /tmp/pristine | |
| tar -xJf wayland.tar.xz -C /tmp/pristine | |
| # .gitignore lives at the repo root here, not inside upstream/ — it is | |
| # this repository's, covering mcpp's build output. It is the one | |
| # expected difference. | |
| if diff -r --exclude=.gitignore /tmp/pristine/wayland-1.26.0 upstream; then | |
| echo "upstream/ is pristine" | |
| else | |
| echo "::error::upstream/ differs from the wayland 1.26.0 release tarball." | |
| echo "Everything this fork adds belongs under mcpp/." | |
| exit 1 | |
| fi |