diff --git a/.github/workflows/build-cbcbox.yml b/.github/workflows/build-cbcbox.yml new file mode 100644 index 000000000000..f8b2db84d7ad --- /dev/null +++ b/.github/workflows/build-cbcbox.yml @@ -0,0 +1,295 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the "Linux ARM64" jobs of +# https://github.com/h-g-s/cbcbox/blob/cd7056156bebb4347c2273ccbc0ca5e06170970d/.github/workflows/wheel.yml +name: Build cbcbox wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/cbcbox.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-cbcbox.yml' + - 'docs/packages/cbcbox.yaml' + - 'patches/cbcbox/**' + push: + branches: [main] + paths: + - '.github/workflows/build-cbcbox.yml' + - 'docs/packages/cbcbox.yaml' + - 'patches/cbcbox/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # cbcbox bumps pyproject.toml's version on master without ever pushing a + # git tag for it (v2.931/v2.932/v2.933 are tagged, 2.934/2.935 are not), so + # pin the exact commit that carries version = "2.935": cd70561 bumped + # 2.934 -> 2.935 on 2026-08-16T22:43:04Z and the 2.935 wheels were uploaded + # to PyPI ~3h20m later (2026-08-17T02:08Z), with no further pyproject.toml + # change in between. Update this alongside the docs/packages/cbcbox.yaml + # version entry. + CBCBOX_REF: cd7056156bebb4347c2273ccbc0ca5e06170970d + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: cbcbox + version: ${{ inputs.version }} + + compile_linux_riscv64_generic: + name: Compile cbcbox ${{ matrix.version }} Linux riscv64 generic + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + runs-on: ubuntu-24.04-riscv + timeout-minutes: 300 + env: + CBCBOX_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout cbcbox ${{ env.CBCBOX_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: h-g-s/cbcbox + ref: ${{ env.CBCBOX_REF }} + persist-credentials: false + + - name: Build generic binary (manylinux_2_39_riscv64) + run: | + for i in 1 2 3; do + docker pull "${MANYLINUX_RISCV64_IMAGE}" && break + echo "Attempt $i failed, retrying in 30s..." + sleep 30 + done + docker run --rm \ + -v "$PWD:/project" \ + -e CBCBOX_BUILD_VARIANT=generic \ + -e CBCBOX_BUILD_ONLY=1 \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash -c 'export PATH=/opt/python/cp313-cp313/bin:$PATH && + dnf install -y gcc-gfortran && + pip install setuptools wheel patchelf && + cd /project && + python setup.py build_ext' + sudo chown -R "$USER:$USER" cbc_dist/ 2>/dev/null || true + tar -czf cbc_dist_generic.tar.gz cbc_dist/ + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cbcbox-${{ env.CBCBOX_VERSION }}-dist-linux-riscv64-generic + path: cbc_dist_generic.tar.gz + if-no-files-found: error + + compile_linux_riscv64_debug: + name: Compile cbcbox ${{ matrix.version }} Linux riscv64 debug + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + runs-on: ubuntu-24.04-riscv + timeout-minutes: 300 + env: + CBCBOX_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout cbcbox ${{ env.CBCBOX_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: h-g-s/cbcbox + ref: ${{ env.CBCBOX_REF }} + persist-credentials: false + + - name: Build debug binary (manylinux_2_39_riscv64) + run: | + for i in 1 2 3; do + docker pull "${MANYLINUX_RISCV64_IMAGE}" && break + echo "Attempt $i failed, retrying in 30s..." + sleep 30 + done + docker run --rm \ + -v "$PWD:/project" \ + -e CBCBOX_BUILD_VARIANT=debug \ + -e CBCBOX_BUILD_ONLY=1 \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash -c 'export PATH=/opt/python/cp313-cp313/bin:$PATH && + dnf install -y gcc-gfortran && + pip install setuptools wheel patchelf && + cd /project && + python setup.py build_ext && + bash scripts/build_mip_debug_cuts.sh cbc_dist_debug Cbc' + sudo chown -R "$USER:$USER" cbc_dist_debug/ 2>/dev/null || true + tar -czf cbc_dist_debug.tar.gz cbc_dist_debug/ + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cbcbox-${{ env.CBCBOX_VERSION }}-dist-linux-riscv64-debug + path: cbc_dist_debug.tar.gz + if-no-files-found: error + + package_linux_riscv64: + name: Package cbcbox ${{ matrix.version }} py3-none-manylinux_riscv64 + needs: [setup, compile_linux_riscv64_generic, compile_linux_riscv64_debug] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + runs-on: ubuntu-24.04-riscv + # 24h: see the "Test wheel" step below (CLAUDE.md gotcha 598) — the 61-case + # MILP-solve test suite is genuinely slow on this fleet's riscv64 hardware, + # not hung. A single build+test job otherwise gives the test step whatever + # is left of this timeout, so a merely-slow (not stuck) suite can still burn + # the whole budget and end in "cancelled" with the already-built wheel never + # uploaded (run 42301146079: cancelled at 5h00m with 28/61 cases done). + timeout-minutes: 1440 + env: + CBCBOX_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout cbcbox ${{ env.CBCBOX_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: h-g-s/cbcbox + ref: ${{ env.CBCBOX_REF }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + # manylinux2014 (upstream's own x86_64/aarch64 wheel tag) predates + # riscv64; map to manylinux_2_39, the policy this job's image is built + # against. + - name: Patch cbcbox source + run: git apply python-wheels/patches/cbcbox/${{ env.CBCBOX_VERSION }}/*.patch + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cbcbox-${{ env.CBCBOX_VERSION }}-dist-linux-riscv64-generic + path: . + - run: tar -xzf cbc_dist_generic.tar.gz + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cbcbox-${{ env.CBCBOX_VERSION }}-dist-linux-riscv64-debug + path: . + - run: tar -xzf cbc_dist_debug.tar.gz + + - name: Build wheel (compilation skipped — binaries pre-built) + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: . + output-dir: wheelhouse + config-file: "{package}/pyproject.toml" + env: + CIBW_BUILD: "cp313-*" + CIBW_SKIP: "*musllinux*" + CIBW_ARCHS: "riscv64" + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_BEFORE_ALL_LINUX: > + dnf install -y gcc-gfortran && + pip install patchelf + CIBW_REPAIR_WHEEL_COMMAND_LINUX: "" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cbcbox-${{ env.CBCBOX_VERSION }}-py3-none-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + # Split from the build step above and given its own bounded timeout + # (CLAUDE.md gotcha 598), so the wheel is uploaded before this runs and a + # slow (or genuinely stuck) test run can't silently consume the rest of + # the job's 24h budget. Reproduces cibuildwheel's own test phase (pytest + # against the installed wheel, PERF_REPORT_DIR for the solver's perf + # reports) without its wrapping, since testing no longer happens inside + # the build action. + - name: Test wheel (manylinux_2_39_riscv64) + timeout-minutes: 1380 # 23h + run: | + docker run --rm \ + -v "$PWD:/project" \ + -v "$PWD/wheelhouse:/wheelhouse" \ + -e PERF_REPORT_DIR=/output \ + -e OPENBLAS_NUM_THREADS=1 \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash -c 'export PATH=/opt/python/cp313-cp313/bin:$PATH && + mkdir -p /output && + pip install /wheelhouse/*.whl pytest && + cd /project && + pytest tests/ -v' + + gpl_sources: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Collect GPL sources + runs-on: ubuntu-24.04-riscv + + env: + CBCBOX_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # cbcbox bundles libgfortran (OpenBLAS is built from source with + # gfortran) into the wheel; its source comes from the image's own gcc, + # not from cbcbox. + - uses: ./actions/collect-gpl-sources + with: + image: ${{ env.MANYLINUX_RISCV64_IMAGE }} + packages: gcc + output: gpl-sources.tar + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cbcbox-${{ env.CBCBOX_VERSION }}-gpl-sources + path: gpl-sources.tar + if-no-files-found: error + + publish: + name: Publish cbcbox ${{ matrix.version }} + needs: [setup, package_linux_riscv64, gpl_sources] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + matrix: ${{ needs.setup.outputs.matrix }} + artifact-pattern: cbcbox-${{ matrix.version }}-py3-none-manylinux_riscv64 + gpl-sources-artifact: cbcbox-${{ matrix.version }}-gpl-sources + gpl-sources-description: gcc (libgfortran, linked into the bundled OpenBLAS) diff --git a/docs/packages/cbcbox.yaml b/docs/packages/cbcbox.yaml new file mode 100644 index 000000000000..b1f139bea084 --- /dev/null +++ b/docs/packages/cbcbox.yaml @@ -0,0 +1,13 @@ +package-name: cbcbox +source-code: https://github.com/h-g-s/cbcbox +license: EPL-2.0 +versions: +- version: '2.935' + files: + - python: 3.9:manylinux + - python: 3.10:manylinux + - python: 3.11:manylinux + - python: 3.12:manylinux + - python: 3.13:manylinux + - python: 3.14:manylinux + - python: 3.15:manylinux diff --git a/patches/cbcbox/2.935/0001-setup.py-map-riscv64-to-manylinux_2_39_riscv64-in-th.patch b/patches/cbcbox/2.935/0001-setup.py-map-riscv64-to-manylinux_2_39_riscv64-in-th.patch new file mode 100644 index 000000000000..066c2ea7fad5 --- /dev/null +++ b/patches/cbcbox/2.935/0001-setup.py-map-riscv64-to-manylinux_2_39_riscv64-in-th.patch @@ -0,0 +1,40 @@ +From 340631d1648be435e1bc354bf4aa4150aec41603 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 15 Oct 2026 12:00:00 +0000 +Subject: [PATCH] setup.py: map riscv64 to manylinux_2_39_riscv64 in the wheel + platform tag + +genericpy_bdist_wheel.get_tag() only special-cases linux_x86_64 and +linux_aarch64 (both mapped to the manylinux2014 policy this project +targets on those architectures). riscv64 falls through unmapped, so a +CIBUILDWHEEL=1 build on linux_riscv64 would otherwise ship a wheel +tagged plain 'linux_riscv64', which pip rejects as non-manylinux. + +manylinux2014 predates riscv64 entirely (PEP 599 has no riscv64 +mapping), so riscv64 is tagged against manylinux_2_39 instead -- the +oldest manylinux policy with a riscv64 image, matching the +quay.io/pypa/manylinux_2_39_riscv64 container used to build it. + +Upstream-Status: Inappropriate [cbcbox's own CI (wheel.yml) does not build for riscv64 at all -- no GitHub-hosted riscv64 runner exists -- so this mapping has no upstream job to exercise it] + +Signed-off-by: Ludovic Henry +--- + setup.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/setup.py b/setup.py +index 15d501d..a8c01e4 100644 +--- a/setup.py ++++ b/setup.py +@@ -27,6 +27,8 @@ class genericpy_bdist_wheel(_bdist_wheel): + plat = "manylinux2014_x86_64" + elif plat == "linux_aarch64": + plat = "manylinux2014_aarch64" ++ elif plat == "linux_riscv64": ++ plat = "manylinux_2_39_riscv64" + return python, abi, plat + + +-- +2.43.0 +