diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d428f71..6098a3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: - name: Configure Python for Bazel run: | echo "VELARIA_PYTHON_BIN=$(which python)" >> "$GITHUB_ENV" + echo "VELARIA_PLATFORM_TAG=macosx_11_0_arm64" >> "$GITHUB_ENV" - name: Native Build run: | @@ -104,6 +105,10 @@ jobs: dist/linux/velaria-*.whl rm -f dist/linux/velaria-*-linux_*.whl + - name: Validate manylinux wheel contents + run: | + python3 ./scripts/validate_native_wheel.py dist/linux/velaria-*.whl python_api/velaria + - name: Upload manylinux wheel uses: actions/upload-artifact@v4 with: @@ -136,8 +141,12 @@ jobs: cp bazel-bin/python_api/velaria-*-native.whl dist/macos/ python3 ./scripts/normalize_wheel_filename.py dist/macos/velaria-*-native.whl + - name: Validate macOS wheel contents + run: | + python3 ./scripts/validate_native_wheel.py dist/macos/velaria-*.whl python_api/velaria + - name: Upload macOS wheel uses: actions/upload-artifact@v4 with: - name: velaria-macos-wheel + name: velaria-macos-arm64-wheel path: dist/macos/*.whl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40a7f0e..8bf22e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,7 @@ jobs: - name: Configure Python for Bazel run: | echo "VELARIA_PYTHON_BIN=$(which python)" >> "$GITHUB_ENV" + echo "VELARIA_PLATFORM_TAG=macosx_11_0_arm64" >> "$GITHUB_ENV" - name: Build native wheel run: | @@ -78,6 +79,10 @@ jobs: dist/linux/velaria-*.whl rm -f dist/linux/velaria-*-linux_*.whl + - name: Validate manylinux wheel contents + run: | + python3 ./scripts/validate_native_wheel.py dist/linux/velaria-*.whl python_api/velaria + - name: Upload manylinux artifact uses: actions/upload-artifact@v4 with: @@ -110,10 +115,14 @@ jobs: cp bazel-bin/python_api/velaria-*-native.whl dist/macos/ python3 ./scripts/normalize_wheel_filename.py dist/macos/velaria-*-native.whl + - name: Validate macOS wheel contents + run: | + python3 ./scripts/validate_native_wheel.py dist/macos/velaria-*.whl python_api/velaria + - name: Upload macOS artifact uses: actions/upload-artifact@v4 with: - name: velaria-macos-wheel + name: velaria-macos-arm64-wheel path: dist/macos/*.whl publish-release: @@ -132,7 +141,7 @@ jobs: - name: Download macOS artifact uses: actions/download-artifact@v4 with: - name: velaria-macos-wheel + name: velaria-macos-arm64-wheel path: dist/macos - name: Publish GitHub release diff --git a/python_api/BUILD.bazel b/python_api/BUILD.bazel index 2049562..3e7b369 100644 --- a/python_api/BUILD.bazel +++ b/python_api/BUILD.bazel @@ -51,6 +51,12 @@ py_package( deps = [":velaria_py_pkg"], ) +filegroup( + name = "velaria_python_sources", + srcs = glob(["velaria/**/*.py"]), + visibility = ["//visibility:public"], +) + py_wheel( name = "velaria_whl", distribution = "velaria", @@ -68,6 +74,7 @@ genrule( name = "velaria_native_whl", srcs = [ ":velaria_whl", + ":velaria_python_sources", "//:velaria_pyext", "//scripts:build_native_wheel.py", "@velaria_local_python//:python_tag.txt", @@ -81,7 +88,8 @@ genrule( + "$(location @velaria_local_python//:python_tag.txt) " + "$(location @velaria_local_python//:abi_tag.txt) " + "$(location @velaria_local_python//:platform_tag.txt) " - + "velaria $@", + + "velaria $@" + + " $(locations :velaria_python_sources)", tools = ["//scripts:build_native_wheel.py"], ) diff --git a/scripts/build_native_wheel.py b/scripts/build_native_wheel.py index f8b508c..244e513 100644 --- a/scripts/build_native_wheel.py +++ b/scripts/build_native_wheel.py @@ -3,6 +3,7 @@ import base64 import csv import hashlib +import os import pathlib import shutil import sys @@ -19,11 +20,27 @@ def _hash_bytes(data: bytes) -> str: return "sha256=" + base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=") +def _copy_package_sources(package_root: pathlib.Path, distribution: str, source_paths: list[str]) -> None: + for source in source_paths: + source_path = pathlib.Path(source) + parts = source_path.parts + try: + package_index = max(i for i, part in enumerate(parts) if part == distribution) + except ValueError as exc: + raise RuntimeError( + f"could not infer package-relative path for source file {source_path}" + ) from exc + rel = pathlib.Path(*parts[package_index + 1 :]) + destination = package_root / rel + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination) + + def main() -> int: - if len(sys.argv) != 8: + if len(sys.argv) < 8: raise SystemExit( "usage: build_native_wheel.py " - " " + " [package_sources...]" ) pure_whl = pathlib.Path(sys.argv[1]) @@ -33,6 +50,7 @@ def main() -> int: platform_tag = _read_text(pathlib.Path(sys.argv[5])) distribution = sys.argv[6] out_whl = pathlib.Path(sys.argv[7]) + package_sources = sys.argv[8:] with tempfile.TemporaryDirectory(prefix="velaria-wheel-") as tmp: tmpdir = pathlib.Path(tmp) @@ -43,6 +61,7 @@ def main() -> int: dist_info = next(wheel_root.glob("*.dist-info")) package_dir = wheel_root / distribution package_dir.mkdir(parents=True, exist_ok=True) + _copy_package_sources(package_dir, distribution, package_sources) shutil.copy2(native_so, package_dir / "_velaria.so") wheel_metadata = dist_info / "WHEEL" diff --git a/scripts/validate_native_wheel.py b/scripts/validate_native_wheel.py new file mode 100644 index 0000000..dd3dbad --- /dev/null +++ b/scripts/validate_native_wheel.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import pathlib +import re +import sys +import zipfile + +def _find_dist_info_prefix(names: set[str]) -> str: + for name in sorted(names): + match = re.match(r"^(velaria-[^/]+\.dist-info)/", name) + if match: + return match.group(1) + raise RuntimeError("missing velaria dist-info directory") + + +def _read_wheel_text(zf: zipfile.ZipFile, member: str) -> str: + return zf.read(member).decode("utf-8") + + +def _expected_members(source_dir: pathlib.Path) -> set[str]: + if not source_dir.exists(): + raise RuntimeError(f"package source dir not found: {source_dir}") + if not source_dir.is_dir(): + raise RuntimeError(f"package source path is not a directory: {source_dir}") + + package_name = source_dir.name + members = {f"{package_name}/_velaria.so"} + for path in sorted(source_dir.rglob("*.py")): + members.add(path.relative_to(source_dir.parent).as_posix()) + return members + + +def main() -> int: + if len(sys.argv) != 3: + raise SystemExit("usage: validate_native_wheel.py ") + + wheel_path = pathlib.Path(sys.argv[1]).resolve() + source_dir = pathlib.Path(sys.argv[2]).resolve() + if not wheel_path.exists(): + raise SystemExit(f"wheel not found: {wheel_path}") + + with zipfile.ZipFile(wheel_path, "r") as zf: + names = set(zf.namelist()) + missing = sorted(_expected_members(source_dir) - names) + if missing: + raise SystemExit( + "wheel is missing required members:\n" + "\n".join(f"- {name}" for name in missing) + ) + + dist_info = _find_dist_info_prefix(names) + wheel_text = _read_wheel_text(zf, f"{dist_info}/WHEEL") + if "Root-Is-Purelib: false" not in wheel_text: + raise SystemExit("wheel must declare Root-Is-Purelib: false") + if "Tag: py3-none-any" in wheel_text: + raise SystemExit("wheel must not keep pure-python py3-none-any tag") + + native_info = zf.getinfo("velaria/_velaria.so") + if native_info.file_size <= 0: + raise SystemExit("velaria/_velaria.so is empty") + + print(f"[wheel-check] ok {wheel_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/python_configure.bzl b/tools/python_configure.bzl index 7944f91..e282517 100644 --- a/tools/python_configure.bzl +++ b/tools/python_configure.bzl @@ -1,5 +1,6 @@ def _local_python_config_impl(repository_ctx): override = repository_ctx.os.environ.get("VELARIA_PYTHON_BIN", "") + platform_override = repository_ctx.os.environ.get("VELARIA_PLATFORM_TAG", "") candidates = [] if override: candidates.append(override) @@ -14,7 +15,7 @@ def _local_python_config_impl(repository_ctx): ]) probe = """ -import json, os, sys, sysconfig +import json, os, platform, sys, sysconfig inc = sysconfig.get_path("include") or "" plat = sysconfig.get_path("platinclude") or "" paths = [p for p in [inc, plat] if p] @@ -29,6 +30,8 @@ print(json.dumps({ "version": sys.version.split()[0], "include_dir": os.path.dirname(header) if header else "", "header": header, + "machine": platform.machine().lower(), + "sys_platform": sys.platform, "python_tag": f"cp{sys.version_info[0]}{sys.version_info[1]}", "abi_tag": f"cp{sys.version_info[0]}{sys.version_info[1]}", "platform_tag": sysconfig.get_platform().replace("-", "_").replace(".", "_"), @@ -54,11 +57,21 @@ print(json.dumps({ if chosen == None: fail("Could not find a local CPython interpreter with Python.h. Set VELARIA_PYTHON_BIN to a usable interpreter.") + platform_tag = chosen["platform_tag"] + if platform_override: + platform_tag = platform_override + elif chosen.get("sys_platform") == "darwin": + machine = chosen.get("machine", "") + if machine == "arm64": + platform_tag = "macosx_11_0_arm64" + elif machine == "x86_64" and platform_tag == "macosx_10_13_universal2": + platform_tag = "macosx_10_13_x86_64" + repository_ctx.symlink(chosen["include_dir"], "include") repository_ctx.file("defs.bzl", 'VELARIA_PYTHON_INCLUDE = "include"\n') repository_ctx.file("python_tag.txt", chosen["python_tag"] + "\n") repository_ctx.file("abi_tag.txt", chosen["abi_tag"] + "\n") - repository_ctx.file("platform_tag.txt", chosen["platform_tag"] + "\n") + repository_ctx.file("platform_tag.txt", platform_tag + "\n") repository_ctx.file( "BUILD.bazel", """ @@ -79,5 +92,5 @@ exports_files(["include_marker", "python_tag.txt", "abi_tag.txt", "platform_tag. local_python_configure = repository_rule( implementation = _local_python_config_impl, - environ = ["VELARIA_PYTHON_BIN"], + environ = ["VELARIA_PLATFORM_TAG", "VELARIA_PYTHON_BIN"], )