Skip to content

Commit b660b75

Browse files
committed
feat: support PEP 803, free-threaded tag
Support should come out in CMake 4.4 Assisted-by: Copilot:Kimi-K2.6 Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
1 parent 3a75f15 commit b660b75

8 files changed

Lines changed: 215 additions & 46 deletions

File tree

docs/configuration/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,10 @@ wheel.py-api = "cp38"
372372

373373
Scikit-build-core will only target ABI3 if the version of Python is equal to or
374374
newer than the one you set. `${SKBUILD_SABI_COMPONENT}` is set to
375-
`Development.SABIModule` when targeting ABI3, and is an empty string otherwise.
375+
`Development.SABIModule` when targeting ABI3 or ABI3T, and is an empty string
376+
otherwise. For free-threaded Python (PEP 703), you can use `cp315t` to target
377+
the free-threaded stable ABI, which sets `Py_TARGET_ABI3T` instead of
378+
`Py_LIMITED_API`.
376379

377380
If you are not using CPython at all, you can specify any version of Python is
378381
fine:

docs/guide/build.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ The three new items here (compared to SDists) are the [compatibility tags][]:
189189
`py3` for pure Python wheels, or `py312` (etc) for compiled wheels.
190190
- `abi tag`: The interpreter ABI this was built for. `none` for pure Python
191191
wheels or compiled wheels that don't use the Python API, `abi3` for stable ABI
192-
/ limited API wheels, and `cp312` (etc) for normal compiled wheels.
192+
/ limited API wheels, `abi3t` for free-threaded stable ABI wheels, and `cp312`
193+
(etc) for normal compiled wheels.
193194
- `platform tag`: This is the platform the wheel is valid on, such as `any`,
194195
`linux_x86_64`, or `manylinux_2_17_x86_64`.
195196

src/scikit_build_core/builder/builder.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -209,27 +209,41 @@ def configure(
209209
)
210210
cache_config["SKBUILD_PROJECT_VERSION_FULL"] = str(version)
211211

212+
py_api = self.settings.wheel.py_api
213+
ft_abi = False
212214
if limited_api is None:
213-
if self.settings.wheel.py_api.startswith("cp3"):
214-
target_minor_version = int(self.settings.wheel.py_api[3:])
215+
if py_api.startswith("cp3") and py_api.endswith("t"):
216+
target_minor_version = int(py_api[3:-1])
217+
ft_abi = (
218+
sys.implementation.name == "cpython"
219+
and sysconfig.get_config_var("Py_GIL_DISABLED")
220+
and target_minor_version <= sys.version_info.minor
221+
)
222+
limited_api = ft_abi
223+
elif py_api.startswith("cp3"):
224+
target_minor_version = int(py_api[3:])
215225
limited_api = target_minor_version <= sys.version_info.minor
216226
else:
217227
limited_api = False
218228

219229
if limited_api and sys.implementation.name != "cpython":
220230
limited_api = False
231+
ft_abi = False
221232
logger.info("PyPy doesn't support the Limited API, ignoring")
222233

223-
if limited_api and sysconfig.get_config_var("Py_GIL_DISABLED"):
234+
if limited_api and sysconfig.get_config_var("Py_GIL_DISABLED") and not ft_abi:
224235
limited_api = False
225236
logger.info(
226-
"Free-threaded Python doesn't support the Limited API currently, ignoring"
237+
"Free-threaded Python doesn't support the classic Limited API, ignoring"
227238
)
228239

229240
python_library = get_python_library(self.config.env, abi3=False)
230-
python_sabi_library = (
231-
get_python_library(self.config.env, abi3=True) if limited_api else None
232-
)
241+
python_sabi_library = None
242+
if limited_api:
243+
if ft_abi:
244+
python_sabi_library = get_python_library(self.config.env, abi3t=True)
245+
else:
246+
python_sabi_library = get_python_library(self.config.env, abi3=True)
233247
python_include_dir = get_python_include_dir()
234248
numpy_include_dir = get_numpy_include_dir()
235249

