Skip to content

Commit 66f0663

Browse files
committed
feat: Extending cPython builds to match all the interpreter envs
1 parent 93e1a96 commit 66f0663

3 files changed

Lines changed: 95 additions & 8 deletions

File tree

.github/workflows/build-wheels-python-dependent.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
runs-on: ${{ matrix.runner }}
1919
env:
2020
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
# PyO3 (cryptography, etc.): allow building against CPython newer than PyO3's declared max when using stable ABI
22+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
2123
strategy:
2224
fail-fast: false
2325
matrix:
@@ -148,7 +150,7 @@ jobs:
148150
bash os_dependencies/linux_arm.sh
149151
# Source Rust environment after installation
150152
. \$HOME/.cargo/env
151-
python build_wheels_from_file.py dependent_requirements_${{ matrix.arch }}
153+
python build_wheels_from_file.py --force-interpreter-binary dependent_requirements_${{ matrix.arch }}
152154
"
153155
154156
- name: Build Python dependent wheels - ARMv7 Legacy (in Docker)
@@ -171,7 +173,7 @@ jobs:
171173
bash os_dependencies/linux_arm.sh
172174
# Source Rust environment after installation
173175
. \$HOME/.cargo/env
174-
python build_wheels_from_file.py dependent_requirements_${{ matrix.arch }}
176+
python build_wheels_from_file.py --force-interpreter-binary dependent_requirements_${{ matrix.arch }}
175177
"
176178
177179
- name: Build Python dependent wheels - Linux/macOS
@@ -184,11 +186,11 @@ jobs:
184186
export ARCHFLAGS="-arch x86_64"
185187
fi
186188
187-
python build_wheels_from_file.py dependent_requirements_${{ matrix.arch }}
189+
python build_wheels_from_file.py --force-interpreter-binary dependent_requirements_${{ matrix.arch }}
188190
189191
- name: Build Python dependent wheels for ${{ matrix.python-version }} - Windows
190192
if: matrix.os == 'Windows'
191-
run: python build_wheels_from_file.py dependent_requirements_${{ matrix.arch }}
193+
run: python build_wheels_from_file.py --force-interpreter-binary dependent_requirements_${{ matrix.arch }}
192194

193195

194196
- name: Upload artifacts

_helper_functions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,23 @@ def get_no_binary_args(requirement_name: str) -> list:
107107
return []
108108

109109

110+
def _safe_text_for_stdout(text: str) -> str:
111+
"""Avoid UnicodeEncodeError when printing pip/tool output on Windows (e.g. cp1252 console)."""
112+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
113+
if encoding.lower() in ("utf-8", "utf8"):
114+
return text
115+
try:
116+
text.encode(encoding)
117+
return text
118+
except UnicodeEncodeError:
119+
return text.encode(encoding, errors="replace").decode(encoding, errors="replace")
120+
121+
110122
def print_color(text: str, color: str = Fore.BLUE):
111123
"""Print colored text specified by color argument based on colorama
112124
- default color BLUE
113125
"""
114-
print(f"{color}", f"{text}", Style.RESET_ALL)
126+
print(f"{color}", f"{_safe_text_for_stdout(text)}", Style.RESET_ALL)
115127

116128

117129
def merge_requirements(requirement: Requirement, another_req: Requirement) -> Requirement:

build_wheels_from_file.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,64 @@
33
#
44
# SPDX-License-Identifier: Apache-2.0
55
#
6+
from __future__ import annotations
7+
68
import argparse
79
import os
10+
import platform
811
import subprocess
912
import sys
1013

1114
from colorama import Fore
15+
from packaging.requirements import InvalidRequirement
16+
from packaging.requirements import Requirement
17+
from packaging.utils import canonicalize_name
1218

1319
from _helper_functions import get_no_binary_args
1420
from _helper_functions import print_color
1521

