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
62 changes: 61 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
- 'docs'

env:
BUILDER_VERSION: v0.9.96
BUILDER_VERSION: latest
BUILDER_SOURCE: releases
BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net
PACKAGE_NAME: aws-crt-python
Expand Down Expand Up @@ -57,6 +57,8 @@ jobs:
- cp313-cp313
- cp314-cp314
- cp314-cp314t
- cp315-cp315
- cp315-cp315t
permissions:
id-token: write # This is required for requesting the JWT
steps:
Expand All @@ -83,6 +85,8 @@ jobs:
- cp313-cp313
- cp314-cp314
- cp314-cp314t
- cp315-cp315
- cp315-cp315t
permissions:
id-token: write # This is required for requesting the JWT
steps:
Expand Down Expand Up @@ -308,6 +312,34 @@ jobs:
python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')"
python builder.pyz build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python38.outputs.python-path }}"

windows-315:
# Python 3.15 (standard and free-threaded). Free-threaded exercises the
# abi3t build path (Py_TARGET_ABI3T / PyModExport, see setup.py).
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
freethreaded: [false, true]
permissions:
id-token: write # This is required for requesting the JWT
steps:
- uses: actions/setup-python@v5
id: python315
with:
python-version: '3.15'
allow-prereleases: true
freethreaded: ${{ matrix.freethreaded }}
- uses: ilammy/setup-nasm@v1
- name: configure AWS credentials (containers)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.CRT_CI_ROLE }}
aws-region: ${{ env.AWS_DEFAULT_REGION }}
- name: Build ${{ env.PACKAGE_NAME }} + consumers
run: |
python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')"
python builder.pyz build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python315.outputs.python-path }}"

macos:
runs-on: macos-14 # latest
permissions:
Expand All @@ -324,6 +356,34 @@ jobs:
chmod a+x builder
./builder build -p ${{ env.PACKAGE_NAME }}

macos-315:
# Python 3.15 (standard and free-threaded). Free-threaded exercises the
# abi3t build path (Py_TARGET_ABI3T / PyModExport, see setup.py).
runs-on: macos-14 # latest
strategy:
fail-fast: false
matrix:
freethreaded: [false, true]
permissions:
id-token: write # This is required for requesting the JWT
steps:
- uses: actions/setup-python@v5
id: python315
with:
python-version: '3.15'
allow-prereleases: true
freethreaded: ${{ matrix.freethreaded }}
- name: configure AWS credentials (containers)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.CRT_CI_ROLE }}
aws-region: ${{ env.AWS_DEFAULT_REGION }}
- name: Build ${{ env.PACKAGE_NAME }} + consumers
run: |
python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')"
chmod a+x builder
./builder build -p ${{ env.PACKAGE_NAME }} --python "${{ steps.python315.outputs.python-path }}"

macos-x64:
runs-on: macos-14-large # latest
permissions:
Expand Down
69 changes: 64 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,44 @@ def __init__(self, name, extra_cmake_args=[], libname=None):


class awscrt_build_ext(setuptools.command.build_ext.build_ext):
def get_ext_filename(self, ext_name):
filename = super().get_ext_filename(ext_name)
if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15):
# setuptools' build_ext (which this class extends) names extensions with the
# interpreter's EXT_SUFFIX, e.g. "_awscrt.cpython-315t-x86_64-linux-gnu.so"
# -- a name only a 3.15t interpreter will import. This abi3t (free-threaded
# stable ABI) build produces one binary for ALL 3.15+ free-threaded
# interpreters, so swap in ".abi3t.so", the version-agnostic suffix the
# import system accepts. setuptools does this automatically for classic abi3
# ("_awscrt.abi3.so") but doesn't know abi3t yet. Dropping the arch/OS part
# is safe: platform selection happens via the wheel's platform tag
# (e.g. manylinux2014_x86_64), never via the .so filename. Windows needs no
# rename (abi3t modules keep the plain ".pyd" name); the ".so" check skips it.
#
# We can remove this override once setuptools supports abi3t natively (tracked
# in pypa/setuptools#5205; the check makes it a harmless no-op if setuptools
# starts emitting ".abi3t.so" itself).
ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
if ext_suffix.endswith('.so') and filename.endswith(ext_suffix):
filename = filename[:-len(ext_suffix)] + '.abi3t.so'
return filename

