Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions .github/workflows/build-pairmux.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
From 1549b4f2bf3baca546cfe81a4009466de0456d94 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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-<platform> 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 <git@ludovic.dev>
---
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 _<goos>_<goarch>[_<variant>]. Go's
--
2.50.1 (Apple Git-155)