From 79d59ab2d4f4628c0e00772d0b8c48729858d324 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Tue, 13 Oct 2026 12:27:44 +0000 Subject: [PATCH 1/3] Port greenify 0.5.0 to riscv64 Upstream (douban/greenify) is actively maintained, not abandoned: latest tag v0.5.0 (Dec 2025), tested across cp39-cp314 in its own CI, no CPython-internal struct access anywhere in its C sources. greenify's only architecture-specific code is elf_hook.c's choice of Elf32 vs Elf64 struct types, keyed off __x86_64 alone, so every other 64-bit target (including riscv64) silently misreads 64-bit ELF headers as 32-bit. Patch 0001 fixes that by also matching __LP64__. Patch 0002 trims the test suite's public-site HTTP checks, which need outbound internet access this runner isn't provisioned for; the local-server case alone already exercises the patched code path. The wheel builds exactly as upstream's own CI does it: a single cp39-abi3 wheel via `bdist_wheel --py-limited-api=cp39`, reused across cp312/cp313/cp314. No cp314t: upstream never builds or tests a free-threaded interpreter either. --- .github/workflows/build-greenify.yml | 121 ++++++++++++++++++ docs/packages/greenify.yaml | 5 + ...64-bit-ELF-by-__LP64__-not-just-__x8.patch | 44 +++++++ ...ead-drop-the-public-site-HEAD-checks.patch | 42 ++++++ 4 files changed, 212 insertions(+) create mode 100644 .github/workflows/build-greenify.yml create mode 100644 docs/packages/greenify.yaml create mode 100644 patches/greenify/0.5.0/0001-elf_hook-detect-64-bit-ELF-by-__LP64__-not-just-__x8.patch create mode 100644 patches/greenify/0.5.0/0002-tests-http_head-drop-the-public-site-HEAD-checks.patch diff --git a/.github/workflows/build-greenify.yml b/.github/workflows/build-greenify.yml new file mode 100644 index 000000000000..abff3aba6817 --- /dev/null +++ b/.github/workflows/build-greenify.yml @@ -0,0 +1,121 @@ +# 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. + CIBW_BEFORE_BUILD: pip install Cython + 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 + CIBW_TEST_COMMAND: bash {project}/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 diff --git a/docs/packages/greenify.yaml b/docs/packages/greenify.yaml new file mode 100644 index 000000000000..2d8a38a8abc2 --- /dev/null +++ b/docs/packages/greenify.yaml @@ -0,0 +1,5 @@ +package-name: greenify +source-code: https://github.com/douban/greenify +license: BSD +versions: +- version: 0.5.0 diff --git a/patches/greenify/0.5.0/0001-elf_hook-detect-64-bit-ELF-by-__LP64__-not-just-__x8.patch b/patches/greenify/0.5.0/0001-elf_hook-detect-64-bit-ELF-by-__LP64__-not-just-__x8.patch new file mode 100644 index 000000000000..3dec273e0114 --- /dev/null +++ b/patches/greenify/0.5.0/0001-elf_hook-detect-64-bit-ELF-by-__LP64__-not-just-__x8.patch @@ -0,0 +1,44 @@ +From 4b9ea056867922688404a92227d2a2bd4407bae1 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 +--- + 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 + #include + //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 diff --git a/patches/greenify/0.5.0/0002-tests-http_head-drop-the-public-site-HEAD-checks.patch b/patches/greenify/0.5.0/0002-tests-http_head-drop-the-public-site-HEAD-checks.patch new file mode 100644 index 000000000000..30aad6a00c54 --- /dev/null +++ b/patches/greenify/0.5.0/0002-tests-http_head-drop-the-public-site-HEAD-checks.patch @@ -0,0 +1,42 @@ +From 6d756b2c41421f70319aa430394297dfb9a59788 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 +--- + 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 = [] From 2068ea5713ce33aa253f3d61831032a8596b1cfd Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Wed, 14 Oct 2026 10:26:49 +0000 Subject: [PATCH 2/3] greenify: install setuptools before build, not just Cython --no-build-isolation means pip never fetches a build backend into an isolated env, so setuptools.build_meta must already be importable; this image's interpreters don't ship setuptools pre-installed. --- .github/workflows/build-greenify.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-greenify.yml b/.github/workflows/build-greenify.yml index abff3aba6817..c08a3d615632 100644 --- a/.github/workflows/build-greenify.yml +++ b/.github/workflows/build-greenify.yml @@ -86,7 +86,11 @@ jobs: # 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. - CIBW_BEFORE_BUILD: pip install Cython + # 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 From 0d8b70222b9809812f71260dd2f3b7ef80dd20f0 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 15 Oct 2026 09:02:44 +0000 Subject: [PATCH 3/3] greenify: cd into {project} before running misc/test.sh 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 temporary directory instead of {project}, so the test step failed with "cd: tests/http_head: No such file or directory" before even starting the fake HTTP server. Moving the cd into CIBW_TEST_COMMAND itself (same pattern already used by build-sqlglotc.yml/build-leidenalg.yml/ build-symusic.yml/etc.) fixes it without touching the upstream script. --- .github/workflows/build-greenify.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-greenify.yml b/.github/workflows/build-greenify.yml index c08a3d615632..e6bd0ca2f642 100644 --- a/.github/workflows/build-greenify.yml +++ b/.github/workflows/build-greenify.yml @@ -99,7 +99,11 @@ jobs: # 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 - CIBW_TEST_COMMAND: bash {project}/misc/test.sh + # 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: