Skip to content

Commit 07ded41

Browse files
authored
Support abi3 wheel build and use it for less common targets (GH-37)
* Support building against the Limited API by providing fallback implementations. * Avoid raising costly exceptions for large numbers in the GCD Python fallback code. Integer comparisons still tend to be faster than creating a new exception, raising and catching it. * Use math.gcd() in Limited API on Python 3.13+. * Build: Serve many lesser used platforms with abi3 wheels. * Work around Cython forgetting to include "stdlib.h".
1 parent 6e2db49 commit 07ded41

File tree

8 files changed

+1043
-50
lines changed

8 files changed

+1043
-50
lines changed

.github/workflows/wheels.yml

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ jobs:
4444
contents: write
4545

4646
steps:
47-
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.1.1
47+
- name: Check out project
48+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
4849

4950
- name: Set up Python
5051
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -72,26 +73,31 @@ jobs:
7273
outputs:
7374
include: ${{ steps.set-matrix.outputs.include }}
7475
steps:
75-
- uses: actions/checkout@v6.0.1
76+
- name: Check out project
77+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
7678
- name: Install cibuildwheel
7779
# Nb. keep cibuildwheel version pin consistent with job below
78-
run: pipx install cibuildwheel==3.3.0
80+
run: pipx install cibuildwheel==3.4.1
7981
- id: set-matrix
8082
run: |
8183
MATRIX=$(
8284
{
83-
cibuildwheel --print-build-identifiers --platform linux \
85+
cibuildwheel --print-build-identifiers --platform linux --archs x86_64 \
8486
| jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \
8587
| sed -e '/aarch64\|armv7l/s|ubuntu-latest|ubuntu-24.04-arm|' \
8688
&& cibuildwheel --print-build-identifiers --platform macos \
8789
| jq -nRc '{"only": inputs, "os": "macos-latest"}' \
88-
&& cibuildwheel --print-build-identifiers --platform windows \
90+
&& cibuildwheel --print-build-identifiers --platform windows --archs x86,AMD64 \
8991
| jq -nRc '{"only": inputs, "os": "windows-2022"}' \
9092
&& cibuildwheel --print-build-identifiers --platform windows --archs ARM64 \
9193
| jq -nRc '{"only": inputs, "os": "windows-11-arm"}'
9294
} | jq -sc
9395
)
96+
echo "include=$MATRIX"
9497
echo "include=$MATRIX" >> $GITHUB_OUTPUT
98+
env:
99+
# Skip abi3 targets here:
100+
CIBW_SKIP: "cp3{9,1?}-win_arm64 cp3{9,1?}-win32 cp3{9,1?}-macosx_x86_64 pp* *musllinux*"
95101

96102
build_wheels:
97103
name: Build ${{ matrix.only }}
@@ -104,8 +110,8 @@ jobs:
104110
include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }}
105111

106112
steps:
107-
- name: Check out the repo
108-
uses: actions/checkout@v6.0.1
113+
- name: Check out project
114+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
109115

110116
- name: Set up QEMU
111117
if: runner.os == 'Linux'
@@ -114,14 +120,14 @@ jobs:
114120
platforms: all
115121

116122
- name: Build wheels
117-
uses: pypa/cibuildwheel@v3.3.0
123+
uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1
118124
with:
119125
only: ${{ matrix.only }}
120126

