diff --git a/build.sh b/build.sh index fe03a50..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 @@ -83,6 +83,15 @@ 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 + bash "$ROOT_DIR/raylib/vendor_headless_gl.sh" dist/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/raylib/__init__.py b/raylib/raylib/__init__.py index 25a1724..e7e2d1c 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"] = "llvmpipe" + + +_setup_headless_gl() + from ._backend import BACKEND_ARCHIVES, BACKEND_CFFI_MODULES, detect_backend, host_backends from .version import __version__ 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/raylib/vendor_headless_gl.sh b/raylib/vendor_headless_gl.sh new file mode 100644 index 0000000..1cd3bed --- /dev/null +++ b/raylib/vendor_headless_gl.sh @@ -0,0 +1,65 @@ +#!/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) + continue;; + esac + next="$next $dep" + done + done + todo="$next" +done + +for f in "$L"/*; do + patchelf --force-rpath --set-rpath '$ORIGIN' "$f" +done + +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" 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