diff --git a/.github/workflows/release-aarch64.yml b/.github/workflows/release-aarch64.yml new file mode 100644 index 0000000..abc0487 --- /dev/null +++ b/.github/workflows/release-aarch64.yml @@ -0,0 +1,111 @@ +# Builds an aarch64-only wheel of mldebug_xdp and publishes it as a GitHub release. +# The aarch64 wheel is produced by post-processing the standard wheel: +# Windows and x86 Linux binaries are stripped out and the wheel is re-tagged +# as py3-none-linux_aarch64. + +name: MLDebugger Release (aarch64) + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag (Default: v-aarch64)' + required: false + default: '' + release_name: + description: 'Release name (Default: "Release v (aarch64)")' + required: false + default: '' + +permissions: + contents: write + +env: + GIT_MODE: 1 + +jobs: + build: + runs-on: [ build ] + steps: + - uses: actions/checkout@v4 + with: + clean: true + lfs: true + + - name: Pull LFS files + run: git lfs pull + + - name: Add uv to PATH + run: echo "$(python -m site --user-base)/bin" >> $GITHUB_PATH + + - name: "Set up Python" + uses: actions/setup-python@v4 + with: + python-version-file: "pyproject.toml" + + - name: Create and activate virtual environment + run: | + uv venv + source .venv/bin/activate + + - name: Update submodules + run: | + git submodule update --init --recursive + git submodule foreach --recursive 'git lfs pull || true' + + - name: Build base wheel with UV + run: | + uv build --wheel . + + - name: Build aarch64 wheel + run: | + uv pip install --system wheel + BASE_WHEEL=$(find dist -name "mldebug_xdp-*-py3-none-any.whl" | head -1) + if [ -z "$BASE_WHEEL" ]; then + echo "No base wheel found in dist/" >&2 + exit 1 + fi + python scripts/build_aarch64_wheel.py "$BASE_WHEEL" --out-dir dist + + - name: Store aarch64 wheel path + id: wheel-path + run: | + WHEEL_PATH=$(find dist -name "*-linux_aarch64.whl" | head -1) + if [ -z "$WHEEL_PATH" ]; then + echo "aarch64 wheel not found" >&2 + exit 1 + fi + echo "wheel_path=$WHEEL_PATH" >> $GITHUB_OUTPUT + echo "wheel_name=$(basename $WHEEL_PATH)" >> $GITHUB_OUTPUT + + - name: Resolve release tag and name + id: release_meta + shell: bash + run: | + DEFAULT_TAG="v${{ github.run_number }}-aarch64" + DEFAULT_NAME="Release v${{ github.run_number }} (aarch64)" + TAG="${{ github.event.inputs.release_tag }}" + NAME="${{ github.event.inputs.release_name }}" + TAG="${TAG:-$DEFAULT_TAG}" + NAME="${NAME:-$DEFAULT_NAME}" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "name=$NAME" >> $GITHUB_OUTPUT + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.release_meta.outputs.tag }} + release_name: ${{ steps.release_meta.outputs.name }} + draft: false + prerelease: false + + - name: Upload aarch64 wheel to release + run: | + gh release upload "${{ steps.release_meta.outputs.tag }}" \ + "${{ steps.wheel-path.outputs.wheel_path }}" \ + --clobber + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/build_aarch64_wheel.py b/scripts/build_aarch64_wheel.py new file mode 100644 index 0000000..06da5af --- /dev/null +++ b/scripts/build_aarch64_wheel.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Post-process a built mldebug_xdp wheel into a linux_aarch64 wheel. + +Takes the standard ``py3-none-any`` wheel produced by ``uv build``, removes +the Windows and x86 Linux binaries that are not usable on aarch64, and +re-tags the wheel as ``py3-none-linux_aarch64``. + +Usage: + python scripts/build_aarch64_wheel.py dist/mldebug_xdp-0.1.0-py3-none-any.whl + python scripts/build_aarch64_wheel.py dist/ # picks the first .whl in dir +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + +# Files inside the wheel (relative to wheel root) that are NOT needed on aarch64. +EXCLUDE_FILES = [ + "mldebug/bin/c++filt.exe", + "mldebug/bin/llvm-objdump.elf", + "mldebug/bin/llvm-objdump.exe", + "mldebug/backend/xrt_backend.cp310-win_amd64.pyd", + "mldebug/backend/xrt_backend.cpython-310-x86_64-linux-gnu.so", +] + +PLATFORM_TAG = "linux_aarch64" +PYTHON_TAG = "py3" +ABI_TAG = "none" + + +def resolve_input_wheel(arg: Path) -> Path: + if arg.is_dir(): + wheels = sorted(arg.glob("*.whl")) + if not wheels: + sys.exit(f"no .whl found in {arg}") + return wheels[0] + if not arg.is_file(): + sys.exit(f"wheel not found: {arg}") + return arg + + +def rewrite_wheel_metadata(dist_info: Path) -> None: + wheel_meta = dist_info / "WHEEL" + lines = wheel_meta.read_text().splitlines() + out: list[str] = [] + saw_root = False + saw_tag = False + for line in lines: + if line.startswith("Tag:"): + if not saw_tag: + out.append(f"Tag: {PYTHON_TAG}-{ABI_TAG}-{PLATFORM_TAG}") + saw_tag = True + # drop any additional Tag lines from the original wheel + continue + if line.startswith("Root-Is-Purelib:"): + out.append("Root-Is-Purelib: false") + saw_root = True + continue + out.append(line) + if not saw_root: + out.append("Root-Is-Purelib: false") + wheel_meta.write_text("\n".join(out) + "\n") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("wheel", type=Path, help="Path to source wheel or dist directory") + ap.add_argument( + "--out-dir", + type=Path, + default=Path("dist"), + help="Directory to write the aarch64 wheel into (default: dist)", + ) + args = ap.parse_args() + + src_wheel = resolve_input_wheel(args.wheel) + out_dir = args.out_dir.resolve() + out_dir.mkdir(parents=True, exist_ok=True) + + work = out_dir / "_aarch64_unpack" + if work.exists(): + shutil.rmtree(work) + work.mkdir() + + print(f"unpacking {src_wheel.name}") + subprocess.check_call( + [sys.executable, "-m", "wheel", "unpack", str(src_wheel), "--dest", str(work)], + ) + + unpacked_dirs = [p for p in work.iterdir() if p.is_dir()] + if len(unpacked_dirs) != 1: + sys.exit(f"expected one unpacked dir under {work}, got {unpacked_dirs}") + unpacked = unpacked_dirs[0] + + for rel in EXCLUDE_FILES: + target = unpacked / rel + if target.exists(): + target.unlink() + print(f"removed {rel}") + else: + print(f"warning: not found in wheel, skipping: {rel}", file=sys.stderr) + + dist_info_dirs = list(unpacked.glob("*.dist-info")) + if len(dist_info_dirs) != 1: + sys.exit(f"expected one *.dist-info under {unpacked}, got {dist_info_dirs}") + rewrite_wheel_metadata(dist_info_dirs[0]) + + # Re-pack. ``wheel pack`` regenerates RECORD and uses the WHEEL Tag + # entry to pick the output filename, so the result is automatically + # named mldebug_xdp--py3-none-linux_aarch64.whl. + print("repacking with aarch64 platform tag") + subprocess.check_call( + [sys.executable, "-m", "wheel", "pack", str(unpacked), "--dest-dir", str(out_dir)], + ) + + shutil.rmtree(work) + + produced = sorted(out_dir.glob(f"*-{PLATFORM_TAG}.whl")) + if not produced: + sys.exit("aarch64 wheel was not produced") + print(f"wrote {produced[-1]}") + + +if __name__ == "__main__": + main()