Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 143 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
# (which ships clang-format-18 by default).
#
# Top-level jobs (lint, build, debug-build, sanitize, motif, osiris,
# xfig, xwpe, xephem, gimp, gtk1, cinepaint, xmms, grace, sdl3) run in
# parallel. The sdl3 job builds the
# xfig, xwpe, xephem, gimp, gtk1, cinepaint, xmms, grace, sdl3,
# wayland) run in parallel. The sdl3 job builds the
# compat stack against the native SDL3 backend (SDL_BACKEND=sdl3); every
# other job uses the default SDL2 backend, so the SDL3 translation shim in
# src/sdl-compat.h is only exercised there. Osiris and Xfig each have no Motif
# src/sdl-compat.h is only exercised there and in the wayland job. The
# wayland job additionally builds a Wayland-enabled SDL3 and runs the compat
# stack against weston headless, the only job that exercises compositor-
# dependent behaviour such as the override-redirect xdg_popup path. Osiris and Xfig each have no Motif
# dependency, so they live in their own jobs rather than queueing behind
# the Motif + ViolaWWW + Mosaic chain. GIMP 0.54 is a Motif client but
# gets its own job too: its plug-in build pulls in libpng/jpeg/tiff that
Expand Down Expand Up @@ -44,7 +47,7 @@ concurrency:

env:
COMMON_BUILD_PKGS: "clang ccache make pkg-config python3 libsdl2-dev libsdl2-ttf-dev libpixman-1-dev"
MOTIF_BUILD_PKGS: "autoconf automake libtool bison flex gawk m4"
MOTIF_BUILD_PKGS: "autoconf automake libtool bison flex gawk m4 libfontconfig1-dev libxft-dev"
# Osiris (Qt 2.3.2 fork) configures with Meson and builds with Ninja,
# and Osiris itself hard-depends on libjpeg, libpng, and freetype for
# image loading + font rendering (the xft/opengl/sm probes are gated
Expand Down Expand Up @@ -2139,3 +2142,139 @@ jobs:
- name: ccache stats
if: ${{ !cancelled() }}
run: ccache --show-stats

# ---- Wayland backend regression ----
# The only job that exercises the SDL3 backend against a real Wayland
# compositor. Some behaviour is observable only there: an override-redirect
# popup must map as a Wayland xdg_popup anchored to its parent rather than a
# free-standing xdg_toplevel, and that is compositor-dependent, so neither the
# SDL2 jobs nor the sdl3 job (console-only SDL3, dummy driver) can see it. This
# job builds a Wayland-enabled SDL3 (the sdl3 job's SDL3 is built
# -DSDL_UNIX_CONSOLE_BUILD=ON with no windowing backend, so its cached prefix
# cannot be reused here), runs weston headless, and drives the Wayland-backend
# regression checks; the menu-rendering harness is the current content, and new
# compositor-dependent checks belong here too. weston headless needs only
# XDG_RUNTIME_DIR, no GPU or logind seat.
wayland:
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
SDL3_PREFIX: ${{ github.workspace }}/.sdl3-wayland-prefix
# Same bison yacc-compat shim the motif job uses for the Mrm parser
# (ubuntu ships bison but no /usr/bin/yacc).
MOTIF_YACC: "bison -y"
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install build dependencies
# SDL3-from-source deps (cmake/ninja, freetype/harfbuzz for SDL3_ttf) as
# in the sdl3 job, plus the Wayland dev set so SDL3 builds its Wayland
# video driver and weston can run headless, plus the Motif autoreconf
# chain for the --with-xft libXm the font and menu checks link. libx11-dev
# supplies the few system X11 private headers the staged libXt/libX11
# sources fall through to (this job has no libsdl2-dev to pull them in).
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang ccache make pkg-config python3 libpixman-1-dev \
cmake ninja-build git libfreetype-dev libharfbuzz-dev libx11-dev \
weston libwayland-dev wayland-protocols libxkbcommon-dev libdecor-0-dev \
libegl-dev libgles-dev \
${{ env.MOTIF_BUILD_PKGS }}

- name: Cache Wayland SDL3 + SDL3_ttf prefix
id: sdl3-cache
# Exact-match key bound to the build script (versions + SHA-256 + flags
# all live there), so bumping SDL3 or its build options invalidates the
# cache without a hand-maintained version string, and no restore-keys
# fallback can resurrect a prefix built with different flags.
uses: actions/cache@v6
with:
path: ${{ env.SDL3_PREFIX }}
key: sdl3-wayland-prefix-v1-${{ runner.os }}-${{ hashFiles('scripts/build-sdl3.sh') }}