@@ -265,20 +279,26 @@ def configure(
265279
if numpy_include_dir:
266280
cache_config[f"{prefix}_NumPy_INCLUDE_DIR"] = numpy_include_dir
267281

268-
cache_config["SKBUILD_SOABI"] = get_soabi(self.config.env, abi3=limited_api)
282+
cache_config["SKBUILD_SOABI"] = get_soabi(
283+
self.config.env, abi3=(limited_api and not ft_abi), abi3t=ft_abi
284+
)
269285

270286
# Allow CMakeLists to detect this is supposed to be a limited ABI build
271287
cache_config["SKBUILD_SABI_COMPONENT"] = (
272288
"Development.SABIModule" if limited_api else ""
273289
)
274290

275291
# Allow users to detect the version requested in settings
276-
py_api = self.settings.wheel.py_api
277-
cache_config["SKBUILD_SABI_VERSION"] = (
278-
f"{py_api[2]}.{py_api[3:]}"
279-
if limited_api and py_api.startswith("cp")
280-
else ""
281-
)
292+
if limited_api and py_api.startswith("cp"):
293+
version_str = py_api[2:]
294+
if version_str.endswith("t"):
295+
version_str = version_str[:-1]
296+
cache_config["SKBUILD_SABI_VERSION"] = f"{version_str[0]}.{version_str[1:]}"
297+
else:
298+
cache_config["SKBUILD_SABI_VERSION"] = ""
299+
300+
if ft_abi:
301+
cache_config["Py_TARGET_ABI3T"] = "1"
282302

283303
if cache_entries:
284304
cache_config.update(cache_entries)

src/scikit_build_core/builder/sysconfig.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ def __dir__() -> list[str]:
4444
return __all__
4545

4646

47-
def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path | None:
47+
def get_python_library(
48+
env: Mapping[str, str], *, abi3: bool = False, abi3t: bool = False
49+
) -> Path | None:
4850
# When cross-compiling, check DIST_EXTRA_CONFIG first
4951
config_file = env.get("DIST_EXTRA_CONFIG", None)
5052
if config_file and Path(config_file).is_file():
@@ -53,21 +55,24 @@ def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path |
5355
result = cp.get("build_ext", "library_dirs", fallback="")
5456
if result:
5557
logger.info("Reading DIST_EXTRA_CONFIG:build_ext.library_dirs={}", result)
56-
minor = "" if abi3 else sys.version_info[1]
57-
if env.get("SETUPTOOLS_EXT_SUFFIX", "").endswith("t.pyd"):
58-
return Path(result) / f"python3{minor}t.lib"
59-
return Path(result) / f"python3{minor}.lib"
58+
minor = "" if (abi3 or abi3t) else sys.version_info[1]
59+
suffix = "t" if abi3t else ""
60+
return Path(result) / f"python3{minor}{suffix}.lib"
6061

6162
libdirstr = sysconfig.get_config_var("LIBDIR")
6263
ldlibrarystr = sysconfig.get_config_var("LDLIBRARY")
6364
librarystr = sysconfig.get_config_var("LIBRARY")
64-
if abi3:
65+
if abi3 or abi3t:
66+
if abi3t and sysconfig.get_config_var("Py_GIL_DISABLED"):
67+
replacement = f"python3{sys.version_info[1]}t"
68+
target = "python3t"
69+
else:
70+
replacement = f"python3{sys.version_info[1]}"
71+
target = "python3"
6572
if ldlibrarystr is not None:
66-
ldlibrarystr = ldlibrarystr.replace(
67-
f"python3{sys.version_info[1]}", "python3"
68-
)
73+
ldlibrarystr = ldlibrarystr.replace(replacement, target)
6974
if librarystr is not None:
70-
librarystr = librarystr.replace(f"python3{sys.version_info[1]}", "python3")
75+
librarystr = librarystr.replace(replacement, target)
7176

7277
libdir: Path | None = libdirstr and Path(libdirstr)
7378
ldlibrary: Path | None = ldlibrarystr and Path(ldlibrarystr)
@@ -158,7 +163,11 @@ def get_cmake_platform(env: Mapping[str, str] | None) -> str:
158163
return PLAT_TO_CMAKE.get(plat, plat)
159164

160165

161-
def get_soabi(env: Mapping[str, str], *, abi3: bool = False) -> str:
166+
def get_soabi(
167+
env: Mapping[str, str], *, abi3: bool = False, abi3t: bool = False
168+
) -> str:
169+
if abi3t:
170+
return "" if sysconfig.get_platform().startswith("win") else "abi3t"
162171
if abi3:
163172
return "" if sysconfig.get_platform().startswith("win") else "abi3"
164173

@@ -226,6 +235,11 @@ def info_print(
226235
get_python_library(os.environ, abi3=True),
227236
color=color,
228237
)
238+
rich_print(
239+
"{bold}Detected ABI3T Python Library:",
240+
get_python_library(os.environ, abi3t=True),
241+
color=color,
242+
)
229243
rich_print(
230244
"{bold}Detected Python Include Directory:",
231245
get_python_include_dir(),
@@ -251,6 +265,11 @@ def info_print(
251265
get_soabi(os.environ, abi3=True),
252266
color=color,
253267
)
268+
rich_print(
269+
"{color}Detected ABI3T SOABI:",
270+
get_soabi(os.environ, abi3t=True),
271+
color=color,
272+
)
254273
rich_print(
255274
"{bold}Detected ABI flags:",
256275
get_abi_flags(),

src/scikit_build_core/builder/wheel_tag.py

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,30 +99,69 @@ def compute_best(
9999

100100
if py_api:
101101
pyvers_new = py_api.split(".")
102-
if all(x.startswith("cp3") and x[3:].isdecimal() for x in pyvers_new):
103-
if len(pyvers_new) != 1:
104-
msg = "Unexpected py-api, must be a single cp version (e.g. cp39), not {py_api}"
105-
raise AssertionError(msg)
102+
if all(
103+
(
104+
x.startswith("cp3")
105+
and x[3:].isdecimal()
106+
and not sysconfig.get_config_var("Py_GIL_DISABLED")
107+
)
108+
or (
109+
x.startswith("cp3")
110+
and len(x) > 4
111+
and x[3:-1].isdecimal()
112+
and x.endswith("t")
113+
and sysconfig.get_config_var("Py_GIL_DISABLED")
114+
)
115+
for x in pyvers_new
116+
):
106117
if root_is_purelib:
107118
msg = f"Unexpected py-api, since platlib is set to false, must be Pythonless (e.g. py2.py3), not {py_api}"
108119
raise AssertionError(msg)
109120

110-
minor = int(pyvers_new[0][3:])
111-
if (
112-
sys.implementation.name == "cpython"
113-
and minor <= sys.version_info.minor
114-
and not sysconfig.get_config_var("Py_GIL_DISABLED")
121+
# Separate classic and free-threaded tags
122+
classic_tags = [
123+
x for x in pyvers_new if x.startswith("cp3") and x[3:].isdecimal()
124+
]
125+
ft_tags = [
126+
x
127+
for x in pyvers_new
128+
if x.startswith("cp3")
129+
and len(x) > 4
130+
and x[3:-1].isdecimal()
131+
and x.endswith("t")
132+
]
133+
134+
if sys.implementation.name == "cpython" and sysconfig.get_config_var(
135+
"Py_GIL_DISABLED"
115136
):
116-
pyvers = pyvers_new
117-
abi = "abi3"
118-
else:
119-
msg = "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high or free-threaded"
120-
logger.debug(msg, sys.implementation.name, minor)
137+
# Free-threaded: only accept cp3XXt tags
138+
if ft_tags:
139+
target = ft_tags[0]
140+
minor = int(target[3:-1])
141+
if minor <= sys.version_info.minor:
142+
pyvers = [target]
143+
abi = "abi3t"
144+
else:
145+
msg = "Ignoring py-api, version (3.{}) is too high"
146+
logger.debug(msg, minor)
147+
# Classic CPython
148+
elif classic_tags:
149+
target = classic_tags[0]
150+
minor = int(target[3:])
151+
if (
152+
sys.implementation.name == "cpython"
153+
and minor <= sys.version_info.minor
154+
):
155+
pyvers = [target]
156+
abi = "abi3"
157+
else:
158+
msg = "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high"
159+
logger.debug(msg, sys.implementation.name, minor)
121160
elif all(x.startswith("py") and x[2:].isdecimal() for x in pyvers_new):
122161
pyvers = pyvers_new
123162
abi = "none"
124163
else:
125-
msg = f"Unexpected py-api, must be abi3 (e.g. cp39) or Pythonless (e.g. py2.py3), not {py_api}"
164+
msg = f"Unexpected py-api, must be abi3 (e.g. cp39), abi3t (e.g. cp315t), or Pythonless (e.g. py2.py3), not {py_api}"
126165
raise AssertionError(msg)
127166

128167
return cls(pyvers=pyvers, abis=[abi], archs=plats, build_tag=build_tag)
@@ -174,7 +213,7 @@ def as_tags_set(self) -> frozenset[packaging.tags.Tag]:
174213
parser.add_argument(
175214
"--abi",
176215
default="",
177-
help="Specify py-api, like 'cp38' or 'py3'",
216+
help="Specify py-api, like 'cp38', 'cp315t', or 'py3'",
178217
)
179218
parser.add_argument(
180219
"--purelib",

src/scikit_build_core/resources/scikit-build.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"py-api": {
219219
"type": "string",
220220
"default": "",
221-
"description": "The Python version tag used in the wheel file."
221+
"description": "The Python version tag used in the wheel file. Use cp38 for classic stable ABI, cp315t for free-threaded stable ABI, or py3 for pure Python."
222222
},
223223
"expand-macos-universal-tags": {
224224
"type": "boolean",

src/scikit_build_core/settings/skbuild_model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,11 @@ class WheelSettings:
289289
290290
You can also set this to "cp38" to enable the CPython 3.8+ Stable
291291
ABI / Limited API (only on CPython and if the version is sufficient,
292-
otherwise this has no effect). Or you can set it to "py3" or "py2.py3" to
293-
ignore Python ABI compatibility. The ABI tag is inferred from this tag.
292+
otherwise this has no effect). For free-threaded Python, you can use
293+
"cp315t" to enable the free-threaded stable ABI (only on CPython
294+
free-threaded builds and if the version is sufficient). Or you can set
295+
it to "py3" or "py2.py3" to ignore Python ABI compatibility. The ABI
296+
tag is inferred from this tag.
294297
295298
This value is used to construct ``SKBUILD_SABI_COMPONENT`` CMake variable.
296299
"""

0 commit comments

Comments
 (0)