Skip to content

feat(cursor): 第六个成员 libwayland-cursor——客户端自己的鼠标指针 (#2) #26

feat(cursor): 第六个成员 libwayland-cursor——客户端自己的鼠标指针 (#2)

feat(cursor): 第六个成员 libwayland-cursor——客户端自己的鼠标指针 (#2) #26

Workflow file for this run

# 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