- name: Build Wayland-enabled SDL3 + SDL3_ttf
if: steps.sdl3-cache.outputs.cache-hit != 'true'
run: bash scripts/build-sdl3.sh

- name: Assert SDL3 built with the Wayland driver
# SDL's CMake disables a backend with missing deps instead of failing, so
# verify the Wayland driver is present up front; otherwise every popup
# check would fail later for a mysterious reason. SDL loads libwayland
# via dlopen, so it does not appear in ldd; grep the compiled-in driver
# description string instead (absent from a console-only SDL3 build).
run: |
strings "$SDL3_PREFIX"/lib*/libSDL3.so | grep -q "SDL Wayland video driver" \
|| { echo "SDL3 was built without the Wayland video driver"; exit 1; }

- name: Cache upstream tarballs and extracted source/headers
# Shared with the other compile jobs (content-hashed, CFLAGS-insensitive),
# with the Motif subtree excluded so this job neither poisons nor reuses
# the app-source caches.
uses: actions/cache@v6
with:
path: |
build/upstream
!build/upstream/**/*.o
!build/upstream/**/*.d
!build/upstream/motif
!build/upstream/mosaic
!build/upstream/osiris
!build/upstream/xfig-*
!build/upstream/.cache/xfig-*
!build/upstream/gimp-*
!build/upstream/.cache/gimp-*
key: upstream-src-v2-${{ runner.os }}-${{ hashFiles('scripts/sync-upstream-headers.py') }}
restore-keys: |
upstream-src-v2-${{ runner.os }}-

- name: Cache Motif source clone and autoreconf output
uses: actions/cache@v6
with:
path: build/upstream/motif
key: motif-src-${{ runner.os }}-${{ hashFiles('mk/motif.mk', 'compat/motif-patches/**') }}

- name: Cache ccache
uses: actions/cache@v6
with:
path: ~/.cache/ccache
key: ccache-wayland-v1-${{ runner.os }}-${{ github.sha }}
restore-keys: |
ccache-wayland-v1-${{ runner.os }}-

- name: Configure ccache and SDL3 environment
run: |
ccache --max-size=600M
ccache --zero-stats
{
echo "CC=ccache clang"
echo "PKG_CONFIG_PATH=$SDL3_PREFIX/lib/pkgconfig:$SDL3_PREFIX/lib64/pkgconfig"
echo "LD_LIBRARY_PATH=$SDL3_PREFIX/lib:$SDL3_PREFIX/lib64"
} >>"$GITHUB_ENV"

- name: Build compat stack (SDL_BACKEND=sdl3) and Motif --with-xft
run: |
make SDL_BACKEND=sdl3 -j"$(nproc)"
make SDL_BACKEND=sdl3 motif -j"$(nproc)"

- name: Run Wayland backend regression (weston headless)
# CI_REQUIRE=1 turns any SKIP into a failure so a missing weston / SDL3
# cannot let this required gate pass green without exercising anything.
run: |
export XDG_RUNTIME_DIR="$(mktemp -d)"; chmod 700 "$XDG_RUNTIME_DIR"
CI_REQUIRE=1 MENUVAL_WORK="$GITHUB_WORKSPACE/build/wayland-logs" \
bash scripts/validate-menu-rendering.sh

- name: Upload weston + WAYLAND_DEBUG logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: wayland-logs
path: build/wayland-logs
if-no-files-found: warn
retention-days: 7

- name: ccache stats
if: ${{ !cancelled() }}
run: ccache --show-stats
15 changes: 15 additions & 0 deletions include/X11/Intrinsic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef LIBX11_COMPAT_INTRINSIC_WRAPPER_H
#define LIBX11_COMPAT_INTRINSIC_WRAPPER_H

/* Some Motif headers default to compact resource-name tables whose offsets are
* tied to the libXm they were built with. Compat clients link against the
* system libXm, so force Motif's literal resource-name macros before
* <Xm/XmStrDefs.h> sees <X11/Intrinsic.h>.
*/
#ifndef XMSTRINGDEFINES
#define XMSTRINGDEFINES 1
#endif

#include_next <X11/Intrinsic.h>

