From 32055817feadf35898d03d816046a315c8f333c6 Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Tue, 22 Sep 2026 13:56:03 -0700 Subject: [PATCH 1/9] add cp315 and cp315t to CI matrix --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77e98ae8e..da825676b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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: @@ -111,6 +115,8 @@ jobs: - cp312-cp312 - cp313-cp313 - cp313-cp313t + - cp315-cp315 + - cp315-cp315t permissions: id-token: write # This is required for requesting the JWT steps: @@ -137,6 +143,8 @@ jobs: - cp312-cp312 - cp313-cp313 - cp313-cp313t + - cp315-cp315 + - cp315-cp315t permissions: id-token: write # This is required for requesting the JWT steps: From f5207bddd39f04f0810a60c15614de6546bb90e8 Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Tue, 22 Sep 2026 14:10:48 -0700 Subject: [PATCH 2/9] set builder version to latest and remove 315 from musllinux eol versions --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da825676b..a8811b07b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -115,8 +115,6 @@ jobs: - cp312-cp312 - cp313-cp313 - cp313-cp313t - - cp315-cp315 - - cp315-cp315t permissions: id-token: write # This is required for requesting the JWT steps: @@ -143,8 +141,6 @@ jobs: - cp312-cp312 - cp313-cp313 - cp313-cp313t - - cp315-cp315 - - cp315-cp315t permissions: id-token: write # This is required for requesting the JWT steps: From f6480736ac6e8c7126418516648ac7a5b7b67353 Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Wed, 23 Sep 2026 10:23:32 -0700 Subject: [PATCH 3/9] initial cp315t support implementation --- setup.py | 46 ++++++++++++++++++-- source/module.c | 113 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 136 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index bcf0b0456..8490f50b2 100644 --- a/setup.py +++ b/setup.py @@ -325,6 +325,21 @@ 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): + # abi3t extensions must be named ".abi3t.so" so that both + # free-threaded and non-free-threaded 3.15+ interpreters will load + # them (see "Tagging and distribution" in the abi3t migration howto). + # setuptools has no native abi3t support yet, so rewrite the default + # per-version suffix (e.g. ".cpython-315t-x86_64-linux-gnu.so"). + # Windows keeps its default ".pyd" suffix per the same guidance, + # which this condition leaves untouched. + 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 _build_dependencies_impl(self, build_dir, install_path, osx_arch=None): cmake = get_cmake_path() @@ -507,7 +522,18 @@ def get_tag(self): python, abi, plat = super().get_tag() # on CPython, our wheels are abi3 and compatible back to 3.11 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: one wheel serves 3.15t, 3.16t, and later. + # + # TODO(abi3t): Option B switch point. Once this wheel has proven + # stable for free-threaded users (no customer regressions for + # 1-2 release cycles), promote the ABI tag to "abi3.abi3t" so + # non-free-threaded 3.15+ interpreters are also served by this + # wheel and cp313-abi3 becomes legacy for 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 @@ -613,9 +639,21 @@ 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. + # One wheel covers 3.15t, 3.16t, and later free-threaded interpreters. + # Py_LIMITED_API is not supported on free-threaded builds (see #760); + # 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', '0x30f0000')) + # 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 diff --git a/source/module.c b/source/module.c index 45f3322ea..bae5f4780 100644 --- a/source/module.c +++ b/source/module.c @@ -1175,27 +1175,29 @@ 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 + * (single-phase modules are cached for the life of the process). The abi3t + * multi-phase path can re-run exec if the module is removed from sys.modules + * and re-imported, so 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(); @@ -1224,9 +1226,82 @@ 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. 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_STATIC_DATA(Py_mod_name, s_module_name), + PySlot_STATIC_DATA(Py_mod_doc, 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 From 3eb38498244dcaf8a6fbfdc20e132bfa286d7477 Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Wed, 23 Sep 2026 11:44:47 -0700 Subject: [PATCH 4/9] cast away const for void * --- source/module.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/module.c b/source/module.c index bae5f4780..7789d8489 100644 --- a/source/module.c +++ b/source/module.c @@ -1253,8 +1253,10 @@ PyABIInfo_VAR(s_abi_info); static PySlot s_module_slots[] = { PySlot_STATIC_DATA(Py_mod_abi, &s_abi_info), - PySlot_STATIC_DATA(Py_mod_name, s_module_name), - PySlot_STATIC_DATA(Py_mod_doc, s_module_doc), + /* 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), @@ -1288,9 +1290,9 @@ PyMODINIT_FUNC PyInit__awscrt(void) { return NULL; } -#ifdef Py_GIL_DISABLED +# ifdef Py_GIL_DISABLED PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif +# endif if (s_module_exec(m) != 0) { Py_DECREF(m); From bf7fc4aaede49f893d5feb8a93ebf80f668b6adf Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Wed, 23 Sep 2026 13:47:24 -0700 Subject: [PATCH 5/9] update setup.py --- setup.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/setup.py b/setup.py index 8490f50b2..840f7f66c 100644 --- a/setup.py +++ b/setup.py @@ -328,13 +328,20 @@ 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): - # abi3t extensions must be named ".abi3t.so" so that both - # free-threaded and non-free-threaded 3.15+ interpreters will load - # them (see "Tagging and distribution" in the abi3t migration howto). - # setuptools has no native abi3t support yet, so rewrite the default - # per-version suffix (e.g. ".cpython-315t-x86_64-linux-gnu.so"). - # Windows keeps its default ".pyd" suffix per the same guidance, - # which this condition leaves untouched. + # 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' @@ -520,17 +527,16 @@ 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: if python.startswith("cp") and sys.version_info >= (3, 15): # Built against abi3t (see awscrt_ext), the stable ABI for - # free-threaded builds: one wheel serves 3.15t, 3.16t, and later. - # - # TODO(abi3t): Option B switch point. Once this wheel has proven - # stable for free-threaded users (no customer regressions for - # 1-2 release cycles), promote the ABI tag to "abi3.abi3t" so - # non-free-threaded 3.15+ interpreters are also served by this - # wheel and cp313-abi3 becomes legacy for 3.13/3.14 only. + # 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) @@ -641,12 +647,11 @@ def awscrt_ext(): # prefer building with stable ABI, so a wheel can work with multiple major versions if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15): # 3.15 introduces abi3t: the stable ABI for free-threaded builds. - # One wheel covers 3.15t, 3.16t, and later free-threaded interpreters. - # Py_LIMITED_API is not supported on free-threaded builds (see #760); + # 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', '0x30f0000')) + 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. From 95b26bbb02860a5bb3522276554896c48d04c84e Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Wed, 23 Sep 2026 14:13:39 -0700 Subject: [PATCH 6/9] doc changes --- source/module.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/source/module.c b/source/module.c index 7789d8489..06d638218 100644 --- a/source/module.c +++ b/source/module.c @@ -1180,11 +1180,10 @@ AWS_STATIC_STRING_FROM_LITERAL(s_crash_handler_env_var, "AWS_CRT_CRASH_HANDLER") * * 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 - * (single-phase modules are cached for the life of the process). The abi3t - * multi-phase path can re-run exec if the module is removed from sys.modules - * and re-imported, so this function guards itself with a static flag. - * Python's import machinery serializes extension-module exec, so the plain + * 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, @@ -1233,9 +1232,10 @@ static int s_module_exec(PyObject *module) { /* * Module export hook (PEP 793) for abi3t builds: the stable ABI for - * free-threaded Python, introduced in CPython 3.15. abi3t removes - * PyModuleDef-based single-phase init, so the module is described by a static - * PySlot array instead. Notes on each slot: + * 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). From 82896d99d5acce9e85c48766250d7c25eb7ed964 Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Wed, 23 Sep 2026 14:41:39 -0700 Subject: [PATCH 7/9] add windows/macos 315 jobs --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8811b07b..4e958757b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -312,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: @@ -328,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: From 499d1a5564ff2d680f0ef7917266cb9999471a4c Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Thu, 24 Sep 2026 07:42:38 -0700 Subject: [PATCH 8/9] fix for windows --- setup.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/setup.py b/setup.py index 840f7f66c..43fdc2da2 100644 --- a/setup.py +++ b/setup.py @@ -347,6 +347,22 @@ def get_ext_filename(self, ext_name): filename = filename[:-len(ext_suffix)] + '.abi3t.so' return filename + def get_export_symbols(self, ext): + symbols = super().get_export_symbols(ext) + if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15): + # On Windows, setuptools tells the linker to export "PyInit_" + # (/EXPORT:PyInit__awscrt). abi3t modules define "PyModExport_" + # instead (PEP 793, see source/module.c) -- PyInit doesn't exist in + # that build, so the link fails with an unresolved external unless + # we export the right symbol. No-op on non-Windows (symbols is + # empty there; ELF/Mach-O don't use export lists). + # + # Like get_ext_filename above, remove this override once setuptools + # supports abi3t natively (pypa/setuptools#5205); the replace() is a + # harmless no-op if setuptools starts emitting PyModExport itself. + symbols = [s.replace('PyInit_', 'PyModExport_') for s in symbols] + return symbols + def _build_dependencies_impl(self, build_dir, install_path, osx_arch=None): cmake = get_cmake_path() From d1512033a5ee0753b3278f622ec2af782e6cd463 Mon Sep 17 00:00:00 2001 From: Steve Kim Date: Thu, 24 Sep 2026 11:31:24 -0700 Subject: [PATCH 9/9] simplified windows support --- setup.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 43fdc2da2..c2d9548ec 100644 --- a/setup.py +++ b/setup.py @@ -348,20 +348,20 @@ def get_ext_filename(self, ext_name): return filename def get_export_symbols(self, ext): - symbols = super().get_export_symbols(ext) if FREE_THREADED_BUILD and sys.version_info[:2] >= (3, 15): # On Windows, setuptools tells the linker to export "PyInit_" - # (/EXPORT:PyInit__awscrt). abi3t modules define "PyModExport_" + # (/EXPORT:PyInit__awscrt), but abi3t modules define "PyModExport_" # instead (PEP 793, see source/module.c) -- PyInit doesn't exist in - # that build, so the link fails with an unresolved external unless - # we export the right symbol. No-op on non-Windows (symbols is - # empty there; ELF/Mach-O don't use export lists). + # 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); the replace() is a - # harmless no-op if setuptools starts emitting PyModExport itself. - symbols = [s.replace('PyInit_', 'PyModExport_') for s in symbols] - return symbols + # 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()