Skip to content

Commit 1f4bbeb

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

3 files changed

Lines changed: 96 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: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,65 @@
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: abi3 wheels; avoid PyO3 max-Python / heavy Rust rebuilds in dependent jobs.
27+
# - pydantic-core: maturin + jiter + PyO3 can fail from sdist on some CI combos (e.g. ARM64 3.9:
28+
# jiter vs pyo3-ffi PyUnicode_* / extract API). Prefer compatible wheels from find-links or PyPI.
29+
_FORCE_INTERPRETER_BINARY_SKIP_EXACT = frozenset(
30+
{
31+
canonicalize_name("cryptography"),
32+
canonicalize_name("pydantic-core"),
33+
canonicalize_name("protobuf"),
34+
canonicalize_name("ruamel.yaml.clib"),
35+
}
36+
)
37+
38+
39+
def _force_interpreter_skip_package(canonical_dist_name: str) -> bool:
40+
if canonical_dist_name in _FORCE_INTERPRETER_BINARY_SKIP_EXACT:
41+
return True
42+
# PyObjC meta and framework bindings (pyobjc-framework-corebluetooth, etc.)
43+
return canonical_dist_name == "pyobjc" or canonical_dist_name.startswith("pyobjc-")
44+
45+
46+
def _force_interpreter_no_binary_args(requirement_line: str) -> list[str]:
47+
"""Return pip --no-binary for this package so pip cannot reuse e.g. cp311-abi3 wheels on 3.13."""
48+
line = requirement_line.strip()
49+
if not line:
50+
return []
51+
try:
52+
req = Requirement(line)
53+
except InvalidRequirement:
54+
return []
55+
if _force_interpreter_skip_package(canonicalize_name(req.name)):
56+
return []
57+
return ["--no-binary", req.name]
58+
59+
60+
def _apply_force_interpreter_binary(cli_flag: bool) -> bool:
61+
"""Linux/macOS only: forcing sdist builds for cryptography etc. is unreliable on Windows CI."""
62+
return cli_flag and platform.system() != "Windows"
63+
64+
1665
parser = argparse.ArgumentParser(description="Process build arguments.")
1766
parser.add_argument(
1867
"requirements_path",
@@ -36,6 +85,16 @@
3685
action="store_true",
3786
help="CI exclude-tests mode: fail if all wheels succeed (expect some to fail, e.g. excluded packages)",
3887
)
88+
parser.add_argument(
89+
"--force-interpreter-binary",
90+
action="store_true",
91+
help=(
92+
"For each requirement, pass --no-binary <pkg> so pip builds a wheel for the current "
93+
"interpreter instead of reusing a compatible abi3 / older cpXY wheel from --find-links. "
94+
"Ignored on Windows (source builds for e.g. cryptography are not used in CI there). "
95+
"Some packages are always skipped (e.g. cryptography, pydantic-core, protobuf, PyObjC, ruamel.yaml.clib)."
96+
),
97+
)
3998

4099
args = parser.parse_args()
41100

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

57116
for requirement in requirements:
117+
requirement = requirement.strip()
118+
if not requirement or requirement.startswith("#"):
119+
continue
58120
# Get no-binary args for packages that should be built from source
59121
no_binary_args = get_no_binary_args(requirement)
122+
force_interpreter_args = (
123+
_force_interpreter_no_binary_args(requirement)
124+
if _apply_force_interpreter_binary(args.force_interpreter_binary)
125+
else []
126+
)
60127

61128
out = subprocess.run(
62129
[
63130
f"{sys.executable}",
64131
"-m",
65132
"pip",
66133
"wheel",
67-
f"{requirement}",
134+
requirement,
68135
"--find-links",
69136
"downloaded_wheels",
70137
"--wheel-dir",
71138
"downloaded_wheels",
72139
]
73-
+ no_binary_args,
140+
+ no_binary_args
141+
+ force_interpreter_args,
74142
stdout=subprocess.PIPE,
75143
stderr=subprocess.PIPE,
76144
)
@@ -100,6 +168,11 @@
100168
for requirement in in_requirements:
101169
# Get no-binary args for packages that should be built from source
102170
no_binary_args = get_no_binary_args(requirement)
171+
force_interpreter_args = (
172+
_force_interpreter_no_binary_args(requirement)
173+
if _apply_force_interpreter_binary(args.force_interpreter_binary)
174+
else []
175+
)
103176

104177
out = subprocess.run(
105178
[
@@ -113,7 +186,8 @@
113186
"--wheel-dir",
114187
"downloaded_wheels",
115188
]
116-
+ no_binary_args,
189+
+ no_binary_args
190+
+ force_interpreter_args,
117191
stdout=subprocess.PIPE,
118192
stderr=subprocess.PIPE,
119193
)

0 commit comments

Comments
 (0)