#endif
2 changes: 2 additions & 0 deletions include/X11/Xft/Xft.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <fontconfig/fontconfig.h>

typedef unsigned int FT_UInt;

/* FcEndian is a fontconfig type. When the host fontconfig header is picked up
* alongside this shim (Xfig pkg-configs the host fontconfig even when our
* libXft-compat resolves the Fc* / Xft* surface) the upstream definition wins.
Expand Down Expand Up @@ -141,6 +142,7 @@ XftDraw *XftDrawCreate(Display *dpy,
Drawable drawable,
Visual *visual,
Colormap colormap);
XftDraw *XftDrawCreateBitmap(Display *dpy, Pixmap bitmap);
void XftDrawDestroy(XftDraw *draw);
void XftDrawChange(XftDraw *draw, Drawable drawable);
Bool XftDrawSetClipRectangles(XftDraw *draw,
Expand Down
2 changes: 2 additions & 0 deletions include/fontconfig/fontconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ FcBool FcPatternRemove(FcPattern *pattern, const char *object, int n);
int FcUcs4ToUtf8(FcChar32 ucs4, FcChar8 dest[FC_UTF8_MAX_LEN]);
int FcUtf8ToUcs4(const FcChar8 *src, FcChar32 *dst, int len);
FcPattern *FcNameParse(const FcChar8 *name);
FcBool FcInit(void);
void FcFini(void);
FcBool FcConfigSubstitute(FcConfig *config, FcPattern *pattern, int kind);
void FcDefaultSubstitute(FcPattern *pattern);
FcPattern *FcFontMatch(FcConfig *config, FcPattern *pattern, FcResult *result);
Expand Down
41 changes: 24 additions & 17 deletions mk/motif.mk
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MOTIF_COMMIT := 1f62cee9d8b8bae6a55c8e1dec1c4e8f1ace1d2f
MOTIF_COMMIT := df9f87bbb9f0f54ab54e97481b9b01232311543c
MOTIF_URL := https://github.com/thentenaar/motif
MOTIF_SRC_DIR := $(OUT)/upstream/motif
MOTIF_SRC_STAMP := $(MOTIF_SRC_DIR)/.source-stamp
Expand Down Expand Up @@ -136,7 +136,8 @@ MOTIF_COMMON_CONFIGURE_FLAGS := \
--prefix=$(abspath $(OUT)/motif-install) \
$(MOTIF_GLW_FLAG) \
--disable-tests \
--without-xft \
--disable-dependency-tracking \
--with-xft \
--with-jpeg=no \
--with-png=no \
--with-xrandr=no \
Expand Down Expand Up @@ -193,7 +194,7 @@ $(MOTIF_DEMOS_BUILD_DIR):
@mkdir -p $@

$(MOTIF_CONFIG_STAMP): $(MOTIF_AUTOGEN_STAMP) $(PKGCONFIG_FILES) \
$(MOTIF_GL_PKGCONFIG) $(TARGET) $(LIBXT_TARGET) $(LIBXPM_TARGET) $(XEXT_COMPAT_TARGET) $(XMU_COMPAT_TARGET) $(XINERAMA_COMPAT_TARGET) | $(MOTIF_BUILD_DIR)
$(MOTIF_GL_PKGCONFIG) $(TARGET) $(LIBXT_TARGET) $(LIBXPM_TARGET) $(XEXT_COMPAT_TARGET) $(XMU_COMPAT_TARGET) $(XINERAMA_COMPAT_TARGET) $(XFT_COMPAT_TARGET) | $(MOTIF_BUILD_DIR)
@echo " CONFIG motif"
$(Q)cd $(MOTIF_BUILD_DIR) && \
$(motif_runtime_env) \
Expand Down Expand Up @@ -221,7 +222,7 @@ $(MOTIF_BUILD_STAMP): $(MOTIF_CONFIG_STAMP)
$(Q)touch $@

$(MOTIF_DEMOS_CONFIG_STAMP): $(MOTIF_AUTOGEN_STAMP) $(PKGCONFIG_FILES) \
$(MOTIF_GL_PKGCONFIG) $(TARGET) $(LIBXT_TARGET) $(LIBXPM_TARGET) $(XEXT_COMPAT_TARGET) $(XMU_COMPAT_TARGET) $(XINERAMA_COMPAT_TARGET) | $(MOTIF_DEMOS_BUILD_DIR)
$(MOTIF_GL_PKGCONFIG) $(TARGET) $(LIBXT_TARGET) $(LIBXPM_TARGET) $(XEXT_COMPAT_TARGET) $(XMU_COMPAT_TARGET) $(XINERAMA_COMPAT_TARGET) $(XFT_COMPAT_TARGET) | $(MOTIF_DEMOS_BUILD_DIR)
@echo " CONFIG motif demos"
$(Q)cd $(MOTIF_DEMOS_BUILD_DIR) && \
$(motif_runtime_env) \
Expand Down Expand Up @@ -335,24 +336,29 @@ MOTIF_DIFF_JOBS ?= 1
MOTIF_DIFF_FILTER ?=
MOTIF_DIFF_INSTALL_DEPS ?= 0
MOTIF_DIFF_LOCAL ?= 0

# Thresholds calibrated to the GitHub Actions ubuntu-24.04 runner with
# xfonts-base / 100dpi / 75dpi / scalable installed. The system-side
# Motif Xm widget defaults differ from the libx11-compat-built Xm in a
# handful of representative demos, all real library behavior gaps to
# chase in follow-up PRs rather than CI-side regressions.
# xfonts-base / 100dpi / 75dpi / scalable installed. Both sides now build
# Motif --with-xft: the reference side renders through the system libXft /
# freetype / fontconfig, the compat side through the in-tree Xft over
# SDL_ttf. The remaining gap is a real library-behavior difference to chase
# in follow-up PRs, not a CI-side regression.
#
# MAE is 0.11, which the bulk of the representative set clears with margin.
# The two captures that cannot clear it are ColorSel and Ext18List: their
# native side renders helvetica from the adobe-helvetica BITMAP X font,
# which the compat SDL_ttf substitute cannot match at any width (measured
# ~0.133 / ~0.11 on CI, verified independent of SDL/freetype version, DPI,
# and probe-font choice). They are listed in MOTIF_DIFF_ALLOW_DIFF so the
# compare reports them as expected-diff instead of failing the gate; drop
# them from that list once the compat side can render bitmap helvetica.
# changed_ratio stays a coarse backstop because glyph antialiasing alone
# puts correct output at 0.15 to 0.20.
# The two captures that cannot clear it are ColorSel and Ext18List, both
# text-dense list widgets: SDL_ttf glyph rasterization and the in-tree
# fontconfig matcher do not land byte-identical to system freetype/Xft, so
# a screen full of small antialiased text accumulates a residual MAE
# (measured with both sides --with-xft: ColorSel 0.1375 / 0.307 changed,
# Ext18List 0.1211 / 0.283; the other representatives clear 0.11 with
# margin). They are listed in MOTIF_DIFF_ALLOW_DIFF so the compare reports
# them as expected-diff instead of failing the gate; tighten once the Xft
# text path matches system freetype more closely. changed_ratio stays a
# coarse backstop because glyph antialiasing alone puts correct output at
# 0.15 to 0.20.
MOTIF_DIFF_MAE_THRESHOLD ?= 0.11
MOTIF_DIFF_ALLOW_DIFF ?= ColorSel,Ext18List

# The allow-list exemption is bounded: an allow-listed capture that clears
# these ceilings is still failed, so a crash frame, a missing widget, or an
# unrelated break in ColorSel/Ext18List cannot hide behind expected-diff.
Expand Down Expand Up @@ -394,6 +400,7 @@ motif_ui_replay_env = \
--env LD_LIBRARY_PATH=$(abspath $(MOTIF_DEMOS_BUILD_DIR))/lib/Xm/.libs:$(abspath $(MOTIF_DEMOS_BUILD_DIR))/lib/Mrm/.libs:$(abspath $(MOTIF_DEMOS_BUILD_DIR))/clients/uil/.libs:$(abspath $(OUT))$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH}

.PHONY: check-differential-motif check-differential-motif-full check-demos-motif motif-demos-screenshots check-paperplane-macos check-paperplane-linux check-smoke-motif profile-ui FORCE

## Compare representative Motif demo screenshots for system libX11 vs
## libx11-compat. Set MOTIF_DIFF_LOCAL=1 to run on the current host (used
## by the GitHub Actions differential workflow); otherwise the script
Expand Down
12 changes: 8 additions & 4 deletions mk/toolchain.mk
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ MAKEFLAGS += --no-builtin-rules --no-builtin-variables

UNAME_S := $(shell uname -s)

# GNU make predefines CC=cc. Prefer clang for local builds while preserving
# explicit command-line and environment overrides.
# GNU make predefines CC=cc. Prefer clang when it is on PATH (the tree's
# primary toolchain and macOS's system compiler), but fall back to the system
# cc (gcc on a typical Linux host) when there is no clang, so a box without
# clang still builds without the caller having to pass CC=. Explicit
# command-line and environment overrides always win.
CC_DEFAULT := $(shell command -v clang >/dev/null 2>&1 && printf clang || printf cc)
ifeq ($(origin CC),default)
CC := clang
CC := $(CC_DEFAULT)
else
CC ?= clang
CC ?= $(CC_DEFAULT)
endif

PKG_CONFIG ?= pkg-config
Expand Down
24 changes: 22 additions & 2 deletions mk/xnedit.mk
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ XNEDIT_SYSROOT_STAMP := $(XNEDIT_SYSROOT)/.stamp

XNEDIT_RPATH_FLAGS :=
ifeq ($(UNAME_S),Linux)
XNEDIT_RPATH_FLAGS += -Wl,-rpath,$(abspath $(OUT)) \
# --disable-new-dtags emits DT_RPATH rather than DT_RUNPATH. DT_RUNPATH is not
# consulted when the loader resolves a dependency's own NEEDED entries, so with
# the default new dtags the binary finds libXm/libXt/libX11 but not their
# transitive deps (libXft-compat.so is NEEDED by libXm, not by xnedit), and the
# bare binary fails to start without LD_LIBRARY_PATH. DT_RPATH is searched for
# the whole dependency tree, so it resolves the transitive compat libs the way
# macOS @rpath already does, letting ./xnedit run with no LD_LIBRARY_PATH.
XNEDIT_RPATH_FLAGS += -Wl,--disable-new-dtags \
-Wl,-rpath,$(abspath $(OUT)) \
-Wl,-rpath,$(abspath $(XNEDIT_LIB_ALIASES)) \
-Wl,-rpath-link,$(abspath $(OUT))
endif
Expand Down Expand Up @@ -82,7 +90,8 @@ $(XNEDIT_LIB_ALIASES_STAMP): $(TARGET) $(LIBXT_TARGET) $(XEXT_COMPAT_TARGET) \
$(XFT_COMPAT_TARGET) $(MOTIF_STAGE_STAMP) mk/xnedit.mk
@mkdir -p $(XNEDIT_LIB_ALIASES)
$(Q)rm -f $(XNEDIT_LIB_ALIASES)/libX*.so $(XNEDIT_LIB_ALIASES)/libX*.dylib \
$(XNEDIT_LIB_ALIASES)/libXm.so $(XNEDIT_LIB_ALIASES)/libXm.dylib
$(XNEDIT_LIB_ALIASES)/libXm.so $(XNEDIT_LIB_ALIASES)/libXm.dylib \
$(XNEDIT_LIB_ALIASES)/libSDL2*.so
$(Q)set -e; for pair in \
libX11.so:libX11-compat.so \
libXt.so:libXt-compat.so \
Expand All @@ -93,6 +102,17 @@ $(XNEDIT_LIB_ALIASES_STAMP): $(TARGET) $(LIBXT_TARGET) $(XEXT_COMPAT_TARGET) \
alias="$${pair%%:*}"; target="$${pair##*:}"; \
ln -sf "$(abspath $(OUT))/$$target" "$(XNEDIT_LIB_ALIASES)/$$alias"; \
done
# The SDL2 shims are NEEDED by libX11-compat.so, which is loaded through the
# libX11.so alias symlink above, so its RUNPATH=$$ORIGIN resolves to this
# lib-aliases dir rather than $(OUT). Mirror the shims here too so DT_RPATH
# handoff to the shim resolves without LD_LIBRARY_PATH. Guarded on existence:
# the SDL3 backend links libSDL3 directly and ships no shim (also true on
# macOS), so there is nothing to alias there.
$(Q)set -e; for shim in libSDL2-x11compat.so libSDL2_ttf-x11compat.so; do \
if [ -e "$(abspath $(OUT))/$$shim" ]; then \
ln -sf "$(abspath $(OUT))/$$shim" "$(XNEDIT_LIB_ALIASES)/$$shim"; \
fi; \
done
ifeq ($(UNAME_S),Darwin)
$(Q)set -e; for pair in \
libX11.dylib:libX11-compat.so \
Expand Down
Loading
Loading