Skip to content

Commit 0884c43

Browse files
authored
pyswisseph: add build-pyswisseph.yml for riscv64 wheels (#1391)
* **Package**: `pyswisseph` * **Version**: `2.10.3.2` * **Source**: https://github.com/astrorigin/pyswisseph * **Docs**: https://astrorigin.com/pyswisseph C-extension binding to AstroDienst's Swiss Ephemeris astronomical/astrological calculation library (vendors `libswe` and `swephelp` as git submodules). Upstream publishes no riscv64 wheel. Mirrors [upstream's `build.yml`](https://github.com/astrorigin/pyswisseph/blob/v2.10.03.2/.github/workflows/build.yml): build-from-checkout cibuildwheel, both manylinux and musllinux. **License**: AGPL-3.0 (pyswisseph); vendored `libswe` (Swiss Ephemeris) is dual-licensed AGPL-3.0/commercial, vendored `swephelp` is GPL-2.0-or-later. Upstream's own released wheels ship only pyswisseph's own LICENSE.txt, so a patch adds `license_files` for all three. **Testing** - real coverage from the packaged `tests/` suite (94/126 pass with no ephemeris data files present) - deselected 32 tests that assert exact `swe.FLG_SWIEPH`-precision output, which without Astrodienst's multi-file ephemeris dataset (a large third-party download, not bundled by upstream either) silently falls back to lower-precision Moshier and fails the exact-value assertions **Patches** - `0001-Fix-cs2degstr-1-byte-stack-buffer-overflow.patch` - Upstream-Status: To upstream. `pyswe_cs2degstr`'s output buffer is one byte short of what `swe_cs2degstr` itself writes; every call aborts the process on a stack-protector build. Reproduces on x86/arm64, not riscv64-specific. - `0002-Ship-libswe-s-and-swephelp-s-vendored-license-texts-.patch` - Upstream-Status: To upstream. See License above. Built locally (macOS arm64, cp311) with both patches applied; 94 passed, 32 deselected, 0 failed.
1 parent fc68073 commit 0884c43

3 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# SPDX-FileCopyrightText: 2026 The RISE Project
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# Based on upstream's own wheel build:
5+
# https://github.com/astrorigin/pyswisseph/blob/v2.10.03.2/.github/workflows/build.yml
6+
name: Build pyswisseph wheels (riscv64)
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
version:
12+
description: 'pyswisseph version to build (e.g. 2.10.3.2)'
13+
required: true
14+
default: '2.10.3.2'
15+
pull_request:
16+
paths:
17+
- '.github/workflows/build-pyswisseph.yml'
18+
- 'patches/pyswisseph/**'
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ inputs.version || '2.10.3.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+
PYSWISSEPH_VERSION: ${{ inputs.version || '2.10.3.2' }}
29+
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
30+
MUSLLINUX_RISCV64_IMAGE: quay.io/pypa/musllinux_1_2_riscv64
31+
32+
jobs:
33+
setup:
34+
uses: $/.github/workflows/_setup.yml
35+
36+
build_wheels:
37+
needs: [setup]
38+
name: Build pyswisseph ${{ inputs.version || '2.10.3.2' }} ${{ matrix.python }}-${{ matrix.libc }}_riscv64
39+
runs-on: ubuntu-24.04-riscv
40+
timeout-minutes: 60
41+
strategy:
42+
fail-fast: false
43+
matrix:
44+
python: ["cp312", "cp313", "cp314", "cp314t"]
45+
libc: [manylinux, musllinux]
46+
47+
steps:
48+
# Git tags zero-pad the third (swisseph library) version segment
49+
# that the PyPI version doesn't (v2.10.03.2 vs. 2.10.3.2).
50+
- name: Compute upstream git tag
51+
id: tag
52+
run: |
53+
set -euo pipefail
54+
IFS='.' read -r major minor patch build <<< "${PYSWISSEPH_VERSION}"
55+
echo "value=v${major}.${minor}.$(printf '%02d' "${patch}").${build}" >> "$GITHUB_OUTPUT"
56+
57+
- name: Checkout pyswisseph ${{ steps.tag.outputs.value }}
58+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
59+
with:
60+
repository: astrorigin/pyswisseph
61+
ref: ${{ steps.tag.outputs.value }}
62+
submodules: recursive
63+
persist-credentials: false
64+
65+
- name: Checkout python-wheels
66+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
67+
with:
68+
path: python-wheels
69+
persist-credentials: false
70+
71+
- name: Patch pyswisseph source
72+
run: git apply python-wheels/patches/pyswisseph/${{ env.PYSWISSEPH_VERSION }}/00*.patch
73+
74+
- uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
75+
env:
76+
CIBW_ARCHS: riscv64
77+
CIBW_BUILD: ${{ matrix.python }}-${{ matrix.libc }}_riscv64
78+
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
79+
CIBW_MUSLLINUX_RISCV64_IMAGE: ${{ env.MUSLLINUX_RISCV64_IMAGE }}
80+
CIBW_TEST_REQUIRES: pytest
81+
# Real coverage from the packaged suite (94/126 tests pass with no
82+
# ephemeris data files present). The deselected tests all assert
83+
# exact swe.FLG_SWIEPH-precision output (fixed-star catalog lookups,
84+
# eclipse/rise/heliacal timings, ...), which without the multi-file
85+
# Swiss Ephemeris dataset (a large download from Astrodienst, not
86+
# bundled in the sdist or by upstream's own CI either) silently
87+
# falls back to the lower-precision Moshier ephemeris and fails the
88+
# exact-value assertions - not a riscv64-specific failure.
89+
CIBW_TEST_COMMAND: >-
90+
pytest {package}/tests -v
91+
--deselect tests/test_swe_azalt_refrac.py::TestSweAzaltRefrac::test_01
92+
--deselect tests/test_swe_calc.py::TestSweCalc::test_01
93+
--deselect tests/test_swe_calc_pctr.py::TestSweCalcPctr::test_01
94+
--deselect tests/test_swe_calc_ut.py::TestSweCalcUt::test_01
95+
--deselect tests/test_swe_fixstar.py::TestSweFixstar::test_01
96+
--deselect tests/test_swe_fixstar2.py::TestSweFixstar2::test_01
97+
--deselect tests/test_swe_fixstar2_mag.py::TestSweFixstar2Mag::test_01
98+
--deselect tests/test_swe_fixstar2_ut.py::TestSweFixstar2Ut::test_01
99+
--deselect tests/test_swe_fixstar_mag.py::TestSweFixstarMag::test_01
100+
--deselect tests/test_swe_fixstar_ut.py::TestSweFixstarUt::test_01
101+
--deselect tests/test_swe_gauquelin_sector.py::TestSweGauquelinSector::test_01
102+
--deselect tests/test_swe_gauquelin_sector.py::TestSweGauquelinSector::test_02
103+
--deselect tests/test_swe_get_current_file_data.py::TestSweGetCurrentFileData::test_01
104+
--deselect tests/test_swe_get_orbital_elements.py::TestSweGetOrbitalElements::test_01
105+
--deselect tests/test_swe_heliacal_pheno_ut.py::TestSweHeliacalPhenoUt::test_01
106+
--deselect tests/test_swe_helio_cross.py::TestSweHelioCross::test_01
107+
--deselect tests/test_swe_helio_cross_ut.py::TestSweHelioCrossUt::test_01
108+
--deselect tests/test_swe_house_pos.py::TestSweHousePos::test_01
109+
--deselect tests/test_swe_lun_eclipse.py::TestSweLunEclipse::test_01
110+
--deselect tests/test_swe_lun_occult.py::TestSweLunOccult::test_01
111+
--deselect tests/test_swe_pheno.py::TestSwePheno::test_01
112+
--deselect tests/test_swe_pheno_ut.py::TestSwePhenoUt::test_01
113+
--deselect tests/test_swe_rise_trans.py::TestSweRiseTrans::test_01
114+
--deselect tests/test_swe_rise_trans.py::TestSweRiseTrans::test_02
115+
--deselect tests/test_swe_rise_trans_true_hor.py::TestSweRiseTrans::test_01
116+
--deselect tests/test_swe_rise_trans_true_hor.py::TestSweRiseTrans::test_02
117+
--deselect tests/test_swe_set_sid_mode.py::TestSweSetSidMode::test_01
118+
--deselect tests/test_swe_set_topo.py::TestSweSetTopo::test_01
119+
--deselect tests/test_swe_sol_eclipse.py::TestSweSolEclipse::test_01
120+
--deselect tests/test_swe_solcross.py::TestSweSolcross::test_01
121+
--deselect tests/test_swe_solcross_ut.py::TestSweSolcrossUt::test_01
122+
--deselect tests/test_swe_vis_limit_mag.py::TestSweVisLimitMag::test_01
123+
124+
- name: Check wheel contents
125+
run: |
126+
python3 - wheelhouse/*.whl <<'EOF'
127+
import sys, zipfile
128+
path = sys.argv[1]
129+
names = zipfile.ZipFile(path).namelist()
130+
exts = [n for n in names if n.endswith((".so", ".pyd"))]
131+
assert exts and all("swisseph" in n for n in exts), exts
132+
licences = {n.split(".dist-info/licenses/", 1)[1] for n in names
133+
if ".dist-info/licenses/" in n and not n.endswith("/")}
134+
assert licences == {"LICENSE.txt", "libswe/LICENSE", "swephelp/LICENSE.txt"}, licences
135+
EOF
136+
137+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
138+
with:
139+
name: pyswisseph-${{ env.PYSWISSEPH_VERSION }}-${{ matrix.python }}-${{ matrix.libc }}_riscv64
140+
path: ./wheelhouse/*.whl
141+
if-no-files-found: error
142+
143+
publish:
144+
name: Publish pyswisseph ${{ inputs.version || '2.10.3.2' }}
145+
needs: [setup, build_wheels]
146+
permissions:
147+
contents: write
148+
pull-requests: write
149+
uses: $/.github/workflows/_publish-wheel.yml
150+
with:
151+
artifact-pattern: pyswisseph-${{ inputs.version || '2.10.3.2' }}-*riscv64
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
From afee88b34c8a8b84231b54a9682753a1deb1a47d Mon Sep 17 00:00:00 2001
2+
From: Ludovic Henry <git@ludovic.dev>
3+
Date: Mon, 7 Sep 2026 17:17:03 +0200
4+
Subject: [PATCH 1/2] Fix cs2degstr 1-byte stack buffer overflow
5+
6+
pyswe_cs2degstr's char ret[9] is one byte too small for
7+
swe_cs2degstr's own output: %2d + ODEGREE_STRING (2-byte UTF-8 degree
8+
sign) + %02d + "'" + %02d + NUL is 10 bytes. Any call overflows the
9+
stack buffer by one byte, and a stack-protector build (the default
10+
with most modern gcc/clang) aborts the process immediately -
11+
reproduced locally with swe.cs2degstr(98923700), SIGABRT on both
12+
arm64/macOS clang and via the built extension.
13+
14+
Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against astrorigin/pyswisseph]
15+
---
16+
pyswisseph.c | 2 +-
17+
1 file changed, 1 insertion(+), 1 deletion(-)
18+
19+
diff --git a/pyswisseph.c b/pyswisseph.c
20+
index bb074f6..0f25677 100644
21+
--- a/pyswisseph.c
22+
+++ b/pyswisseph.c
23+
@@ -448,7 +448,7 @@ PyDoc_STRVAR(pyswe_cs2degstr__doc__,
24+
static PyObject * pyswe_cs2degstr FUNCARGS_KEYWDS
25+
{
26+
int cs;
27+
- char ret[9];
28+
+ char ret[10];
29+
static char *kwlist[] = {"cs", NULL};
30+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i", kwlist, &cs))
31+
return NULL;
32+
--
33+
2.50.1 (Apple Git-155)
34+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
From b97b94b3243d1d1c267a45ff12c1d78f2a55b668 Mon Sep 17 00:00:00 2001
2+
From: Ludovic Henry <git@ludovic.dev>
3+
Date: Mon, 7 Sep 2026 17:17:03 +0200
4+
Subject: [PATCH 2/2] Ship libswe's and swephelp's vendored license texts in
5+
wheel metadata
6+
7+
setup.py declares no license_files, so setuptools' default
8+
LICEN[CS]E* glob only picks up the top-level LICENSE.txt (pyswisseph's
9+
own AGPL-3.0 text). The wheel then ships with no notice of
10+
libswe/LICENSE (Swiss Ephemeris's own dual AGPL/commercial license
11+
text - the license that actually governs the vendored ephemeris
12+
calculation code) or swephelp/LICENSE.txt (GPL-2.0-or-later, distinct
13+
from pyswisseph's own AGPL-3.0). List all three explicitly so they
14+
land in dist-info/licenses/.
15+
16+
Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against astrorigin/pyswisseph]
17+
---
18+
setup.py | 1 +
19+
1 file changed, 1 insertion(+)
20+
21+
diff --git a/setup.py b/setup.py
22+
index 29cdc6f..ed6a05c 100644
23+
--- a/setup.py
24+
+++ b/setup.py
25+
@@ -288,6 +288,7 @@ setup(
26+
'Topic :: Software Development :: Libraries :: Python Modules'
27+
],
28+
keywords = 'Astrology Ephemeris Swisseph',
29+
+ license_files = ['LICENSE.txt', 'libswe/LICENSE', 'swephelp/LICENSE.txt'],
30+
ext_modules = [swemodule],
31+
setup_requires = ['wheel'],
32+
python_requires = '>=3.5',
33+
--
34+
2.50.1 (Apple Git-155)
35+

0 commit comments

Comments
 (0)