add xvfb package (#77)
This commit is contained in:
@@ -19,6 +19,7 @@ members = [
|
|||||||
"ncurses",
|
"ncurses",
|
||||||
"qt5",
|
"qt5",
|
||||||
"raylib",
|
"raylib",
|
||||||
|
"xvfb",
|
||||||
"zeromq",
|
"zeromq",
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|||||||
133
xvfb/build.sh
Executable file
133
xvfb/build.sh
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
INSTALL_DIR="$DIR/xvfb/install"
|
||||||
|
|
||||||
|
# macOS: Xvfb is Linux-only. Ship an empty install dir so the wheel still
|
||||||
|
# builds; smoketest() is a no-op on Darwin.
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"/{bin,lib,share/X11/xkb}
|
||||||
|
echo "xvfb: macOS not supported, shipping empty install dir"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linux: bundle the Xvfb binary, xkbcomp, xkb keymap data, and the closure
|
||||||
|
# of shared libraries it needs (minus libc/libpthread/etc which the host
|
||||||
|
# always provides). For CI/manylinux we pull from AlmaLinux 8 so the wheel
|
||||||
|
# works on any glibc >= 2.28 distro; on a Debian/Ubuntu dev host we accept
|
||||||
|
# whatever the system has (the local wheel just won't be as portable).
|
||||||
|
if command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y -q xorg-x11-server-Xvfb xorg-x11-xkb-utils xkeyboard-config >/dev/null
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
if [[ "$(id -u)" -eq 0 ]]; then SUDO=""
|
||||||
|
elif command -v sudo >/dev/null 2>&1; then SUDO=sudo
|
||||||
|
else echo "xvfb: need sudo or root to apt-get install" >&2; exit 1; fi
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
$SUDO apt-get update -qq
|
||||||
|
$SUDO apt-get install -y -qq --no-install-recommends \
|
||||||
|
xvfb x11-xkb-utils xkb-data patchelf
|
||||||
|
else
|
||||||
|
echo "xvfb: need dnf or apt-get to fetch upstream Xvfb" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v patchelf >/dev/null 2>&1; then
|
||||||
|
echo "xvfb: patchelf is required but not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"/{bin,lib,share/X11/xkb}
|
||||||
|
|
||||||
|
# Xvfb may live in /usr/bin (RPM/Debian) — just locate it.
|
||||||
|
XVFB_SRC="$(command -v Xvfb || true)"
|
||||||
|
XKBCOMP_SRC="$(command -v xkbcomp || true)"
|
||||||
|
if [[ -z "$XVFB_SRC" || -z "$XKBCOMP_SRC" ]]; then
|
||||||
|
echo "xvfb: Xvfb or xkbcomp not found after install" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$XVFB_SRC" "$INSTALL_DIR/bin/Xvfb"
|
||||||
|
cp "$XKBCOMP_SRC" "$INSTALL_DIR/bin/xkbcomp"
|
||||||
|
chmod u+w "$INSTALL_DIR/bin/Xvfb" "$INSTALL_DIR/bin/xkbcomp"
|
||||||
|
cp -a /usr/share/X11/xkb/. "$INSTALL_DIR/share/X11/xkb/"
|
||||||
|
|
||||||
|
# Two binary patches to make Xvfb relocatable:
|
||||||
|
#
|
||||||
|
# 1. "/usr/bin" -> "" (null bytes)
|
||||||
|
# Xvfb's compile-time XkbBinDirectory points at /usr/bin and is used to
|
||||||
|
# build an absolute path to xkbcomp. Blanking it makes the spawned
|
||||||
|
# command just "xkbcomp", which popen() then resolves via PATH. Our
|
||||||
|
# Python wrapper prepends the bundled bin/ to PATH.
|
||||||
|
#
|
||||||
|
# 2. "-R%s" -> "-I%s" (in the xkbcomp argv format string)
|
||||||
|
# Xvfb passes its XkbBaseDirectory to xkbcomp via -R, expecting xkbcomp
|
||||||
|
# to chdir there *and* add "." to the include path. xkbcomp 1.4.x only
|
||||||
|
# chdirs — the include-path side was added later. Switching to -I makes
|
||||||
|
# 1.4.2 actually search the path we hand it via -xkbdir.
|
||||||
|
python3 - "$INSTALL_DIR/bin/Xvfb" <<'PY'
|
||||||
|
import sys
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, "r+b") as f:
|
||||||
|
data = bytearray(f.read())
|
||||||
|
|
||||||
|
def replace_unique(needle: bytes, replacement: bytes):
|
||||||
|
assert len(needle) == len(replacement)
|
||||||
|
idx = data.find(needle)
|
||||||
|
if idx < 0:
|
||||||
|
sys.exit(f"could not find {needle!r} in Xvfb binary")
|
||||||
|
if data.find(needle, idx + 1) >= 0:
|
||||||
|
sys.exit(f"multiple {needle!r} matches; refusing to patch")
|
||||||
|
data[idx:idx + len(needle)] = replacement
|
||||||
|
|
||||||
|
replace_unique(b"/usr/bin\x00", b"\x00" * 9)
|
||||||
|
replace_unique(b'"-R%s"\x00', b'"-I%s"\x00')
|
||||||
|
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
PY
|
||||||
|
|
||||||
|
# bundle the shared library closure. Recursively walk ldd output to catch
|
||||||
|
# libs-of-libs (e.g. libXfont2 -> libfontenc -> libbz2). Skip core glibc
|
||||||
|
# pieces; everything else gets copied alongside the binary.
|
||||||
|
declare -A SEEN
|
||||||
|
collect_libs() {
|
||||||
|
local target="$1"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
local lib
|
||||||
|
lib=$(echo "$line" | awk '{print $3}')
|
||||||
|
[[ -z "$lib" || "$lib" == "not" ]] && continue
|
||||||
|
[[ ! -e "$lib" ]] && continue
|
||||||
|
local base
|
||||||
|
base=$(basename "$lib")
|
||||||
|
case "$base" in
|
||||||
|
libc.so.*|libpthread.so.*|libm.so.*|librt.so.*|libdl.so.*|libgcc_s.so.*|libresolv.so.*|libutil.so.*|ld-linux-*.so.*|linux-vdso.so.*|linux-gate.so.*)
|
||||||
|
continue ;;
|
||||||
|
esac
|
||||||
|
[[ -n "${SEEN[$base]:-}" ]] && continue
|
||||||
|
SEEN[$base]=1
|
||||||
|
cp -L "$lib" "$INSTALL_DIR/lib/$base"
|
||||||
|
chmod u+w "$INSTALL_DIR/lib/$base"
|
||||||
|
collect_libs "$INSTALL_DIR/lib/$base"
|
||||||
|
done < <(ldd "$target" 2>/dev/null || true)
|
||||||
|
}
|
||||||
|
collect_libs "$INSTALL_DIR/bin/Xvfb"
|
||||||
|
collect_libs "$INSTALL_DIR/bin/xkbcomp"
|
||||||
|
|
||||||
|
# point the binaries (and the bundled libs) at our private lib dir so they
|
||||||
|
# don't accidentally resolve against an incompatible host copy.
|
||||||
|
patchelf --set-rpath '$ORIGIN/../lib' "$INSTALL_DIR/bin/Xvfb"
|
||||||
|
patchelf --set-rpath '$ORIGIN/../lib' "$INSTALL_DIR/bin/xkbcomp"
|
||||||
|
for so in "$INSTALL_DIR"/lib/*.so*; do
|
||||||
|
patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
strip --strip-unneeded "$INSTALL_DIR/bin/Xvfb" "$INSTALL_DIR/bin/xkbcomp" 2>/dev/null || true
|
||||||
|
find "$INSTALL_DIR/lib" -name '*.so*' -exec strip --strip-unneeded {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Installed xvfb to $INSTALL_DIR"
|
||||||
|
du -sh "$INSTALL_DIR"
|
||||||
18
xvfb/pyproject.toml
Normal file
18
xvfb/pyproject.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=64", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "xvfb"
|
||||||
|
version = "1.20.11"
|
||||||
|
description = "Xvfb (X virtual framebuffer) headless X server"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
Xvfb = "xvfb:_run_xvfb"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["xvfb*"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
xvfb = ["install/**/*"]
|
||||||
58
xvfb/setup.py
Normal file
58
xvfb/setup.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from setuptools.command.build_py import build_py
|
||||||
|
|
||||||
|
try:
|
||||||
|
from wheel.bdist_wheel import bdist_wheel
|
||||||
|
except ImportError:
|
||||||
|
bdist_wheel = None
|
||||||
|
|
||||||
|
|
||||||
|
class BuildXvfb(build_py):
|
||||||
|
"""Run build.sh to fetch and bundle Xvfb before collecting package data."""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
build_script = os.path.join(pkg_dir, "build.sh")
|
||||||
|
subprocess.check_call(["bash", build_script], cwd=pkg_dir)
|
||||||
|
|
||||||
|
super().run()
|
||||||
|
|
||||||
|
|
||||||
|
cmdclass = {"build_py": BuildXvfb}
|
||||||
|
|
||||||
|
if bdist_wheel is not None:
|
||||||
|
|
||||||
|
class PlatformWheel(bdist_wheel):
|
||||||
|
"""Produce a platform-specific, Python-version-agnostic wheel."""
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
super().finalize_options()
|
||||||
|
self.root_is_pure = False
|
||||||
|
|
||||||
|
def get_tag(self):
|
||||||
|
system = platform.system()
|
||||||
|
machine = platform.machine()
|
||||||
|
|
||||||
|
if system == "Linux":
|
||||||
|
plat = f"linux_{machine}"
|
||||||
|
elif system == "Darwin":
|
||||||
|
plat = "macosx_11_0_arm64"
|
||||||
|
else:
|
||||||
|
plat = f"{system.lower()}_{machine}"
|
||||||
|
|
||||||
|
return "py3", "none", plat
|
||||||
|
|
||||||
|
cmdclass["bdist_wheel"] = PlatformWheel
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
from setuptools import setup as _setup
|
||||||
|
|
||||||
|
_setup(cmdclass=cmdclass)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup()
|
||||||
39
xvfb/xvfb/__init__.py
Normal file
39
xvfb/xvfb/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DIR = os.path.join(os.path.dirname(__file__), "install")
|
||||||
|
BIN_DIR = os.path.join(DIR, "bin")
|
||||||
|
LIB_DIR = os.path.join(DIR, "lib")
|
||||||
|
XKB_DIR = os.path.join(DIR, "share", "X11", "xkb")
|
||||||
|
|
||||||
|
XVFB_BIN = os.path.join(BIN_DIR, "Xvfb")
|
||||||
|
XKBCOMP_BIN = os.path.join(BIN_DIR, "xkbcomp")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_xvfb():
|
||||||
|
# The bundled Xvfb has its compile-time XkbBinDirectory blanked out so it
|
||||||
|
# invokes xkbcomp via PATH lookup; prepend our bin dir so the bundled
|
||||||
|
# xkbcomp wins. -xkbdir points the server at the bundled keymap data.
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PATH"] = BIN_DIR + os.pathsep + env.get("PATH", "")
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if not any(a == "-xkbdir" for a in args):
|
||||||
|
args = ["-xkbdir", XKB_DIR] + args
|
||||||
|
os.execvpe(XVFB_BIN, [XVFB_BIN] + args, env)
|
||||||
|
|
||||||
|
|
||||||
|
def smoketest():
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return
|
||||||
|
assert os.path.isfile(XVFB_BIN), f"Xvfb not found at {XVFB_BIN}"
|
||||||
|
assert os.path.isfile(XKBCOMP_BIN), f"xkbcomp not found at {XKBCOMP_BIN}"
|
||||||
|
assert os.path.isdir(XKB_DIR), f"xkb data not found at {XKB_DIR}"
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
# Xvfb prints usage to stderr and exits non-zero on `-help`; the banner
|
||||||
|
# mentions Xvfb-specific flags like -screen and -fbdir. If those appear,
|
||||||
|
# the binary loaded its bundled libs and ran far enough to print help.
|
||||||
|
result = subprocess.run([XVFB_BIN, "-help"], capture_output=True, text=True)
|
||||||
|
output = result.stderr + result.stdout
|
||||||
|
assert "-screen scrn WxHxD" in output, \
|
||||||
|
f"Xvfb -help did not produce expected output: {output}"
|
||||||
Reference in New Issue
Block a user