Skip to content

Commit 0882ef9

Browse files
authored
pairmux: add build-pairmux.yml for riscv64 wheels (#947)
* **Package**: `pairmux` * **Version**: `0.5.2` * **Source**: https://github.com/treeleaves30760/pairmux * **Docs**: https://github.com/treeleaves30760/pairmux Cross-compiles upstream's Go CLI binary and packages it the same way upstream does. Upstream publishes no riscv64 wheel. Mirrors the `validate` job of [upstream's `release.yml`](https://github.com/treeleaves30760/pairmux/blob/v0.5.2/.github/workflows/release.yml). **Differs from upstream** - Builds one wheel directly via `go build` + `build_wheels.py`, skipping goreleaser's .deb/.rpm/Homebrew outputs. **Testing** - Drives the real binary through tmux (`new`/`run`/`peek`/`kill`), not just `version`, since the wheel ships no test suite (no sdist at all). **License**: OK **Patches** - `0001-packaging-add-riscv64-to-the-pypi-wheel-platform-tab.patch` - To upstream [not submitted yet]. Without it `build_wheels.py` has no riscv64 entry and rejects the binary; reproduces on any arch, it's a plain dict lookup. Built on cp312; smoke test passes on cp312/cp313/cp314/cp314t.
1 parent 2ba3155 commit 0882ef9

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# SPDX-FileCopyrightText: 2026 The RISE Project
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# This workflow is based on the `validate` job of
5+
# https://github.com/treeleaves30760/pairmux/blob/v0.5.2/.github/workflows/release.yml
6+
name: Build pairmux wheels (riscv64)
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
version:
12+
description: 'pairmux version to build (git tag without the leading v, e.g. 0.5.2)'
13+
required: true
14+
default: '0.5.2'
15+
pull_request:
16+
paths:
17+
- '.github/workflows/build-pairmux.yml'
18+
- 'patches/pairmux/**'
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ inputs.version || '0.5.2' }}-${{ github.head_ref || github.run_id }}
22+
cancel-in-progress: true
23+
24+
permissions:
25+
contents: read # to fetch code (actions/checkout)
26+
27+
env:
28+
# `inputs.version` is empty on pull_request events; default to 0.5.2 there.
29+
PAIRMUX_VERSION: ${{ inputs.version || '0.5.2' }}
30+
31+
jobs:
32+
setup:
33+
uses: $/.github/workflows/_setup.yml
34+
35+
build_wheel:
36+
needs: [setup]
37+
name: Build pairmux ${{ inputs.version || '0.5.2' }} manylinux_riscv64
38+
runs-on: ubuntu-24.04-riscv
39+
timeout-minutes: 30
40+
41+
steps:
42+
- name: Checkout pairmux v${{ env.PAIRMUX_VERSION }}
43+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
44+
with:
45+
repository: treeleaves30760/pairmux
46+
ref: v${{ env.PAIRMUX_VERSION }}
47+
persist-credentials: false
48+
49+
- name: Checkout python-wheels
50+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
51+
with:
52+
path: python-wheels
53+
persist-credentials: false
54+
55+
# packaging/pypi/build_wheels.py has no riscv64 entry: see the patch.
56+
- name: Patch pairmux source
57+
run: |
58+
git apply python-wheels/patches/pairmux/${{ env.PAIRMUX_VERSION }}/00*.patch
59+
60+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
61+
with:
62+
go-version-file: go.mod
63+
64+
# Mirrors .goreleaser.yaml's `pairmux` build: CGO_ENABLED=0 (no cgo, so
65+
# it cross-compiles cleanly) and the same ldflags/version stamp.
66+
- name: Build the pairmux binary
67+
run: |
68+
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -trimpath \
69+
-ldflags "-s -w -X github.com/treeleaves30760/pairmux/internal/version.Version=${{ env.PAIRMUX_VERSION }}" \
70+
-o dist/pairmux ./cmd/pairmux
71+
72+
- name: Build wheel
73+
run: |
74+
python3 packaging/pypi/build_wheels.py \
75+
--version "$PAIRMUX_VERSION" \
76+
--binary dist/pairmux --platform linux_riscv64 \
77+
--out-dir dist/wheels --check
78+
79+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
80+
with:
81+
name: pairmux-${{ env.PAIRMUX_VERSION }}-manylinux_riscv64
82+
path: dist/wheels/*.whl
83+
if-no-files-found: error
84+
85+
test_wheel:
86+
name: Test pairmux ${{ inputs.version || '0.5.2' }} on Python ${{ matrix.python-version }}
87+
needs: [setup, build_wheel]
88+
runs-on: ubuntu-24.04-riscv
89+
timeout-minutes: 15
90+
env:
91+
# Without this uv would reuse the runner image's system CPython for 3.12
92+
# and download a standalone build for the others.
93+
UV_PYTHON_PREFERENCE: only-managed
94+
strategy:
95+
fail-fast: false
96+
matrix:
97+
# This repo's default interpreter matrix (gotcha in workflow-anatomy.md);
98+
# the wheel carries a single native binary, so every interpreter --
99+
# including free-threaded -- exercises the same binary.
100+
python-version: ['3.12', '3.13', '3.14', '3.14t']
101+
102+
steps:
103+
- name: Install tmux
104+
run: |
105+
sudo apt-get update
106+
sudo apt-get install -y tmux
107+
108+
- name: Download wheel
109+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
110+
with:
111+
name: pairmux-${{ env.PAIRMUX_VERSION }}-manylinux_riscv64
112+
113+
- name: Install Python
114+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
115+
with:
116+
python-version: ${{ matrix.python-version }}
117+
activate-environment: true
118+
enable-cache: false
119+
120+
- name: Install wheel
121+
run: uv pip install --reinstall --no-index --find-links . pairmux
122+
123+
# No wheel-level test suite ships (there is no sdist at all, by design
124+
# -- see the packaging/pypi/build_wheels.py module docstring). Drive the
125+
# real binary through tmux the way the README's "Reproducible
126+
# quickstart" does, instead of settling for a bare version check.
127+
- name: Test wheel
128+
run: |
129+
set -euo pipefail
130+
test "$(pairmux version)" = "$PAIRMUX_VERSION"
131+
132+
pairmux --json new --name demo
133+
out=$(pairmux --json run demo "echo hello from pairmux")
134+
echo "$out"
135+
echo "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["status"]=="done", d'
136+
pairmux --json peek demo | grep -q 'hello from pairmux'
137+
pairmux --json kill demo
138+
139+
publish:
140+
name: Publish pairmux ${{ inputs.version || '0.5.2' }}
141+
needs: [setup, build_wheel, test_wheel]
142+
permissions:
143+
contents: write
144+
pull-requests: write
145+
uses: $/.github/workflows/_publish-wheel.yml
146+
with:
147+
artifact-pattern: pairmux-${{ inputs.version || '0.5.2' }}-manylinux_riscv64
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
From 1549b4f2bf3baca546cfe81a4009466de0456d94 Mon Sep 17 00:00:00 2001
2+
From: Ludovic Henry <git@ludovic.dev>
3+
Date: Fri, 4 Sep 2026 22:35:56 +0200
4+
Subject: [PATCH] packaging: add riscv64 to the pypi wheel platform tables
5+
6+
Upstream-Status: To upstream [not submitted yet; no riscv64 CI to validate it against]
7+
8+
pairmux ships one prebuilt Go binary per platform inside a hand-built
9+
py3-none-<platform> wheel (packaging/pypi/build_wheels.py); the platform
10+
comes from a fixed table keyed on (goos, goarch), read from an ELF/Mach-O
11+
header. GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 cross-compiles pairmux
12+
cleanly (it is a zero-dependency, cgo-free binary, same as the four
13+
platforms already built), but the table has no entry for it and
14+
linux_elf_arch() does not recognize EM_RISCV (243), so build_wheels.py
15+
rejects the binary before it gets that far.
16+
17+
Add the missing PLATFORM_TAGS/ELF_MACHINE_TO_WHEEL_ARCH/
18+
LINUX_PLATFORM_ARCHES entries, tagged manylinux_2_39_riscv64. riscv64
19+
predates the manylinux1/2010/2014 legacy aliases that let the x86_64 and
20+
aarch64 rows compress two sub-tags into one, so this row has only the
21+
single perennial tag.
22+
23+
Signed-off-by: Ludovic Henry <git@ludovic.dev>
24+
---
25+
packaging/pypi/build_wheels.py | 5 +++++
26+
1 file changed, 5 insertions(+)
27+
28+
diff --git a/packaging/pypi/build_wheels.py b/packaging/pypi/build_wheels.py
29+
index 38a1432..9afba9c 100755
30+
--- a/packaging/pypi/build_wheels.py
31+
+++ b/packaging/pypi/build_wheels.py
32+
@@ -84,6 +84,9 @@ PLATFORM_TAGS = {
33+
("darwin", "amd64"): "macosx_12_0_x86_64",
34+
("linux", "amd64"): "manylinux_2_17_x86_64.manylinux2014_x86_64",
35+
("linux", "arm64"): "manylinux_2_17_aarch64.manylinux2014_aarch64",
36+
+ # riscv64 predates the manylinux1/2010/2014 legacy aliases, so there is no
37+
+ # second sub-tag to compress in here (unlike the x86_64/aarch64 rows above).
38+
+ ("linux", "riscv64"): "manylinux_2_39_riscv64",
39+
}
40+
41+
# Trove classifiers — all valid values from the official list, so PyPI accepts
42+
@@ -128,10 +131,12 @@ ELF64_HEADER_SIZE = struct.calcsize(ELF64_HEADER_FORMAT)
43+
ELF_MACHINE_TO_WHEEL_ARCH = {
44+
62: "x86_64", # EM_X86_64
45+
183: "aarch64", # EM_AARCH64
46+
+ 243: "riscv64", # EM_RISCV
47+
}
48+
LINUX_PLATFORM_ARCHES = {
49+
PLATFORM_TAGS[("linux", "amd64")]: "x86_64",
50+
PLATFORM_TAGS[("linux", "arm64")]: "aarch64",
51+
+ PLATFORM_TAGS[("linux", "riscv64")]: "riscv64",
52+
}
53+
54+
# goreleaser build-subdir names end with _<goos>_<goarch>[_<variant>]. Go's
55+
--
56+
2.50.1 (Apple Git-155)
57+

0 commit comments

Comments
 (0)