summaryrefslogtreecommitdiff
path: root/subprojects/pixpat/setup.py
diff options
context:
space:
mode:
authorTomi Valkeinen <tomi.valkeinen@ideasonboard.com>2026-05-08 17:22:58 +0300
committerTomi Valkeinen <tomi.valkeinen@ideasonboard.com>2026-05-08 17:22:58 +0300
commit4e2b291a4acdc2cbd39f005c88bda363bc06bd34 (patch)
treee90048d5973ad1164b109d575cf577af7daf50be /subprojects/pixpat/setup.py
parent8f94b39040e79eccd9312ed1e467fe8ebfab8860 (diff)
parente0b7d30fd437292c88141fb08d60681870b86c6e (diff)
Merge commit 'e0b7d30fd437292c88141fb08d60681870b86c6e' as 'subprojects/pixpat'
Diffstat (limited to 'subprojects/pixpat/setup.py')
-rw-r--r--subprojects/pixpat/setup.py152
1 files changed, 152 insertions, 0 deletions
diff --git a/subprojects/pixpat/setup.py b/subprojects/pixpat/setup.py
new file mode 100644
index 0000000..0a24682
--- /dev/null
+++ b/subprojects/pixpat/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 <arch> — sets PIXPAT_TARGET_ARCH
+ and runs `python -m build`; meson lands in pixpat-python/build-<arch>/
+ 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[-<arch>]/."""
+
+ 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.<maj> -> .so.<maj>.<min>.<patch>
+ # 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,
+ }
+)