121127
- name: Build faster Linux wheels
122128
# also build wheels with the most recent manylinux images and gcc
123129
if: runner.os == 'Linux' && !contains(matrix.only, 'i686')
124-
uses: pypa/cibuildwheel@v3.3.0
130+
uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1
125131
env:
126132
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_34
127133
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_34 # manylinux_2_39 ?
@@ -143,9 +149,88 @@ jobs:
143149
path: ./wheelhouse/*.whl
144150
name: wheels-${{ matrix.only }}
145151

152+
build_limited_api_wheels:
153+
name: Build ${{ matrix.target }} Stable ABI ${{ matrix.lapiversion }} wheels
154+
if: >-
155+
github.event_name == 'push' ||
156+
github.event_name == 'release' ||
157+
(github.event_name == 'schedule' && github.repository == 'cython/cython') ||
158+
github.event_name == 'workflow_dispatch'
159+
160+
strategy:
161+
fail-fast: false
162+
matrix:
163+
target:
164+
# Smaller set of platforms that we only provide Stable ABI wheels for:
165+
- 'musllinux_x86_64'
166+
- 'musllinux_aarch64'
167+
- 'manylinux_i686'
168+
- 'musllinux_i686'
169+
- 'manylinux_ppc64le'
170+
- 'musllinux_ppc64le'
171+
- 'manylinux_riscv64'
172+
- 'musllinux_riscv64'
173+
- 'manylinux_armv7l'
174+
- 'musllinux_armv7l'
175+
- 'macosx_x86_64'
176+
- 'win32'
177+
- 'win_arm64'
178+
lapiversion:
179+
- "3.9"
180+
- "3.12"
181+
182+
runs-on: ${{
183+
contains(matrix.target, 'aarch64') && 'ubuntu-24.04-arm' ||
184+
contains(matrix.target, 'armv7l') && 'ubuntu-24.04-arm' ||
185+
contains(matrix.target, 'win_arm64') && 'windows-11-arm' ||
186+
contains(matrix.target, 'win32') && 'windows-latest' ||
187+
contains(matrix.target, 'macosx') && 'macos-latest' ||
188+
'ubuntu-latest'
189+
}}
190+
191+
steps:
192+
- name: Check out project
193+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
194+
195+
- name: Set up Python
196+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
197+
with:
198+
python-version: '3.x'
199+
200+
- name: Set up QEMU
201+
if: runner.os == 'Linux'
202+
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
203+
with:
204+
platforms: all
205+
206+
- name: Setup Visual Studio on Windows
207+
if: startsWith(matrix.target, 'win')
208+
uses: TheMrMilchmann/setup-msvc-dev@79dac248aac9d0059f86eae9d8b5bfab4e95e97c # v4.0.0
209+
with:
210+
arch: ${{ matrix.target == 'win32' && 'x86' || matrix.target == 'win_arm64' && 'arm64' || matrix.target == 'win_amd64' && 'amd64' || '' }}
211+
212+
- name: Build wheels
213+
uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1
214+
env:
215+
CIBW_BUILD: "*${{ matrix.target }}"
216+
CIBW_SKIP: "cp31*t-* pp3*"
217+
CIBW_PROJECT_REQUIRES_PYTHON: ">=${{ matrix.lapiversion }}"
218+
QUICKTIONS_LIMITED_API: "${{ matrix.lapiversion }}"
219+
220+
- name: Check wheel
221+
run: |
222+
python -m pip install twine
223+
python -m twine check ./wheelhouse/*.whl
224+
225+
- name: Upload wheels
226+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
227+
with:
228+
name: Stable-ABI-${{ matrix.target }}-${{ matrix.lapiversion }}
229+
path: ./wheelhouse/*.whl
230+
146231
merge_wheels:
147232
name: Merge wheel archives
148-
needs: build_wheels
233+
needs: [build_wheels, build_limited_api_wheels]
149234
runs-on: ubuntu-latest
150235

151236
steps:
@@ -167,8 +252,8 @@ jobs:
167252
contents: write
168253

169254
steps:
170-
- name: Check out the repo
171-
uses: actions/checkout@v6.0.1
255+
- name: Check out project
256+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
172257

173258
- name: Download files
174259
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include MANIFEST.in LICENSE *.rst
22
include setup.py *.yml tox.ini *.cmd *.txt .coveragerc
3-
recursive-include src *.py *.pyx *.pxd *.c *.txt *.html
3+
recursive-include src *.py *.pyx *.pxd *.c *.h *.txt *.html
44
recursive-include benchmark *.py telco-bench.b

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ clean:
4141
realclean: clean
4242
rm -fr src/*.c src/*.html
4343

44+
src/todecimal.h: gen_todecimal.py
45+
$(PYTHON) $< > $@
46+
4447
qemu-user-static:
4548
docker run --rm --privileged hypriot/qemu-register
4649

gen_todecimal.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
Generate and print a C snippet that maps Unicode digit code points to ASCII digits.
3+
"""
4+
5+
import sys
6+
import unicodedata
7+
from collections import defaultdict
8+
9+
10+
def list_digits():
11+
category_of = unicodedata.category
12+
return [
13+
ch for ch in range(128, 1114111+1)
14+
if category_of(chr(ch)) == 'Nd'
15+
]
16+
17+
18+
def map_to_ascii_digit(digits):
19+
adigit_to_udigit = defaultdict(list)
20+
for ch in digits:
21+
adigit = int(chr(ch))
22+
if ch & 15 == adigit:
23+
adigit_to_udigit['{digit} & 15'].append(ch)
24+
elif (ch - 6) & 15 == adigit:
25+
adigit_to_udigit['({digit} - 6) & 15'].append(ch)
26+
elif (ch - 0x116da) & 15 == adigit:
27+
adigit_to_udigit['({digit} - 0x116da) & 15'].append(ch)
28+
elif (ch - 0x1e5f1) & 15 == adigit:
29+
adigit_to_udigit['({digit} - 0x1e5f1) & 15'].append(ch)
30+
elif ch >= 0x1d7ce and (ch - 0x1d7ce) % 10 == adigit:
31+
adigit_to_udigit['({digit} - 0x1d7ce) % 10'].append(ch)
32+
else:
33+
adigit_to_udigit[str(adigit)].append(ch)
34+
return adigit_to_udigit
35+
36+
37+
def gen_switch_cases(digits_by_adigit):
38+
pyver = sys.version_info
39+
print(f"/* Switch cases generated from Python {pyver[0]}.{pyver[1]}.{pyver[2]} {pyver[3]} {pyver[4]} */")
40+
print("/* Deliberately excluding ASCII digit characters. */")
41+
print()
42+
print(f"/* Lowest digit: {min(ch for characters in digits_by_adigit.values() for ch in characters)} */")
43+
print("switch (digit) {")
44+
45+
for adigit_format, udigits in digits_by_adigit.items():
46+
for ch in udigits:
47+
print(f' case 0x{ch:x}:')
48+
49+
print(f" return {adigit_format.format(digit='digit')};")
50+
51+
print(" default:")
52+
print(" return -1;")
53+
print("}")
54+
55+
56+
if __name__ == '__main__':
57+
gen_switch_cases(map_to_ascii_digit(list_digits()))

