From e0b7d30fd437292c88141fb08d60681870b86c6e Mon Sep 17 00:00:00 2001 From: Tomi Valkeinen Date: Fri, 8 May 2026 17:22:58 +0300 Subject: Squashed 'subprojects/pixpat/' content from commit d444626 git-subtree-dir: subprojects/pixpat git-subtree-split: d444626e6ba988ec6d487800721e447f94b1eaf5 --- setup.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 setup.py (limited to 'setup.py') diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0a24682 --- /dev/null +++ b/setup.py @@ -0,0 +1,152 @@ +"""setup.py overrides several setuptools commands to integrate the meson-built +libpixpat.so into the Python wheel. + +Layout (where things land for each workflow): + + meson setup build / meson compile -C build — repo-root build/, the + C++ developer's meson dir. Python tooling never touches this. + + pip install -e . — DevEditableWheel + symlinks build/libpixpat.so into pixpat-python/pixpat/_lib/. + Requires the user to have built `build/` themselves. + + pip install . — MesonBuildPy invokes + meson into pixpat-python/build/native/, copies the .so straight into + setuptools' build_lib (pixpat-python/build/lib/pixpat/_lib/), and + bdist_wheel zips that into the wheel. The source tree's _lib/ is + never touched on this path, so wheel builds don't depend on (or + pollute) it. + + scripts/build_wheel.sh — sets PIXPAT_TARGET_ARCH + and runs `python -m build`; meson lands in pixpat-python/build-/ + native/, setuptools alongside. + +bdist_wheel: setuptools' default produces a `py3-none-any` (pure-Python) wheel +when there's no compiled extension. We bundle a `.so` as package data, so we +MUST tag the wheel for a specific platform — otherwise pip would +install our x86_64 wheel on an arm64 box. Target arch is read from +PIXPAT_TARGET_ARCH and falls back to the running machine's arch — important +because setuptools' editable_wheel delegates wheel construction to whatever +bdist_wheel is registered, so this class is invoked during `pip install -e .` +too. +""" + +import os +import platform +import shutil +import subprocess +from pathlib import Path + +from setuptools import setup +from setuptools.command.bdist_wheel import bdist_wheel +from setuptools.command.build import build +from setuptools.command.build_py import build_py +from setuptools.command.editable_wheel import editable_wheel + +REPO_ROOT = Path(__file__).resolve().parent +LIB_DIR = REPO_ROOT / 'pixpat-python' / 'pixpat' / '_lib' + + +def _target_arch() -> str: + return os.environ.get('PIXPAT_TARGET_ARCH') or platform.machine() + + +def _wheel_build_root() -> Path: + """Top-level dir where setuptools and meson stage wheel-build output. + + Plain `pip install .` uses pixpat-python/build/. Cross-compile via + PIXPAT_TARGET_ARCH gets a per-arch sibling so meson can keep arches + incrementally compiled side by side. + """ + arch = os.environ.get('PIXPAT_TARGET_ARCH') + suffix = f'-{arch}' if arch else '' + return REPO_ROOT / 'pixpat-python' / f'build{suffix}' + + +class ImpureBdistWheel(bdist_wheel): + def finalize_options(self): + super().finalize_options() + self.root_is_pure = False + + def get_tag(self): + return ('py3', 'none', f'linux_{_target_arch()}') + + +class WheelBuild(build): + """Direct setuptools' staging into pixpat-python/build[-]/.""" + + def initialize_options(self): + super().initialize_options() + self.build_base = str(_wheel_build_root()) + + +class MesonBuildPy(build_py): + """Compile libpixpat.so via meson and stage it into build_lib for the wheel.""" + + def run(self): + super().run() + # editable_mode is set by setuptools' editable_wheel command. In that + # path DevEditableWheel has already symlinked the user's repo-root + # build/ into the source _lib/, so meson must not run. + if not self.editable_mode: + self._build_native() + + def _build_native(self): + meson_dir = _wheel_build_root() / 'native' + arch = _target_arch() + cross_args = [] + if arch != platform.machine(): + cross_file = REPO_ROOT / 'pixpat-native' / 'cross' / f'{arch}-linux-gnu.txt' + if not cross_file.exists(): + raise SystemExit(f'no cross-file for {arch}: {cross_file}') + cross_args = ['--cross-file', str(cross_file)] + + if not (meson_dir / 'meson-info').exists(): + meson_dir.parent.mkdir(parents=True, exist_ok=True) + subprocess.check_call( + ['meson', 'setup', str(meson_dir), '--buildtype=release', *cross_args], + cwd=REPO_ROOT, + ) + subprocess.check_call(['meson', 'compile', '-C', str(meson_dir)], cwd=REPO_ROOT) + + out_dir = Path(self.build_lib) / 'pixpat' / '_lib' + out_dir.mkdir(parents=True, exist_ok=True) + # follow_symlinks resolves meson's libpixpat.so -> .so. -> .so... + # chain into a regular file. The wheel ships only the unversioned name, so we + # don't have to track meson's project version here. + shutil.copy2(meson_dir / 'libpixpat.so', out_dir / 'libpixpat.so') + + +class DevEditableWheel(editable_wheel): + """Symlink the user's repo-root meson build into _lib/ before installing.""" + + def run(self): + build_so = REPO_ROOT / 'build' / 'libpixpat.so' + + if not build_so.exists(): + raise SystemExit( + f'editable install needs {build_so}.\n' + 'Configure and build first:\n' + ' meson setup build\n' + ' meson compile -C build' + ) + + LIB_DIR.mkdir(parents=True, exist_ok=True) + for old in LIB_DIR.glob('libpixpat.so*'): + old.unlink() + # Relative path so the symlink survives moves of the source tree. + (LIB_DIR / 'libpixpat.so').symlink_to( + Path('..') / '..' / '..' / 'build' / 'libpixpat.so' + ) + + super().run() + + +setup( + cmdclass={ + 'bdist_wheel': ImpureBdistWheel, + 'build': WheelBuild, + 'build_py': MesonBuildPy, + 'editable_wheel': DevEditableWheel, + } +) -- cgit v1.2.3