From fb88ba713f8689d5a051b7caed67f36320426123 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 30 Jul 2026 08:45:54 -0700 Subject: [PATCH 1/3] raylib: vendor Linux runtime libraries --- build.sh | 8 ++++++++ raylib/build_cffi.py | 32 +++++++++++++++++++++++++++++--- raylib/setup.py | 12 ++++++++++-- test_wheels_in_image.sh | 1 + 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/build.sh b/build.sh index fe03a50..ca60bbf 100755 --- a/build.sh +++ b/build.sh @@ -83,6 +83,14 @@ python3 build_versions.py apply "$POST_N" */pyproject.toml uv build --all-packages --wheel --out-dir dist --no-create-gitignore restore_build_versions +if [[ -n "${BUILD_SH_IN_MANYLINUX:-}" ]]; then + mkdir -p dist/unrepaired/ + mv dist/comma_deps_raylib-*.whl dist/unrepaired/ + auditwheel repair \ + --ldpaths "$ROOT_DIR/raylib/raylib/install/lib:/usr/lib64:/lib64" \ + --wheel-dir dist/ dist/unrepaired/comma_deps_raylib-*.whl +fi + if [[ -n "${BUILD_SH_IN_MANYLINUX:-}" ]]; then VENV_DIR="$ROOT_DIR/.venv-manylinux" else diff --git a/raylib/build_cffi.py b/raylib/build_cffi.py index e0350f1..3f102bd 100644 --- a/raylib/build_cffi.py +++ b/raylib/build_cffi.py @@ -4,6 +4,7 @@ import os import platform import re +import shutil import subprocess import sys @@ -15,7 +16,7 @@ RAYLIB_LIB_PATH = os.path.join(PACKAGE_DIR, "install", "lib") sys.path.insert(0, PACKAGE_DIR) -from _backend import BACKEND_ARCHIVES, BACKEND_CFFI_MODULES, BACKEND_LINK_ARGS, detect_backend # noqa: E402 +from _backend import BACKEND_ARCHIVES, BACKEND_CFFI_MODULES, BACKEND_LINK_ARGS, DESKTOP, detect_backend # noqa: E402 from validate_bindings import validate_static_bindings # noqa: E402 RAYLIB_BACKEND = detect_backend() @@ -31,6 +32,26 @@ def _raylib_archive(): return archive +def _private_desktop_libs(): + private_libs = {} + for soname in ("libGL.so.1", "libGLX.so.0", "libX11.so.6", "libXext.so.6"): + source = subprocess.check_output(["gcc", f"-print-file-name={soname}"], text=True).strip() + if source == soname or not os.path.isfile(source): + raise FileNotFoundError(f"{soname} not found") + + private_soname = soname.replace(".so", "-comma.so", 1) + target = os.path.join(RAYLIB_LIB_PATH, private_soname) + shutil.copy2(source, target) + subprocess.check_call(["patchelf", "--set-soname", private_soname, target]) + private_libs[soname] = target + + for target in private_libs.values(): + for soname, dependency in private_libs.items(): + subprocess.check_call(["patchelf", "--replace-needed", soname, os.path.basename(dependency), target]) + + return private_libs["libGL.so.1"], private_libs["libX11.so.6"] + + def pre_process_header(filename, remove_function_bodies=False): print("Pre-processing " + filename) with open(filename, "r") as f: @@ -87,10 +108,15 @@ def build_ffi(): extra_compile_args = ["-Wno-error=incompatible-function-pointer-types"] else: print("BUILDING FOR LINUX") + backend_link_args = BACKEND_LINK_ARGS[RAYLIB_BACKEND] + if RAYLIB_BACKEND == DESKTOP and os.environ.get("AUDITWHEEL_PLAT"): + backend_link_args = _private_desktop_libs() + extra_link_args = [ raylib_archive, - '-lm', '-lpthread', '-lrt', '-ldl', '-latomic', - *BACKEND_LINK_ARGS[RAYLIB_BACKEND], + '-lm', '-lpthread', '-lrt', '-ldl', + '-Wl,-Bstatic', '-latomic', '-Wl,-Bdynamic', + *backend_link_args, ] extra_compile_args = ["-Wno-incompatible-pointer-types"] diff --git a/raylib/setup.py b/raylib/setup.py index 2768bed..c2ab743 100644 --- a/raylib/setup.py +++ b/raylib/setup.py @@ -5,7 +5,7 @@ import subprocess import sys -from setuptools import setup +from setuptools import Distribution, setup from setuptools.command.build_py import build_py from setuptools.command.bdist_wheel import bdist_wheel @@ -61,4 +61,12 @@ def get_tag(self): return "py3", "none", plat_tag -setup(cmdclass={"build_py": BuildRaylib, "bdist_wheel": PlatformWheel}) +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True + + +setup( + cmdclass={"build_py": BuildRaylib, "bdist_wheel": PlatformWheel}, + distclass=BinaryDistribution, +) diff --git a/test_wheels_in_image.sh b/test_wheels_in_image.sh index 995aaac..2d89846 100755 --- a/test_wheels_in_image.sh +++ b/test_wheels_in_image.sh @@ -36,6 +36,7 @@ docker run --rm -v "$PWD:/work" -w /work wheeltest bash -lc ' module="$(basename "$(dirname "$toml")" | tr "-" "_")" python -c "import $module; $module.smoketest()" && echo "$module: OK" done + RAYLIB_BACKEND=headless python -c "import raylib; raylib.smoketest()" ' echo From 3d022586fcf85cfb11ccca6a0fa914c55a7d03ac Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Wed, 5 Aug 2026 05:19:10 +0300 Subject: [PATCH 2/3] raylib: vendor Mesa runtime for headless rendering --- build.sh | 3 +- raylib/raylib/__init__.py | 18 +++++++ raylib/vendor_headless_gl.sh | 96 ++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 raylib/vendor_headless_gl.sh diff --git a/build.sh b/build.sh index ca60bbf..6d3bec2 100755 --- a/build.sh +++ b/build.sh @@ -54,7 +54,7 @@ if [[ -n "${BUILD_SH_IN_MANYLINUX:-}" ]]; then if command -v dnf &>/dev/null; then dnf install -y -q \ libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel libXi-devel \ - mesa-libGL-devel mesa-libEGL-devel mesa-libGLES-devel \ + mesa-libGL-devel mesa-libEGL-devel mesa-libGLES-devel mesa-dri-drivers \ wayland-devel wayland-protocols-devel libxkbcommon-devel fi fi @@ -89,6 +89,7 @@ if [[ -n "${BUILD_SH_IN_MANYLINUX:-}" ]]; then auditwheel repair \ --ldpaths "$ROOT_DIR/raylib/raylib/install/lib:/usr/lib64:/lib64" \ --wheel-dir dist/ dist/unrepaired/comma_deps_raylib-*.whl + bash "$ROOT_DIR/raylib/vendor_headless_gl.sh" dist/comma_deps_raylib-*.whl fi if [[ -n "${BUILD_SH_IN_MANYLINUX:-}" ]]; then diff --git a/raylib/raylib/__init__.py b/raylib/raylib/__init__.py index 25a1724..800d289 100644 --- a/raylib/raylib/__init__.py +++ b/raylib/raylib/__init__.py @@ -1,6 +1,24 @@ import importlib +import json import os + +def _setup_headless_gl(): + if os.environ.get("RAYLIB_BACKEND", "").lower() != "headless": + return + libs_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "comma_deps_raylib.libs") + mesa_egl = os.path.join(libs_dir, "libEGL_mesa.so.0") + if os.path.isdir(libs_dir) and os.path.isfile(mesa_egl): + vendor_json = os.path.join(libs_dir, "egl_vendor.json") + with open(vendor_json, "w") as f: + json.dump({"file_format_version": "1.0.0", "ICD": {"library_path": "libEGL_mesa.so.0"}}, f) + os.environ["__EGL_VENDOR_LIBRARY_FILENAMES"] = vendor_json + os.environ.setdefault("LIBGL_DRIVERS_PATH", libs_dir) + os.environ["GALLIUM_DRIVER"] = "softpipe" + + +_setup_headless_gl() + from ._backend import BACKEND_ARCHIVES, BACKEND_CFFI_MODULES, detect_backend, host_backends from .version import __version__ diff --git a/raylib/vendor_headless_gl.sh b/raylib/vendor_headless_gl.sh new file mode 100644 index 0000000..8438105 --- /dev/null +++ b/raylib/vendor_headless_gl.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -euo pipefail +WHEEL="$(realpath "$1")" +TMPDIR="$(mktemp -d)" +OUTDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR" "$OUTDIR"' EXIT + +cd "$TMPDIR" +unzip -q "$WHEEL" +L=comma_deps_raylib.libs + +resolve() { ldconfig -p | awk -v n="$1" '$1==n {print $NF; exit}'; } + +ROOTS="libEGL_mesa.so.0 swrast_dri.so" +DRI_DIR=/usr/lib64/dri + +declare -A DONE +todo="$ROOTS" +while [ -n "$todo" ]; do + next="" + for soname in $todo; do + [ -n "${DONE[$soname]:-}" ] && continue + DONE[$soname]=1 + if [ "$soname" = "swrast_dri.so" ]; then + src="$DRI_DIR/swrast_dri.so" + else + src=$(resolve "$soname") + fi + [ -f "$src" ] || continue + cp -L "$src" "$L/$soname" + for dep in $(readelf -d "$src" 2>/dev/null | awk '/NEEDED/{print $5}' | tr -d '[]'); do + case "$dep" in + libc.so.6|libm.so.6|libpthread.so.0|librt.so.1|libdl.so.2|ld-linux-x86-64.so.2|libLLVM*) + continue;; + esac + next="$next $dep" + done + done + todo="$next" +done + +for f in "$L"/*; do + patchelf --force-rpath --set-rpath '$ORIGIN' "$f" +done + +LLVM_SONAME="$(readelf -d "$L/swrast_dri.so" | awk '/NEEDED.*libLLVM/{print $NF}' | tr -d '[]')" +if [ -n "$LLVM_SONAME" ]; then + python3 - "$L/swrast_dri.so" "$L/$LLVM_SONAME" <<'PY' +import re, subprocess, sys +swrast, out = sys.argv[1], sys.argv[2] +lines = subprocess.check_output(["objdump", "-T", swrast], text=True).splitlines() +ver = next((m.group(1) for line in lines if (m := re.search(r"\((LLVM_[0-9]+)\)", line))), "") +if not ver: + sys.exit(f"no LLVM version found in {swrast}") +TYPES = {"DF", "F", "IF", "WF", "DO", "D", "O", "WO"} +funcs, datas = [], [] +for line in lines: + parts = line.split() + if f"({ver})" not in parts: + continue + idx = parts.index(f"({ver})") + name = parts[idx + 1] if idx + 1 < len(parts) else "" + if not name or name.startswith("."): + continue + typ = next((p for p in parts[1:idx] if p in TYPES), "DF") + (funcs if typ in ("DF", "F", "IF", "WF") else datas).append(name) +c = [f"__attribute__((visibility(\"default\"))) void *{n}(void){{ return 0; }}" for n in funcs] +c += [f"__attribute__((visibility(\"default\"))) unsigned long long {n} = 0;" for n in datas] +open("/tmp/llvm_stub.c", "w").write("\n".join(c) + "\n") +open("/tmp/llvm_stub.map", "w").write(f"{ver} {{ global: *; }};\n") +subprocess.check_call(["gcc", "-shared", "-fPIC", + "-Wl,--version-script=/tmp/llvm_stub.map", "-Wl,-soname," + out.rsplit("/", 1)[-1], + "-o", out, "/tmp/llvm_stub.c"]) +PY +fi + +OUT="$OUTDIR/$(basename "$WHEEL")" +python3 - "$OUT" <<'PY' +import base64, hashlib, sys, zipfile +from pathlib import Path +root = Path(".") +record = next(root.glob("*.dist-info/RECORD")) +rows = [] +for f in sorted(root.rglob("*")): + if not f.is_file() or f == record: + continue + h = hashlib.sha256(f.read_bytes()).digest() + rows.append(f"{f.as_posix()},sha256={base64.urlsafe_b64encode(h).rstrip(b'=').decode()},{f.stat().st_size}") +rows.append(f"{record.as_posix()},,") +record.write_text("\n".join(rows) + "\n") +with zipfile.ZipFile(sys.argv[1], "w", zipfile.ZIP_DEFLATED) as z: + for f in sorted(root.rglob("*")): + if f.is_file(): + z.write(f, f.as_posix()) +PY +cp "$OUT" "$WHEEL" From fea8b2f43b025912680c52bbe9a63a2b81961e06 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Wed, 5 Aug 2026 07:42:49 +0300 Subject: [PATCH 3/3] llvmpipe for better perfomance --- raylib/raylib/__init__.py | 2 +- raylib/vendor_headless_gl.sh | 33 +-------------------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/raylib/raylib/__init__.py b/raylib/raylib/__init__.py index 800d289..e7e2d1c 100644 --- a/raylib/raylib/__init__.py +++ b/raylib/raylib/__init__.py @@ -14,7 +14,7 @@ def _setup_headless_gl(): json.dump({"file_format_version": "1.0.0", "ICD": {"library_path": "libEGL_mesa.so.0"}}, f) os.environ["__EGL_VENDOR_LIBRARY_FILENAMES"] = vendor_json os.environ.setdefault("LIBGL_DRIVERS_PATH", libs_dir) - os.environ["GALLIUM_DRIVER"] = "softpipe" + os.environ["GALLIUM_DRIVER"] = "llvmpipe" _setup_headless_gl() diff --git a/raylib/vendor_headless_gl.sh b/raylib/vendor_headless_gl.sh index 8438105..1cd3bed 100644 --- a/raylib/vendor_headless_gl.sh +++ b/raylib/vendor_headless_gl.sh @@ -30,7 +30,7 @@ while [ -n "$todo" ]; do cp -L "$src" "$L/$soname" for dep in $(readelf -d "$src" 2>/dev/null | awk '/NEEDED/{print $5}' | tr -d '[]'); do case "$dep" in - libc.so.6|libm.so.6|libpthread.so.0|librt.so.1|libdl.so.2|ld-linux-x86-64.so.2|libLLVM*) + libc.so.6|libm.so.6|libpthread.so.0|librt.so.1|libdl.so.2|ld-linux-x86-64.so.2) continue;; esac next="$next $dep" @@ -43,37 +43,6 @@ for f in "$L"/*; do patchelf --force-rpath --set-rpath '$ORIGIN' "$f" done -LLVM_SONAME="$(readelf -d "$L/swrast_dri.so" | awk '/NEEDED.*libLLVM/{print $NF}' | tr -d '[]')" -if [ -n "$LLVM_SONAME" ]; then - python3 - "$L/swrast_dri.so" "$L/$LLVM_SONAME" <<'PY' -import re, subprocess, sys -swrast, out = sys.argv[1], sys.argv[2] -lines = subprocess.check_output(["objdump", "-T", swrast], text=True).splitlines() -ver = next((m.group(1) for line in lines if (m := re.search(r"\((LLVM_[0-9]+)\)", line))), "") -if not ver: - sys.exit(f"no LLVM version found in {swrast}") -TYPES = {"DF", "F", "IF", "WF", "DO", "D", "O", "WO"} -funcs, datas = [], [] -for line in lines: - parts = line.split() - if f"({ver})" not in parts: - continue - idx = parts.index(f"({ver})") - name = parts[idx + 1] if idx + 1 < len(parts) else "" - if not name or name.startswith("."): - continue - typ = next((p for p in parts[1:idx] if p in TYPES), "DF") - (funcs if typ in ("DF", "F", "IF", "WF") else datas).append(name) -c = [f"__attribute__((visibility(\"default\"))) void *{n}(void){{ return 0; }}" for n in funcs] -c += [f"__attribute__((visibility(\"default\"))) unsigned long long {n} = 0;" for n in datas] -open("/tmp/llvm_stub.c", "w").write("\n".join(c) + "\n") -open("/tmp/llvm_stub.map", "w").write(f"{ver} {{ global: *; }};\n") -subprocess.check_call(["gcc", "-shared", "-fPIC", - "-Wl,--version-script=/tmp/llvm_stub.map", "-Wl,-soname," + out.rsplit("/", 1)[-1], - "-o", out, "/tmp/llvm_stub.c"]) -PY -fi - OUT="$OUTDIR/$(basename "$WHEEL")" python3 - "$OUT" <<'PY' import base64, hashlib, sys, zipfile