pyproject.toml

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,28 @@ requires = ["Cython>=3.2.4", "setuptools"]
44
[tool.cibuildwheel]
55
build-verbosity = 2
66
skip = ["pp*", "*-musllinux_i686", "cp35*", "cp36*", "cp37*"]
7-
enable =["cpython-freethreading", "cpython-prerelease"]
7+
enable =[
8+
"cpython-prerelease",
9+
]
10+
environment-pass = "QUICKTIONS_LIMITED_API"
811
# test-command = "python -m unittest {package}/test_fractions.py -p -v"
912

13+
[tool.cibuildwheel.windows]
14+
archs = ["AMD64", "x86", "ARM64"]
15+
16+
[tool.cibuildwheel.macos]
17+
# https://cibuildwheel.readthedocs.io/en/stable/faq/#what-to-provide suggests to provide
18+
# x86_64 and one of universal2 or arm64 wheels. x86_64 is still required by older pips,
19+
# so additional arm64 wheels suffice.
20+
#archs = ["x86_64", "universal2"]
21+
archs = ["x86_64", "arm64"]
22+
1023
[tool.cibuildwheel.linux]
1124
archs = ["x86_64", "aarch64", "i686", "ppc64le", "armv7l", "riscv64"]
1225
repair-wheel-command = "auditwheel repair --strip -w {dest_dir} {wheel}"
1326

