Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,10 @@ wheel.py-api = "cp38"

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

If you are not using CPython at all, you can specify any version of Python is
fine:
Expand Down
3 changes: 2 additions & 1 deletion docs/guide/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ The three new items here (compared to SDists) are the [compatibility tags][]:
`py3` for pure Python wheels, or `py312` (etc) for compiled wheels.
- `abi tag`: The interpreter ABI this was built for. `none` for pure Python
wheels or compiled wheels that don't use the Python API, `abi3` for stable ABI
/ limited API wheels, and `cp312` (etc) for normal compiled wheels.
/ limited API wheels, `abi3t` for free-threaded stable ABI wheels, and `cp312`
(etc) for normal compiled wheels.
- `platform tag`: This is the platform the wheel is valid on, such as `any`,
`linux_x86_64`, or `manylinux_2_17_x86_64`.

Expand Down
7 changes: 5 additions & 2 deletions docs/reference/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,11 @@ print(mk_skbuild_docs())

You can also set this to "cp38" to enable the CPython 3.8+ Stable
ABI / Limited API (only on CPython and if the version is sufficient,
otherwise this has no effect). Or you can set it to "py3" or "py2.py3" to
ignore Python ABI compatibility. The ABI tag is inferred from this tag.
otherwise this has no effect). For free-threaded Python, you can use
"cp315t" to enable the free-threaded stable ABI (only on CPython
free-threaded builds and if the version is sufficient). Or you can set
it to "py3" or "py2.py3" to ignore Python ABI compatibility. The ABI
tag is inferred from this tag.

