Skip to content
Draft
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
129 changes: 129 additions & 0 deletions .github/workflows/build-greenify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on upstream's own CI:
# https://github.com/douban/greenify/blob/v0.5.0/.github/workflows/publish.yml
name: Build greenify wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/greenify.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-greenify.yml'
- 'docs/packages/greenify.yaml'
- 'patches/greenify/**'
push:
branches: [main]
paths:
- '.github/workflows/build-greenify.yml'
- 'docs/packages/greenify.yaml'
- 'patches/greenify/**'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: greenify
version: ${{ inputs.version }}

# One cp39-abi3 wheel: upstream's own bdist_wheel --py-limited-api=cp39,
# reused across cp312/cp313/cp314. No cp314t: upstream never builds or
# tests a free-threaded interpreter either.
build_abi3:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Build greenify ${{ matrix.version }} cp39-abi3-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 60
env:
GREENIFY_VERSION: ${{ matrix.version }}

steps:
- name: Checkout greenify v${{ env.GREENIFY_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: douban/greenify
ref: v${{ env.GREENIFY_VERSION }}
persist-credentials: false

- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false

- name: Apply patches
run: git apply -v python-wheels/patches/greenify/${{ env.GREENIFY_VERSION }}/*.patch

- name: Build and test wheel
uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
with:
output-dir: wheelhouse/
env:
CIBW_ARCHS: riscv64
CIBW_BUILD: 'cp312-* cp313-* cp314-*'
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
# setup_requires=["Cython"] can't be trusted to self-fetch inside
# an isolated build env (easy_install is gone from modern
# setuptools); install it up front and keep it visible instead.
# setuptools itself must also be installed explicitly: this image's
# interpreters don't ship it pre-installed, and --no-build-isolation
# means pip never fetches a build backend into an isolated env, so
# "import setuptools.build_meta" would otherwise fail outright.
CIBW_BEFORE_BUILD: pip install Cython setuptools
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
# bdist_wheel only tags abi3 when told the floor explicitly
# (gotcha 11); setup.py's own Py_LIMITED_API macro is already
# fixed at 3.9, matching upstream's own --py-limited-api=cp39.
CIBW_CONFIG_SETTINGS: --build-option=--py-limited-api=cp39
# mod_http_head/setup.py imports distutils.core; cp312+ dropped
# the stdlib module, so setuptools (which shims it) must be present.
CIBW_TEST_REQUIRES: gevent setuptools
# misc/test.sh does a relative `cd tests/http_head`, assuming the
# cwd is already the project root; cibuildwheel runs
# CIBW_TEST_COMMAND from a fresh temp directory instead, so the
# cd has to happen here, not inside the script.
CIBW_TEST_COMMAND: cd {project} && bash misc/test.sh

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: greenify-${{ env.GREENIFY_VERSION }}-cp39-abi3-manylinux_riscv64
path: ./wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish greenify ${{ matrix.version }}
needs: [setup, build_abi3]
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:
artifact-pattern: greenify-${{ matrix.version }}-*-manylinux_riscv64
5 changes: 5 additions & 0 deletions docs/packages/greenify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: greenify
source-code: https://github.com/douban/greenify
license: BSD
versions:
- version: 0.5.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
From 4b9ea056867922688404a92227d2a2bd4407bae1 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Tue, 13 Oct 2026 00:00:00 +0000
Subject: [PATCH 1/2] elf_hook: detect 64-bit ELF by __LP64__, not just
__x86_64

Upstream-Status: To upstream [not yet submitted upstream from this session]

elf_hook.c picks the Elf64_* struct family only when __x86_64 is
defined, and falls back to Elf32_* for every other target - including
every other 64-bit architecture (aarch64, riscv64, ...). On a 64-bit
ELF shared object this misreads the section/symbol table layout
entirely (wrong struct sizes and field offsets), so elf_hook()'s
symbol lookup and PLT/GOT patch silently fail on anything but
x86_64. This matches the fact that greenify has never published a
wheel for any architecture other than x86_64 (confirmed via PyPI's
full release history), even though nothing else in the ELF-hook
implementation (the actual GOT/PLT relocation patch in elf_hook())
is architecture-specific - it only touches the GOT/PLT entry via the
generic ELF relocation mechanism, not an instruction encoding.

__LP64__ is defined by GCC/Clang on every LP64 target (x86_64,
aarch64, riscv64, ...), so this keeps x86_64's existing behaviour
and extends the 64-bit path to riscv64 without touching anything
else.

Signed-off-by: Ludovic Henry <git@ludovic.dev>
---
src/elf_hook.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/elf_hook.c b/src/elf_hook.c
index 8c4dac1..2d30a93 100644
--- a/src/elf_hook.c
+++ b/src/elf_hook.c
@@ -12,7 +12,7 @@
#include <unistd.h>
#include <errno.h>
//rename standart types for convenience
-#ifdef __x86_64
+#if defined(__x86_64) || defined(__LP64__)
#define Elf_Ehdr Elf64_Ehdr
#define Elf_Shdr Elf64_Shdr
#define Elf_Sym Elf64_Sym
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
From 6d756b2c41421f70319aa430394297dfb9a59788 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Tue, 13 Oct 2026 00:00:01 +0000
Subject: [PATCH 2/2] tests/http_head: drop the public-site HEAD checks

Upstream-Status: Inappropriate [riscv64 CI runner here has no general internet egress for arbitrary public-site HTTP]

test.py's test_sites tuple HEADs google.com/twitter.com/douban.com/
github.com over plain port-80 HTTP alongside the local fake slow
server, to prove the patched C extension's blocking connect/send/recv
don't block the gevent hub even against real external hosts. That
needs outbound internet access to specific third-party sites from the
CI container, which this repo's riscv64 runners aren't provisioned
for, and would make the job flaky against hosts we don't control
either way.

The local_addr case already exercises the exact same code path
(greenify.patch_lib() against mod_http_head, then concurrent
gevent.spawn() of the C and the plain-Python HEAD check against a
real blocking server) and is what the elapsed-time assertion is
actually checking, so it alone keeps the test meaningful.

Signed-off-by: Ludovic Henry <git@ludovic.dev>
---
tests/http_head/test.py | 4 ----
1 file changed, 4 deletions(-)

diff --git a/tests/http_head/test.py b/tests/http_head/test.py
index c95f947..ca2bb19 100644
--- a/tests/http_head/test.py
+++ b/tests/http_head/test.py
@@ -62,10 +62,6 @@ def main():
local_addr = ("localhost", str(fake_slow_http_server.PORT))
test_sites = (
local_addr,
- ("google.com", "80"),
- ("twitter.com", "80"),
- ("douban.com", "80"),
- ("github.com", "80"),
)
for fn in [python_http_head_check, c_http_head_check]:
stack = []
Loading