diff --git a/.github/workflows/build-pairmux.yml b/.github/workflows/build-pairmux.yml new file mode 100644 index 000000000..552df0ab8 --- /dev/null +++ b/.github/workflows/build-pairmux.yml @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `validate` job of +# https://github.com/treeleaves30760/pairmux/blob/v0.5.2/.github/workflows/release.yml +name: Build pairmux wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'pairmux version to build (git tag without the leading v, e.g. 0.5.2)' + required: true + default: '0.5.2' + pull_request: + paths: + - '.github/workflows/build-pairmux.yml' + - 'patches/pairmux/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.5.2' }}-${{ 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.5.2 there. + PAIRMUX_VERSION: ${{ inputs.version || '0.5.2' }} + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheel: + needs: [setup] + name: Build pairmux ${{ inputs.version || '0.5.2' }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 30 + + steps: + - name: Checkout pairmux v${{ env.PAIRMUX_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: treeleaves30760/pairmux + ref: v${{ env.PAIRMUX_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + # packaging/pypi/build_wheels.py has no riscv64 entry: see the patch. + - name: Patch pairmux source + run: | + git apply python-wheels/patches/pairmux/${{ env.PAIRMUX_VERSION }}/00*.patch + + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + # Mirrors .goreleaser.yaml's `pairmux` build: CGO_ENABLED=0 (no cgo, so + # it cross-compiles cleanly) and the same ldflags/version stamp. + - name: Build the pairmux binary + run: | + CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -trimpath \ + -ldflags "-s -w -X github.com/treeleaves30760/pairmux/internal/version.Version=${{ env.PAIRMUX_VERSION }}" \ + -o dist/pairmux ./cmd/pairmux + + - name: Build wheel + run: | + python3 packaging/pypi/build_wheels.py \ + --version "$PAIRMUX_VERSION" \ + --binary dist/pairmux --platform linux_riscv64 \ + --out-dir dist/wheels --check + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pairmux-${{ env.PAIRMUX_VERSION }}-manylinux_riscv64 + path: dist/wheels/*.whl + if-no-files-found: error + + test_wheel: + name: Test pairmux ${{ inputs.version || '0.5.2' }} on Python ${{ matrix.python-version }} + needs: [setup, build_wheel] + runs-on: ubuntu-24.04-riscv + timeout-minutes: 15 + env: + # Without this uv would reuse the runner image's system CPython for 3.12 + # and download a standalone build for the others. + UV_PYTHON_PREFERENCE: only-managed + strategy: + fail-fast: false + matrix: + # This repo's default interpreter matrix (gotcha in workflow-anatomy.md); + # the wheel carries a single native binary, so every interpreter -- + # including free-threaded -- exercises the same binary. + python-version: ['3.12', '3.13', '3.14', '3.14t'] + + steps: + - name: Install tmux + run: | + sudo apt-get update + sudo apt-get install -y tmux + + - name: Download wheel + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pairmux-${{ env.PAIRMUX_VERSION }}-manylinux_riscv64 + + - name: Install Python + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + python-version: ${{ matrix.python-version }} + activate-environment: true + enable-cache: false + + - name: Install wheel + run: uv pip install --reinstall --no-index --find-links . pairmux + + # No wheel-level test suite ships (there is no sdist at all, by design + # -- see the packaging/pypi/build_wheels.py module docstring). Drive the + # real binary through tmux the way the README's "Reproducible + # quickstart" does, instead of settling for a bare version check. + - name: Test wheel + run: | + set -euo pipefail + test "$(pairmux version)" = "$PAIRMUX_VERSION" + + pairmux --json new --name demo + out=$(pairmux --json run demo "echo hello from pairmux") + echo "$out" + echo "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["status"]=="done", d' + pairmux --json peek demo | grep -q 'hello from pairmux' + pairmux --json kill demo + + publish: + name: Publish pairmux ${{ inputs.version || '0.5.2' }} + needs: [setup, build_wheel, test_wheel] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: pairmux-${{ inputs.version || '0.5.2' }}-manylinux_riscv64 diff --git a/patches/pairmux/0.5.2/0001-packaging-add-riscv64-to-the-pypi-wheel-platform-tab.patch b/patches/pairmux/0.5.2/0001-packaging-add-riscv64-to-the-pypi-wheel-platform-tab.patch new file mode 100644 index 000000000..2e2fdaf8d --- /dev/null +++ b/patches/pairmux/0.5.2/0001-packaging-add-riscv64-to-the-pypi-wheel-platform-tab.patch @@ -0,0 +1,57 @@ +From 1549b4f2bf3baca546cfe81a4009466de0456d94 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 4 Sep 2026 22:35:56 +0200 +Subject: [PATCH] packaging: add riscv64 to the pypi wheel platform tables + +Upstream-Status: To upstream [not submitted yet; no riscv64 CI to validate it against] + +pairmux ships one prebuilt Go binary per platform inside a hand-built +py3-none- wheel (packaging/pypi/build_wheels.py); the platform +comes from a fixed table keyed on (goos, goarch), read from an ELF/Mach-O +header. GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 cross-compiles pairmux +cleanly (it is a zero-dependency, cgo-free binary, same as the four +platforms already built), but the table has no entry for it and +linux_elf_arch() does not recognize EM_RISCV (243), so build_wheels.py +rejects the binary before it gets that far. + +Add the missing PLATFORM_TAGS/ELF_MACHINE_TO_WHEEL_ARCH/ +LINUX_PLATFORM_ARCHES entries, tagged manylinux_2_39_riscv64. riscv64 +predates the manylinux1/2010/2014 legacy aliases that let the x86_64 and +aarch64 rows compress two sub-tags into one, so this row has only the +single perennial tag. + +Signed-off-by: Ludovic Henry +--- + packaging/pypi/build_wheels.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/packaging/pypi/build_wheels.py b/packaging/pypi/build_wheels.py +index 38a1432..9afba9c 100755 +--- a/packaging/pypi/build_wheels.py ++++ b/packaging/pypi/build_wheels.py +@@ -84,6 +84,9 @@ PLATFORM_TAGS = { + ("darwin", "amd64"): "macosx_12_0_x86_64", + ("linux", "amd64"): "manylinux_2_17_x86_64.manylinux2014_x86_64", + ("linux", "arm64"): "manylinux_2_17_aarch64.manylinux2014_aarch64", ++ # riscv64 predates the manylinux1/2010/2014 legacy aliases, so there is no ++ # second sub-tag to compress in here (unlike the x86_64/aarch64 rows above). ++ ("linux", "riscv64"): "manylinux_2_39_riscv64", + } + + # Trove classifiers — all valid values from the official list, so PyPI accepts +@@ -128,10 +131,12 @@ ELF64_HEADER_SIZE = struct.calcsize(ELF64_HEADER_FORMAT) + ELF_MACHINE_TO_WHEEL_ARCH = { + 62: "x86_64", # EM_X86_64 + 183: "aarch64", # EM_AARCH64 ++ 243: "riscv64", # EM_RISCV + } + LINUX_PLATFORM_ARCHES = { + PLATFORM_TAGS[("linux", "amd64")]: "x86_64", + PLATFORM_TAGS[("linux", "arm64")]: "aarch64", ++ PLATFORM_TAGS[("linux", "riscv64")]: "riscv64", + } + + # goreleaser build-subdir names end with __[_]. Go's +-- +2.50.1 (Apple Git-155) +