22+
# Do not pass --no-binary for these in --force-interpreter-binary mode:
23+
# - sdists whose legacy setup breaks under PEP 517 isolation (pkg_resources in isolated env).
24+
# - sdists that fail to compile on CI when a usable wheel exists (e.g. ruamel.yaml.clib + clang).
25+
# - PyObjC: all pyobjc / pyobjc-framework-* use pyobjc_setup.py + pkg_resources (macOS).
26+
# - cryptography: ships abi3 wheels; forcing sdist hits PyO3 "max supported Python" until upgraded
27+
_FORCE_INTERPRETER_BINARY_SKIP_EXACT = frozenset(
28+
{
29+
# cryptography: ships abi3 wheels; forcing sdist hits PyO3 "max supported Python" until upgraded
30+
# (use PYO3_USE_ABI3_FORWARD_COMPATIBILITY in CI if you must rebuild from source).
31+
# canonicalize_name("cryptography"),
32+
canonicalize_name("protobuf"),
33+
canonicalize_name("ruamel.yaml.clib"),
34+
}
35+
)
36+
37+
38+
def _force_interpreter_skip_package(canonical_dist_name: str) -> bool:
39+
if canonical_dist_name in _FORCE_INTERPRETER_BINARY_SKIP_EXACT:
40+
return True
41+
# PyObjC meta and framework bindings (pyobjc-framework-corebluetooth, etc.)
42+
return canonical_dist_name == "pyobjc" or canonical_dist_name.startswith("pyobjc-")
43+
44+
45+
def _force_interpreter_no_binary_args(requirement_line: str) -> list[str]:
46+
"""Return pip --no-binary for this package so pip cannot reuse e.g. cp311-abi3 wheels on 3.13."""
47+
line = requirement_line.strip()
48+
if not line:
49+
return []
50+
try:
51+
req = Requirement(line)
52+
except InvalidRequirement:
53+
return []
54+
if _force_interpreter_skip_package(canonicalize_name(req.name)):
55+
return []
56+
return ["--no-binary", req.name]
57+
58+
59+
def _apply_force_interpreter_binary(cli_flag: bool) -> bool:
60+
"""Linux/macOS only: forcing sdist builds for cryptography etc. is unreliable on Windows CI."""
61+
return cli_flag and platform.system() != "Windows"
62+
63+
1664
parser = argparse.ArgumentParser(description="Process build arguments.")
1765
parser.add_argument(
1866
"requirements_path",
@@ -36,6 +84,16 @@
3684
action="store_true",
3785
help="CI exclude-tests mode: fail if all wheels succeed (expect some to fail, e.g. excluded packages)",
3886
)
87+
parser.add_argument(
88+
"--force-interpreter-binary",
89+
action="store_true",
90+
help=(
91+
"For each requirement, pass --no-binary <pkg> so pip builds a wheel for the current "
92+
"interpreter instead of reusing a compatible abi3 / older cpXY wheel from --find-links. "
93+
"Ignored on Windows (source builds for e.g. cryptography are not used in CI there). "
94+
"Some packages are always skipped (e.g. cryptography, protobuf, PyObjC, ruamel.yaml.clib)."
95+
),
96+
)
3997

4098
args = parser.parse_args()
4199

@@ -55,22 +113,31 @@
55113
raise SystemExit(f"Python version dependent requirements directory or file not found ({e})")
56114

57115
for requirement in requirements:
116+
requirement = requirement.strip()
117+
if not requirement or requirement.startswith("#"):
118+
continue
58119
# Get no-binary args for packages that should be built from source
59120
no_binary_args = get_no_binary_args(requirement)
121+
force_interpreter_args = (
122+
_force_interpreter_no_binary_args(requirement)
123+
if _apply_force_interpreter_binary(args.force_interpreter_binary)
124+
else []
125+
)
60126

61127
out = subprocess.run(
62128
[
63129
f"{sys.executable}",
64130
"-m",
65131
"pip",
66132
"wheel",
67-
f"{requirement}",
133+
requirement,
68134
"--find-links",
69135
"downloaded_wheels",
70136
"--wheel-dir",
71137
"downloaded_wheels",
72138
]
73-
+ no_binary_args,
139+
+ no_binary_args
140+
+ force_interpreter_args,
74141
stdout=subprocess.PIPE,
75142
stderr=subprocess.PIPE,
76143
)
@@ -100,6 +167,11 @@
100167
for requirement in in_requirements:
101168
# Get no-binary args for packages that should be built from source
102169
no_binary_args = get_no_binary_args(requirement)
170+
force_interpreter_args = (
171+
_force_interpreter_no_binary_args(requirement)
172+
if _apply_force_interpreter_binary(args.force_interpreter_binary)
173+
else []
174+
)
103175

104176
out = subprocess.run(
105177
[
@@ -113,7 +185,8 @@
113185
"--wheel-dir",
114186
"downloaded_wheels",
115187
]
116-
+ no_binary_args,
188+
+ no_binary_args
189+
+ force_interpreter_args,
117190
stdout=subprocess.PIPE,
118191
stderr=subprocess.PIPE,
119192
)

0 commit comments

Comments
 (0)