This value is used to construct ``SKBUILD_SABI_COMPONENT`` CMake variable.
```
Expand Down
88 changes: 62 additions & 26 deletions src/scikit_build_core/builder/builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import dataclasses
import enum
import os
import re
import shlex
Expand Down Expand Up @@ -35,6 +36,12 @@
DIR = Path(__file__).parent.resolve()


class _SabiMode(enum.Enum):
NONE = enum.auto()
ABI3 = enum.auto()
ABI3T = enum.auto()


def __dir__() -> list[str]:
return __all__

Expand Down Expand Up @@ -209,27 +216,48 @@ def configure(
)
cache_config["SKBUILD_PROJECT_VERSION_FULL"] = str(version)

if limited_api is None:
if self.settings.wheel.py_api.startswith("cp3"):
target_minor_version = int(self.settings.wheel.py_api[3:])
limited_api = target_minor_version <= sys.version_info.minor
py_api = self.settings.wheel.py_api
gil_disabled = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))

sabi = _SabiMode.NONE
if limited_api is True:
# Handle externally-set limited_api (e.g. from setuptools)
if sys.implementation.name != "cpython":
logger.info("PyPy doesn't support the Limited API, ignoring")
elif gil_disabled:
sabi = _SabiMode.ABI3T
else:
limited_api = False

if limited_api and sys.implementation.name != "cpython":
limited_api = False
logger.info("PyPy doesn't support the Limited API, ignoring")

if limited_api and sysconfig.get_config_var("Py_GIL_DISABLED"):
limited_api = False
logger.info(
"Free-threaded Python doesn't support the Limited API currently, ignoring"
)
sabi = _SabiMode.ABI3
elif limited_api is None and py_api.startswith("cp3"):
target_minor_version = int(py_api[3:].rstrip("t"))
if sys.implementation.name != "cpython":
logger.info("py-api {} requires CPython, ignoring")
elif py_api.endswith("t"):
# Free-threaded stable ABI (PEP 803 / abi3t)
if gil_disabled and target_minor_version <= sys.version_info.minor:
sabi = _SabiMode.ABI3T
else:
logger.info(
"py-api {} requires free-threaded CPython >= 3.{}, ignoring",
py_api,
target_minor_version,
)
else:
# Classic stable ABI (abi3)
target_minor_version = int(py_api[3:])
if gil_disabled:
logger.info(
"Free-threaded Python doesn't support the classic Limited API, ignoring"
)
elif target_minor_version <= sys.version_info.minor:
sabi = _SabiMode.ABI3

python_library = get_python_library(self.config.env, abi3=False)
python_sabi_library = (
get_python_library(self.config.env, abi3=True) if limited_api else None
)
python_sabi_library = None
if sabi == _SabiMode.ABI3T:
python_sabi_library = get_python_library(self.config.env, abi3t=True)
elif sabi == _SabiMode.ABI3:
python_sabi_library = get_python_library(self.config.env, abi3=True)
python_include_dir = get_python_include_dir()
numpy_include_dir = get_numpy_include_dir()

Expand Down Expand Up @@ -265,20 +293,28 @@ def configure(
if numpy_include_dir:
cache_config[f"{prefix}_NumPy_INCLUDE_DIR"] = numpy_include_dir

cache_config["SKBUILD_SOABI"] = get_soabi(self.config.env, abi3=limited_api)
cache_config["SKBUILD_SOABI"] = get_soabi(
self.config.env,
abi3=(sabi == _SabiMode.ABI3),
abi3t=(sabi == _SabiMode.ABI3T),
)

# Allow CMakeLists to detect this is supposed to be a limited ABI build
cache_config["SKBUILD_SABI_COMPONENT"] = (
"Development.SABIModule" if limited_api else ""
"Development.SABIModule" if sabi != _SabiMode.NONE else ""
)

# Allow users to detect the version requested in settings
py_api = self.settings.wheel.py_api
cache_config["SKBUILD_SABI_VERSION"] = (
f"{py_api[2]}.{py_api[3:]}"
if limited_api and py_api.startswith("cp")
else ""
)
if sabi != _SabiMode.NONE and py_api.startswith("cp"):
version_str = py_api[2:]
if version_str.endswith("t"):
version_str = version_str[:-1]
Comment thread
henryiii marked this conversation as resolved.
cache_config["SKBUILD_SABI_VERSION"] = f"{version_str[0]}.{version_str[1:]}"
else:
cache_config["SKBUILD_SABI_VERSION"] = ""

if sabi == _SabiMode.ABI3T:
cache_config["Py_TARGET_ABI3T"] = "1"

if cache_entries:
cache_config.update(cache_entries)
Expand Down
41 changes: 30 additions & 11 deletions src/scikit_build_core/builder/sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ def __dir__() -> list[str]:
return __all__


def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path | None:
def get_python_library(
env: Mapping[str, str], *, abi3: bool = False, abi3t: bool = False
) -> Path | None:
Comment thread
henryiii marked this conversation as resolved.
# When cross-compiling, check DIST_EXTRA_CONFIG first
config_file = env.get("DIST_EXTRA_CONFIG", None)
if config_file and Path(config_file).is_file():
Expand All @@ -53,21 +55,24 @@ def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path |
result = cp.get("build_ext", "library_dirs", fallback="")
if result:
logger.info("Reading DIST_EXTRA_CONFIG:build_ext.library_dirs={}", result)
minor = "" if abi3 else sys.version_info[1]
if env.get("SETUPTOOLS_EXT_SUFFIX", "").endswith("t.pyd"):
return Path(result) / f"python3{minor}t.lib"
return Path(result) / f"python3{minor}.lib"
minor = "" if (abi3 or abi3t) else sys.version_info[1]
suffix = "t" if abi3t else ""
return Path(result) / f"python3{minor}{suffix}.lib"

libdirstr = sysconfig.get_config_var("LIBDIR")
ldlibrarystr = sysconfig.get_config_var("LDLIBRARY")
librarystr = sysconfig.get_config_var("LIBRARY")
if abi3:
if abi3 or abi3t:
if abi3t and sysconfig.get_config_var("Py_GIL_DISABLED"):
replacement = f"python3{sys.version_info[1]}t"
target = "python3t"
else:
replacement = f"python3{sys.version_info[1]}"
target = "python3"
if ldlibrarystr is not None:
ldlibrarystr = ldlibrarystr.replace(
f"python3{sys.version_info[1]}", "python3"
)
ldlibrarystr = ldlibrarystr.replace(replacement, target)
if librarystr is not None:
librarystr = librarystr.replace(f"python3{sys.version_info[1]}", "python3")
librarystr = librarystr.replace(replacement, target)

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


def get_soabi(env: Mapping[str, str], *, abi3: bool = False) -> str:
def get_soabi(
env: Mapping[str, str], *, abi3: bool = False, abi3t: bool = False
) -> str:
if abi3t:
return "" if sysconfig.get_platform().startswith("win") else "abi3t"
if abi3:
return "" if sysconfig.get_platform().startswith("win") else "abi3"

Expand Down Expand Up @@ -226,6 +235,11 @@ def info_print(
get_python_library(os.environ, abi3=True),
color=color,
)
rich_print(
"{bold}Detected ABI3T Python Library:",
get_python_library(os.environ, abi3t=True),
color=color,
)
rich_print(
"{bold}Detected Python Include Directory:",
get_python_include_dir(),
Expand All @@ -251,6 +265,11 @@ def info_print(
get_soabi(os.environ, abi3=True),
color=color,
)
rich_print(
"{color}Detected ABI3T SOABI:",
get_soabi(os.environ, abi3t=True),
color=color,
)
rich_print(
"{bold}Detected ABI flags:",
get_abi_flags(),
Expand Down
93 changes: 74 additions & 19 deletions src/scikit_build_core/builder/wheel_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,35 @@ def __dir__() -> list[str]:
return __all__


class _PyTag:
"""Helper for interrogating a single Python ABI tag like 'cp39' or 'cp315t'."""

def __init__(self, tag: str) -> None:
self._tag = tag

@property
def is_classic_abi3(self) -> bool:
return self._tag.startswith("cp3") and self._tag[3:].isdecimal()

@property
def is_ft_abi3(self) -> bool:
return (
self._tag.startswith("cp3")
and self._tag.endswith("t")
and len(self._tag) > 4
and self._tag[3:-1].isdecimal()
)

@property
def minor(self) -> int:
if self.is_ft_abi3:
return int(self._tag[3:-1])
return int(self._tag[3:])

def __str__(self) -> str:
return self._tag


@dataclasses.dataclass(frozen=True)
class WheelTag:
pyvers: list[str]
Expand Down Expand Up @@ -99,30 +128,56 @@ def compute_best(

if py_api:
pyvers_new = py_api.split(".")
if all(x.startswith("cp3") and x[3:].isdecimal() for x in pyvers_new):
if len(pyvers_new) != 1:
msg = "Unexpected py-api, must be a single cp version (e.g. cp39), not {py_api}"
raise AssertionError(msg)
pytags = [_PyTag(x) for x in pyvers_new]
gil_disabled = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
if all(t.is_classic_abi3 or t.is_ft_abi3 for t in pytags):
if root_is_purelib:
msg = f"Unexpected py-api, since platlib is set to false, must be Pythonless (e.g. py2.py3), not {py_api}"
raise AssertionError(msg)

minor = int(pyvers_new[0][3:])
if (
sys.implementation.name == "cpython"
and minor <= sys.version_info.minor
and not sysconfig.get_config_var("Py_GIL_DISABLED")
):
pyvers = pyvers_new
abi = "abi3"
else:
msg = "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high or free-threaded"
logger.debug(msg, sys.implementation.name, minor)
classic_tags = [t for t in pytags if t.is_classic_abi3]
ft_tags = [t for t in pytags if t.is_ft_abi3]

if sys.implementation.name == "cpython" and gil_disabled:
# Free-threaded: only accept cp3XXt tags
if ft_tags:
target = ft_tags[0]
if target.minor <= sys.version_info.minor:
pyvers = [str(target)]
abi = "abi3t"
else:
logger.debug(
"Ignoring py-api, version (3.{}) is too high",
target.minor,
)
elif classic_tags:
logger.debug(
"Ignoring py-api, free-threaded Python doesn't support the classic Stable ABI"
)
# Classic CPython
elif classic_tags:
target = classic_tags[0]
if (
sys.implementation.name == "cpython"
and target.minor <= sys.version_info.minor
):
pyvers = [str(target)]
abi = "abi3"
else:
logger.debug(
"Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high",
sys.implementation.name,
target.minor,
)
elif ft_tags:
logger.debug(
"Ignoring py-api, free-threaded CPython is required for abi3t"
)
elif all(x.startswith("py") and x[2:].isdecimal() for x in pyvers_new):
pyvers = pyvers_new
abi = "none"
else:
msg = f"Unexpected py-api, must be abi3 (e.g. cp39) or Pythonless (e.g. py2.py3), not {py_api}"
msg = f"Unexpected py-api, must be abi3 (e.g. cp39), abi3t (e.g. cp315t), or Pythonless (e.g. py2.py3), not {py_api}"
raise AssertionError(msg)

return cls(pyvers=pyvers, abis=[abi], archs=plats, build_tag=build_tag)
Expand Down Expand Up @@ -174,13 +229,13 @@ def as_tags_set(self) -> frozenset[packaging.tags.Tag]:
parser.add_argument(
"--abi",
default="",
help="Specify py-api, like 'cp38' or 'py3'",
help="Specify py-api, like 'cp38', 'cp315t', or 'py3'",
)
parser.add_argument(
"--purelib",
action="store_true",
help="Specify a non-platlib (pure) tag",
)
args = parser.parse_args()
tag = WheelTag.compute_best(args.archs, args.abi, root_is_purelib=args.purelib)
print(tag) # noqa: T201
comp_tag = WheelTag.compute_best(args.archs, args.abi, root_is_purelib=args.purelib)
print(comp_tag) # noqa: T201
7 changes: 5 additions & 2 deletions src/scikit_build_core/settings/skbuild_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,11 @@ class WheelSettings:
You can also set this to "cp38" to enable the CPython 3.8+ Stable
ABI / Limited API (only on CPython and if the version is sufficient,
otherwise this has no effect). Or you can set it to "py3" or "py2.py3" to
ignore Python ABI compatibility. The ABI tag is inferred from this tag.
otherwise this has no effect). For free-threaded Python, you can use
"cp315t" to enable the free-threaded stable ABI (only on CPython
free-threaded builds and if the version is sufficient). Or you can set
it to "py3" or "py2.py3" to ignore Python ABI compatibility. The ABI
tag is inferred from this tag.
This value is used to construct ``SKBUILD_SABI_COMPONENT`` CMake variable.
"""
Expand Down
Loading
Loading