def get_export_symbols(self, ext):
if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15):
# On Windows, setuptools tells the linker to export "PyInit_<name>"
# (/EXPORT:PyInit__awscrt), but abi3t modules define "PyModExport_<name>"
# instead (PEP 793, see source/module.c) -- PyInit doesn't exist in
# that build, so the link fails with an unresolved external
# (pypa/distutils#387). The /EXPORT flag is redundant anyway:
# PyMODEXPORT_FUNC already declares dllexport, which puts the hook
# in the DLL's export table. So suppress the export list entirely.
# No-op on non-Windows (the list is already empty there).
#
# Like get_ext_filename above, remove this override once
# setuptools supports abi3t natively (pypa/setuptools#5205).
return []
return super().get_export_symbols(ext)

def _build_dependencies_impl(self, build_dir, install_path, osx_arch=None):
cmake = get_cmake_path()

Expand Down Expand Up @@ -505,9 +543,19 @@ def run(self):
class bdist_wheel_abi3(bdist_wheel):
def get_tag(self):
python, abi, plat = super().get_tag()
# on CPython, our wheels are abi3 and compatible back to 3.11
# Rewrite the wheel's compatibility tags to match what awscrt_ext()
# actually compiled against -- pip trusts these tags to pick a wheel,
# so each branch below MUST mirror a compile-flag branch there.
if FREE_THREADED_BUILD:
# free-threaded builds don't use limited API, so skip abi3 tag
if python.startswith("cp") and sys.version_info >= (3, 15):
# Built against abi3t (see awscrt_ext), the stable ABI for
# free-threaded builds. The bare "abi3t" tag is only accepted
# by free-threaded interpreters. If we ever want ONE wheel for
# all 3.15+ interpreters (free-threaded and not), we can promote
# this tag to "abi3.abi3t" -- cp313-abi3 would then serve 3.13/3.14 only.
return "cp315", "abi3t", plat
# 3.13/3.14 free-threaded builds don't support limited API or
# abi3t, so no stable-ABI tag (version-specific wheel)
return python, abi, plat
elif python.startswith("cp") and sys.version_info >= (3, 13):
# 3.13 deprecates PyWeakref_GetObject(), adds alternative
Expand Down Expand Up @@ -613,9 +661,20 @@ def awscrt_ext():
extra_link_args += ['-Wl,--fatal-warnings']

# prefer building with stable ABI, so a wheel can work with multiple major versions
if FREE_THREADED_BUILD and sys.version_info[:2] <= (3, 14):
# 3.14 free threaded (aka no gil) does not support limited api.
# disable it for now. 3.15 promises to support limited api + free threading combo
if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15):
# 3.15 introduces abi3t: the stable ABI for free-threaded builds.
# Py_LIMITED_API is not supported on free-threaded builds as of 3.15;
# Py_TARGET_ABI3T is the free-threaded equivalent. It requires the
# PyModExport_* module export hook (see source/module.c).
# https://docs.python.org/3.15/howto/abi3t-migration.html
define_macros.append(('Py_TARGET_ABI3T', '0x030F0000'))
# setuptools' py_limited_api machinery is abi3-only; the abi3t wheel
# tag and extension filename are handled manually in bdist_wheel_abi3
# and awscrt_build_ext.get_ext_filename.
py_limited_api = False
elif FREE_THREADED_BUILD:
# 3.13/3.14 free threaded (aka no gil) support neither limited api
# nor abi3t. Build version-specific wheels (cp313t/cp314t).
py_limited_api = False
elif sys.version_info >= (3, 13):
# 3.13 deprecates PyWeakref_GetObject(), adds alternative
Expand Down
115 changes: 96 additions & 19 deletions source/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -1175,27 +1175,28 @@ AWS_STATIC_STRING_FROM_LITERAL(s_crash_handler_env_var, "AWS_CRT_CRASH_HANDLER")
* Module Init
******************************************************************************/

PyMODINIT_FUNC PyInit__awscrt(void) {
static struct PyModuleDef s_module_def = {
PyModuleDef_HEAD_INIT,
s_module_name,
s_module_doc,
-1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */
s_module_methods,
NULL, /* slots for multi-phase initialization */
NULL, /* traversal fn to call during GC traversal */
NULL, /* clear fn to call during GC clear */
NULL, /* fn to call during deallocation of the module object */
};
/**
* One-time process-wide initialization, shared by both module init paths below.
*
* WARNING: everything this touches is process-global state (the allocator,
* crash handler, aws-c-* library init, error tables). It must run at most once
* per process. The single-phase PyInit path below guarantees this naturally.
* The abi3t multi-phase path can re-run exec if the module is removed from
* sys.modules and re-imported. This function guards itself with a static
* flag. Python's import machinery serializes extension-module exec, so the plain
* static is not a data race.
*
* Returns 0 on success, -1 (with a Python exception set) on failure,
* matching the Py_mod_exec slot contract.
*/
static int s_module_exec(PyObject *module) {
(void)module;

PyObject *m = PyModule_Create(&s_module_def);
if (!m) {
return NULL;
static bool s_module_initialized = false;
if (s_module_initialized) {
return 0;
}

#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
s_module_initialized = true;

s_init_allocator();

Expand Down Expand Up @@ -1224,9 +1225,85 @@ PyMODINIT_FUNC PyInit__awscrt(void) {
aws_register_error_info(&s_error_list);
s_error_map_init();

return 0;
}

#ifdef Py_TARGET_ABI3T

/*
* Module export hook (PEP 793) for abi3t builds: the stable ABI for
* free-threaded Python, introduced in CPython 3.15. Py_TARGET_ABI3T is
* defined by setup.py (awscrt_ext) for free-threaded 3.15+ builds. abi3t
* removes PyModuleDef-based single-phase init, so the module is described by
* a static PySlot array instead. Notes on each slot:
*
* - Py_mod_gil = Py_MOD_GIL_NOT_USED replaces the PyUnstable_Module_SetGIL()
* call used on the non-abi3t path (PyUnstable_* is not in any stable ABI).
* - Py_mod_multiple_interpreters = NOT_SUPPORTED is REQUIRED for correctness:
* _awscrt keeps process-global state (see s_module_exec), so subinterpreter
* imports must be refused by the interpreter rather than corrupting that
* state with a second exec.
* - No Py_mod_state_size slot: the module keeps state in globals (the
* equivalent of m_size = -1 in the legacy PyModuleDef).
*
* https://docs.python.org/3.15/howto/abi3t-migration.html
*/

PyABIInfo_VAR(s_abi_info);

static PySlot s_module_slots[] = {
PySlot_STATIC_DATA(Py_mod_abi, &s_abi_info),
/* PySlot.sl_ptr is a plain `void *`. These two are read-only strings, so
* casting away const is safe: CPython never writes through the slot. */
PySlot_STATIC_DATA(Py_mod_name, (void *)s_module_name),
PySlot_STATIC_DATA(Py_mod_doc, (void *)s_module_doc),
PySlot_STATIC_DATA(Py_mod_methods, s_module_methods),
PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
PySlot_DATA(Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED),
PySlot_FUNC(Py_mod_exec, s_module_exec),
PySlot_END,
};

PyMODEXPORT_FUNC PyModExport__awscrt(void) {
/* Must ONLY return a pointer to static data; all runtime initialization
* happens later in the Py_mod_exec slot (s_module_exec). */
return s_module_slots;
}

#else /* !Py_TARGET_ABI3T */

PyMODINIT_FUNC PyInit__awscrt(void) {
static struct PyModuleDef s_module_def = {
PyModuleDef_HEAD_INIT,
s_module_name,
s_module_doc,
-1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */
s_module_methods,
NULL, /* slots for multi-phase initialization */
NULL, /* traversal fn to call during GC traversal */
NULL, /* clear fn to call during GC clear */
NULL, /* fn to call during deallocation of the module object */
};

PyObject *m = PyModule_Create(&s_module_def);
if (!m) {
return NULL;
}

# ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
# endif

if (s_module_exec(m) != 0) {
Py_DECREF(m);
return NULL;
}

return m;
}

#endif /* Py_TARGET_ABI3T */

/**
* align with the the vanilla C types Python tends to use.
* This is important when passing arguments between C and Python
Expand Down
Loading