1427
[tool.cibuildwheel.linux.environment]
15-
CFLAGS = "-O3 -g1 -pipe -fPIC"
28+
CFLAGS = "-O3 -g1 -pipe -fPIC -std=c99"
1629
AR = "gcc-ar"
1730
NM = "gcc-nm"
1831
RANLIB = "gcc-ranlib"
@@ -32,12 +45,10 @@ select = "*aarch64"
3245
inherit.environment = "append"
3346
environment.CFLAGS = "-O3 -g1 -pipe -fPIC -march=armv8-a -mtune=cortex-a72"
3447

35-
[tool.cibuildwheel.windows]
36-
archs = ["AMD64", "x86"]
37-
38-
[tool.cibuildwheel.macos]
39-
# https://cibuildwheel.readthedocs.io/en/stable/faq/#what-to-provide suggests to provide
40-
# x86_64 and one of universal2 or arm64 wheels. x86_64 is still required by older pips,
41-
# so additional arm64 wheels suffice.
42-
#archs = ["x86_64", "universal2"]
43-
archs = ["x86_64", "arm64"]
48+
[[tool.cibuildwheel.overrides]]
49+
#select = "*i686 *musllinux* *win32 *win_arm64 *macosx_x86_64 *armv7l"
50+
select = "cp3{9,1?}-*i686 cp3{9,1?}-*musllinux* cp3{9,1?}-*macosx_x86_64 cp3{9,1?}-*armv7l cp3{9,1?}-*ppc64le cp3{9,1?}-*riscv64"
51+
inherit.test-requires = "append"
52+
test-requires = ["abi3audit>=0.0.25"]
53+
inherit.test-command = "append"
54+
test-command = ["abi3audit --strict --report {wheel}"]

setup.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
from setuptools import setup, Extension
77

88

9-
ext_modules = [
10-
Extension("quicktions", ["src/quicktions.pyx"]),
11-
]
12-
139
try:
1410
sys.argv.remove("--with-profile")
1511
except ValueError:
@@ -20,6 +16,44 @@
2016
enable_coverage = os.environ.get("WITH_COVERAGE") == "1"
2117
force_rebuild = os.environ.get("FORCE_REBUILD") == "1"
2218

19+
def check_limited_api_option(value):
20+
if not value:
21+
return None
22+
value = value.lower()
23+
if value == "true":
24+
# The default Limited API version is 3.9, unless we're on a lower Python version
25+
# (which is mainly for the sake of testing 3.8 on the CI)
26+
if sys.version_info >= (3, 9):
27+
return (3, 9)
28+
else:
29+
return sys.version_info[:2]
30+
if value == 'false':
31+
return None
32+
major, minor = value.split('.', 1)
33+
return (int(major), int(minor))
34+
35+
extra_setup_args = {}
36+
c_defines = []
37+
38+
option_limited_api = check_limited_api_option(os.environ.get("QUICKTIONS_LIMITED_API"))
39+
if option_limited_api:
40+
c_defines.append(('Py_LIMITED_API', f'0x{option_limited_api[0]:02x}{option_limited_api[1]:02x}0000'))
41+
42+
setup_options = extra_setup_args.setdefault('options', {})
43+
bdist_wheel_options = setup_options.setdefault('bdist_wheel', {})
44+
bdist_wheel_options['py_limited_api'] = f'cp{option_limited_api[0]}{option_limited_api[1]}'
45+
46+
47+
ext_modules = [
48+
Extension(
49+
"quicktions",
50+
["src/quicktions.pyx"],
51+
py_limited_api=True if option_limited_api else False,
52+
define_macros=c_defines,
53+
),
54+
]
55+
56+
2357
try:
2458
sys.argv.remove("--with-cython")
2559
except ValueError:
@@ -101,4 +135,5 @@
101135
"Topic :: Scientific/Engineering :: Mathematics",
102136
"Topic :: Office/Business :: Financial",
103137
],
138+
**extra_setup_args,
104139
)

0 commit comments

Comments
 (0)