diff --git a/.github/workflows/build-apache-tvm-ffi.yml b/.github/workflows/build-apache-tvm-ffi.yml new file mode 100644 index 0000000..d5c40aa --- /dev/null +++ b/.github/workflows/build-apache-tvm-ffi.yml @@ -0,0 +1,192 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on upstream's own wheel build/publish setup: +# https://github.com/apache/tvm-ffi/blob/v0.1.12/.github/workflows/publish_wheel.yml +# https://github.com/apache/tvm-ffi/blob/v0.1.12/.github/actions/build-wheel-for-publish/action.yml +name: Build apache-tvm-ffi wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'apache-tvm-ffi version to build (git tag without leading v, e.g. 0.1.12)' + required: true + default: '0.1.12' + pull_request: + paths: + - '.github/workflows/build-apache-tvm-ffi.yml' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.1.12' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # `inputs.version` is empty on pull_request events; default to 0.1.12 there. + TVM_FFI_VERSION: ${{ inputs.version || '0.1.12' }} + UV_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/ + UV_INDEX_STRATEGY: unsafe-best-match + UV_ONLY_BINARY: ':all:' + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + build_sdist: + # The sdist is architecture-independent: it just packages the source tree, + # the vendored submodules (dlpack, libbacktrace) and a setuptools-scm-derived + # _version.py. Build it once on x86 instead of burning a scarce riscv runner + # (see CLAUDE.md gotcha 4). The resulting sdist is self-contained, so the + # riscv wheel job below needs neither git history nor submodules. + name: Build apache-tvm-ffi ${{ inputs.version || '0.1.12' }} sdist + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + sdist_name: ${{ steps.sdist.outputs.sdist_name }} + steps: + - name: Checkout tvm-ffi v${{ env.TVM_FFI_VERSION }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: apache/tvm-ffi + ref: v${{ env.TVM_FFI_VERSION }} + # submodules must be present so pyproject's sdist.include /3rdparty/** + # globs actually bundle dlpack + libbacktrace; setuptools-scm needs the + # full history + tags to derive the version from the checked-out tag. + submodules: recursive + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Install Python + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + python-version: '3.12' + activate-environment: true + enable-cache: false + + - name: Build sdist + id: sdist + run: | + uv pip install build twine + python -m build --sdist + twine check dist/* + sdists=(dist/*.tar.gz) + echo "sdist_name=$(basename "${sdists[0]}")" >> "$GITHUB_OUTPUT" + + - name: Upload sdist artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: apache-tvm-ffi-${{ env.TVM_FFI_VERSION }}-sdist + path: dist/*.tar.gz + if-no-files-found: error + + build_wheels: + needs: build_sdist + name: Build apache-tvm-ffi ${{ inputs.version || '0.1.12' }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + permissions: + contents: read + strategy: + fail-fast: false + matrix: + # Wheels are abi3 (pyproject sets wheel.py-api = "cp312"), so the single + # cp312 build is loadable on 3.12/3.13/3.14; only the free-threaded + # cp314t build needs its own wheel. This is upstream's cibuildwheel + # `build` list (cp38..cp312 + cp314t) collapsed to the two that differ. + python: [cp312, cp314t] + steps: + - name: Download sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: apache-tvm-ffi-${{ env.TVM_FFI_VERSION }}-sdist + path: dist/ + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + # cibuildwheel accepts an sdist tarball directly: it extracts it and + # builds from the self-contained tree (no submodules/git needed here). + package-dir: dist/${{ needs.build_sdist.outputs.sdist_name }} + env: + CIBW_BUILD: ${{ matrix.python }}-* + # pyproject pins [tool.cibuildwheel.linux] archs to x86_64+aarch64; + # override so this native riscv runner builds the riscv64 wheel. + CIBW_ARCHS: riscv64 + CIBW_SKIP: '*-musllinux_*' + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # Override upstream's build-frontend = "build[uv]". cibuildwheel's audit + # step creates a venv on the *host* runner (not in the container) and, + # when the frontend is build[uv], insists on a host `uv` binary + # (venv.py: `assert uv_path is not None`). The self-hosted riscv runner + # has no host uv, so the abi3 wheel builds fine but the run crashes in + # "Auditing wheel...". Plain `build` uses pip/virtualenv on the host + # instead - the way our local container build was validated. + CIBW_BUILD_FRONTEND: build + # The test-command (pytest {package}/tests/python) and test-groups + # (["test"] pulls in torch/numpy/ml_dtypes/pytest/ninja) are inherited + # from the sdist's pyproject.toml, so we build and test exactly the way + # upstream does. + # + # CIBW_ENVIRONMENT applies to BOTH the build and test phases. We only + # point pip/uv at our registry here (needed so test deps resolve, and + # harmless at build time). We deliberately do NOT set ONLY_BINARY here: + # the build backend needs cython>=3.0, which has no riscv64 wheel on + # any index and must be compiled from its sdist inside the container. + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + UV_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + UV_INDEX_STRATEGY=unsafe-best-match + # Test phase only: force wheels-only so a missing riscv64 wheel for a + # heavy test dep (torch/numpy/ml_dtypes) fails fast instead of silently + # kicking off a multi-hour source build under the runner. GPU-only tests + # self-skip via torch.cuda.is_available(). + CIBW_TEST_ENVIRONMENT: >- + PIP_ONLY_BINARY=:all: + UV_ONLY_BINARY=:all: + # Mirror upstream's test-command (pytest {package}/tests/python -vvs) + # but drop two tests that fail only because of this riscv64 runner's + # environment, not the wheel (2300+ others pass; cp314t, which pulls no + # torch, is fully green): + # * test_libinfo.py - every test there shells out to `tvm-ffi-config` + # and asserts the subprocess's stderr is empty. Importing tvm_ffi + # pulls in torch, whose bundled libcpuinfo can't parse this runner's + # /sys CPU topology (core_id reads "-1") and prints + # "Error in cpuinfo: failed to parse ... core_id" to stderr. The + # noise is nondeterministic across the module's subprocess tests, so + # the whole file is dropped via --ignore (an absolute path, so it's + # unambiguous regardless of pytest's rootdir). + # * test_filelock.py::test_concurrent_access - spawns 16 concurrent + # torch-importing workers under a hard 60s wait; the slower riscv64 + # runner blows the timeout. Dropped with -k by its (unique) name - + # rootdir-independent, unlike a path-based --deselect (which silently + # no-ops when the nodeid prefix doesn't match pytest's rootdir). + CIBW_TEST_COMMAND: >- + pytest {package}/tests/python -vvs + --ignore {package}/tests/python/test_libinfo.py + -k "not test_concurrent_access" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: apache-tvm-ffi-${{ env.TVM_FFI_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish apache-tvm-ffi ${{ inputs.version || '0.1.12' }} to GitLab + needs: [build_wheels] + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Publish wheels and open docs PR + uses: riseproject-dev/python-wheels/actions/publish-wheels@main + with: + artifact-pattern: apache-tvm-ffi-${{ env.TVM_FFI_VERSION }}-*-manylinux_riscv64 + gitlab-username: ${{ vars.GITLAB_DEPLOY_USER }} + gitlab-token: ${{ secrets.GITLAB_DEPLOY_TOKEN }} + gitlab-project-id: ${{ vars.GITLAB_PROJECT_ID }} + gh-token: ${{ secrets.GITHUB_TOKEN }}