diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 8d7dc757..e49c6053 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -66,11 +66,11 @@ jobs: run: | tar zxvf *.tar.gz --strip-components=1 - name: Build wheels - uses: pypa/cibuildwheel@v3.4.1 + uses: pypa/cibuildwheel@54327ab9d35de03b359ac25c97de9417d94639c0 # v4.0.0rc1 env: - CIBW_BUILD: "cp3{9..14}{t,}-${{ matrix.wheel_type }}" + CIBW_BUILD: "cp3{9..15}{t,}-${{ matrix.wheel_type }}" CIBW_ARCHS_LINUX: auto - CIBW_ENABLE: cpython-prerelease cpython-freethreading + CIBW_ENABLE: cpython-prerelease - uses: actions/upload-artifact@v7 with: name: ${{ matrix.wheel_type }}-wheels @@ -157,7 +157,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t", "3.15", "3.15t"] steps: - uses: actions/checkout@v6 - name: Set up Python @@ -192,7 +192,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.9", "3.13", "3.14"] + python_version: ["3.9", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@v6 - name: Set up Python @@ -230,7 +230,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.13", "3.14"] + python_version: ["3.13", "3.14", "3.15"] steps: - uses: actions/checkout@v6 - name: Set up Python @@ -425,6 +425,10 @@ jobs: mv dist/*-wheels/*.whl dist/ rmdir dist/{sdist,*-wheels} ls -R dist + - name: Avoid publishing Python 3.15 wheels + run: | + rm -f dist/*cp315* + ls -R dist - uses: pypa/gh-action-pypi-publish@release/v1 with: skip_existing: true diff --git a/news/289.feature.rst b/news/289.feature.rst new file mode 100644 index 00000000..e65d5673 --- /dev/null +++ b/news/289.feature.rst @@ -0,0 +1 @@ +Python 3.15 is now supported. The wheels are not yet published because the ABI is not yet frozen. diff --git a/src/pystack/_pystack/cpython/code.h b/src/pystack/_pystack/cpython/code.h index d14db7de..d837c66b 100644 --- a/src/pystack/_pystack/cpython/code.h +++ b/src/pystack/_pystack/cpython/code.h @@ -236,4 +236,43 @@ typedef struct } PyCodeObject; } // namespace Python3_14 +namespace Python3_15 { +typedef uint16_t _Py_CODEUNIT; + +typedef struct +{ + PyObject_VAR_HEAD PyObject* co_consts; + PyObject* co_names; + PyObject* co_exceptiontable; + int co_flags; + int co_argcount; + int co_posonlyargcount; + int co_kwonlyargcount; + int co_stacksize; + int co_firstlineno; + int co_nlocalsplus; + int co_framesize; + int co_nlocals; + int co_ncellvars; + int co_nfreevars; + uint32_t co_version; + PyObject* co_localsplusnames; + PyObject* co_localspluskinds; + PyObject* co_filename; + PyObject* co_name; + PyObject* co_qualname; + PyObject* co_linetable; + PyObject* co_weakreflist; + void* co_executors; + void* _co_cached; + uintptr_t _co_instrumentation_version; + void* _co_monitoring; + Py_ssize_t _co_unique_id; + int _co_firsttraceable; + void* co_extra; + /* deal with co_tlbc somehow */ + char co_code_adaptive[1]; +} PyCodeObject; +} // namespace Python3_15 + } // namespace pystack diff --git a/src/pystack/_pystack/cpython/gc.h b/src/pystack/_pystack/cpython/gc.h index b64bd073..5ac0c941 100644 --- a/src/pystack/_pystack/cpython/gc.h +++ b/src/pystack/_pystack/cpython/gc.h @@ -106,4 +106,25 @@ struct _gc_runtime_state } // namespace Python3_14 +namespace Python3_15 { + +struct _gc_runtime_state +{ + int enabled; + int debug; + struct Python3_8::gc_generation young; + struct Python3_8::gc_generation old[2]; + struct Python3_8::gc_generation permanent_generation; + struct gc_generation_stats generation_stats[NUM_GENERATIONS]; + int collecting; + struct _PyInterpreterFrame* frame; + PyObject* garbage; + PyObject* callbacks; + Py_ssize_t heap_size; + Py_ssize_t long_lived_total; + Py_ssize_t long_lived_pending; +}; + +} // namespace Python3_15 + } // namespace pystack diff --git a/src/pystack/_pystack/cpython/interpreter.h b/src/pystack/_pystack/cpython/interpreter.h index b35b26ba..bc2f1742 100644 --- a/src/pystack/_pystack/cpython/interpreter.h +++ b/src/pystack/_pystack/cpython/interpreter.h @@ -416,4 +416,80 @@ typedef struct _is } // namespace Python3_14 +namespace Python3_15 { + +struct _pythreadstate; + +typedef struct +{ + Python3_13::PyMutex mutex; + unsigned long long thread; + size_t level; +} _PyRecursiveMutex; + +struct _import_state +{ + PyObject* modules; + PyObject* modules_by_index; + PyObject* importlib; + int override_frozen_modules; + int override_multi_interp_extensions_check; + PyObject* import_func; + _PyRecursiveMutex lock; + /* diagnostic info in PyImport_ImportModuleLevelObject() */ + struct + { + int import_level; + int64_t accumulated; + int header; + } find_and_load; +}; + +struct _gil_runtime_state +{ + unsigned long interval; + struct _pythreadstate* last_holder; + int locked; + unsigned long switch_number; + pthread_cond_t cond; + pthread_cond_t mutex; +#ifdef FORCE_SWITCHING + pthread_cond_t switch_cond; + pthread_cond_t switch_mutex; +#endif +}; + +typedef struct _is +{ + struct _ceval_state ceval; + struct _is* next; + int64_t id; + Py_ssize_t id_refcount; + int requires_idref; + long _whence; + int _initialized; + int _ready; + int finalizing; + uintptr_t last_restart_version; + struct pythreads + { + uint64_t next_unique_id; + struct _pythreadstate* head; + struct _pythreadstate* preallocated; + struct _pythreadstate* main; + Py_ssize_t count; + size_t stacksize; + } threads; + void* runtime; + struct _pythreadstate* _finalizing; + unsigned long _finalizing_id; + struct _gc_runtime_state gc; + PyObject* sysdict; + PyObject* builtins; + struct _import_state imports; + struct _gil_runtime_state _gil; +} PyInterpreterState; + +} // namespace Python3_15 + } // namespace pystack diff --git a/src/pystack/_pystack/cpython/runtime.h b/src/pystack/_pystack/cpython/runtime.h index 5fa637bd..9ba3153f 100644 --- a/src/pystack/_pystack/cpython/runtime.h +++ b/src/pystack/_pystack/cpython/runtime.h @@ -638,6 +638,246 @@ typedef struct pyruntimestate } // namespace Python3_14 +namespace Python3_15 { + +typedef struct _Py_DebugOffsets +{ + char cookie[8]; + uint64_t version; + uint64_t free_threaded; + // Runtime state offset; + struct _runtime_state + { + uint64_t size; + uint64_t finalizing; + uint64_t interpreters_head; + } runtime_state; + + // Interpreter state offset; + struct _interpreter_state + { + uint64_t size; + uint64_t id; + uint64_t next; + uint64_t threads_head; + uint64_t threads_main; + uint64_t gc; + uint64_t imports_modules; + uint64_t sysdict; + uint64_t builtins; + uint64_t ceval_gil; + uint64_t gil_runtime_state; + uint64_t gil_runtime_state_enabled; + uint64_t gil_runtime_state_locked; + uint64_t gil_runtime_state_holder; + uint64_t code_object_generation; + uint64_t tlbc_generation; + } interpreter_state; + + // Thread state offset; + struct _thread_state + { + uint64_t size; + uint64_t prev; + uint64_t next; + uint64_t interp; + uint64_t current_frame; + uint64_t base_frame; + uint64_t last_profiled_frame; + uint64_t thread_id; + uint64_t native_thread_id; + uint64_t datastack_chunk; + uint64_t status; + uint64_t holds_gil; + uint64_t gil_requested; + uint64_t current_exception; + uint64_t exc_state; + } thread_state; + + // Exception stack item offset + struct + { + uint64_t exc_value; + } err_stackitem; + + // InterpreterFrame offset; + struct _interpreter_frame + { + uint64_t size; + uint64_t previous; + uint64_t executable; + uint64_t instr_ptr; + uint64_t localsplus; + uint64_t owner; + uint64_t stackpointer; + uint64_t tlbc_index; + } interpreter_frame; + + // Code object offset; + struct _code_object + { + uint64_t size; + uint64_t filename; + uint64_t name; + uint64_t qualname; + uint64_t linetable; + uint64_t firstlineno; + uint64_t argcount; + uint64_t localsplusnames; + uint64_t localspluskinds; + uint64_t co_code_adaptive; + uint64_t co_tlbc; + } code_object; + + // PyObject offset; + struct _pyobject + { + uint64_t size; + uint64_t ob_type; + } pyobject; + + // PyTypeObject object offset; + struct _type_object + { + uint64_t size; + uint64_t tp_name; + uint64_t tp_repr; + uint64_t tp_flags; + uint64_t tp_basicsize; + uint64_t tp_dictoffset; + } type_object; + + // PyHeapTypeObject offset; + struct _heap_type_object + { + uint64_t size; + uint64_t ht_cached_keys; + } heap_type_object; + + // PyTuple object offset; + struct _tuple_object + { + uint64_t size; + uint64_t ob_item; + uint64_t ob_size; + } tuple_object; + + // PyList object offset; + struct _list_object + { + uint64_t size; + uint64_t ob_item; + uint64_t ob_size; + } list_object; + + // PySet object offset; + struct _set_object + { + uint64_t size; + uint64_t used; + uint64_t table; + uint64_t mask; + } set_object; + + // PyDict object offset; + struct _dict_object + { + uint64_t size; + uint64_t ma_keys; + uint64_t ma_values; + } dict_object; + + // PyFloat object offset; + struct _float_object + { + uint64_t size; + uint64_t ob_fval; + } float_object; + + // PyLong object offset; + struct _long_object + { + uint64_t size; + uint64_t lv_tag; + uint64_t ob_digit; + } long_object; + + // PyBytes object offset; + struct _bytes_object + { + uint64_t size; + uint64_t ob_size; + uint64_t ob_sval; + } bytes_object; + + // Unicode object offset; + struct _unicode_object + { + uint64_t size; + uint64_t state; + uint64_t length; + uint64_t asciiobject_size; + uint64_t compactunicodeobject_size; + } unicode_object; + + // GC runtime state offset; + struct _gc + { + uint64_t size; + uint64_t collecting; + uint64_t frame; + uint64_t generation_stats_size; + uint64_t generation_stats; + } gc; + + // Generator object offset; + struct _gen_object + { + uint64_t size; + uint64_t gi_name; + uint64_t gi_iframe; + uint64_t gi_frame_state; + } gen_object; + + struct _llist_node + { + uint64_t next; + uint64_t prev; + } llist_node; + + struct _debugger_support + { + uint64_t eval_breaker; + uint64_t remote_debugger_support; + uint64_t remote_debugging_enabled; + uint64_t debugger_pending_call; + uint64_t debugger_script_path; + uint64_t debugger_script_path_size; + } debugger_support; +} _Py_DebugOffsets; + +typedef struct pyruntimestate +{ + _Py_DebugOffsets debug_offsets; + int _initialized; + int preinitializing; + int preinitialized; + int core_initialized; + int initialized; + struct _pythreadstate* finalizing; + unsigned long _finalizing_id; + + struct pyinterpreters + { + Python3_13::PyMutex mutex; + PyInterpreterState* head; + PyInterpreterState* main; + int64_t next_id; + } interpreters; +} PyRuntimeState; + +} // namespace Python3_15 + typedef union { Python3_7::PyRuntimeState v3_7; Python3_8::PyRuntimeState v3_8; @@ -646,6 +886,7 @@ typedef union { Python3_12::PyRuntimeState v3_12; Python3_13::PyRuntimeState v3_13; Python3_14::PyRuntimeState v3_14; + Python3_15::PyRuntimeState v3_15; } PyRuntimeState; } // namespace pystack diff --git a/src/pystack/_pystack/cpython/string.h b/src/pystack/_pystack/cpython/string.h index b6618c3c..17d02fe0 100644 --- a/src/pystack/_pystack/cpython/string.h +++ b/src/pystack/_pystack/cpython/string.h @@ -122,9 +122,23 @@ struct _PyUnicode_State } // namespace Python3_14t +namespace Python3_15t { + +struct _PyUnicode_State +{ + unsigned char interned; + unsigned int kind : 3; + unsigned int compact : 1; + unsigned int ascii : 1; + unsigned int statically_allocated : 1; +}; + +} // namespace Python3_15t + union AnyPyUnicodeState { Python3::_PyUnicode_State python3; Python3_14t::_PyUnicode_State python3_14t; + Python3_15t::_PyUnicode_State python3_15t; }; } // namespace pystack diff --git a/src/pystack/_pystack/cpython/thread.h b/src/pystack/_pystack/cpython/thread.h index 977cd2ae..091481ec 100644 --- a/src/pystack/_pystack/cpython/thread.h +++ b/src/pystack/_pystack/cpython/thread.h @@ -362,4 +362,77 @@ typedef struct _pythreadstate } // namespace Python3_14 +namespace Python3_15 { + +typedef struct _remote_debugger_support +{ + int32_t debugger_pending_call; + char debugger_script_path[512]; +} _PyRemoteDebuggerSupport; + +typedef struct _pythreadstate +{ + struct _pythreadstate* prev; + struct _pythreadstate* next; + PyInterpreterState* interp; + uintptr_t eval_breaker; + struct + { + unsigned int initialized : 1; + unsigned int bound : 1; + unsigned int unbound : 1; + unsigned int bound_gilstate : 1; + unsigned int active : 1; + unsigned int finalizing : 1; + unsigned int cleared : 1; + unsigned int finalized : 1; + unsigned int : 24; + } _status; + int holds_gil; + int gil_requested; + int _whence; + int state; + int py_recursion_remaining; + int py_recursion_limit; + int recursion_headroom; + int tracing; + int what_event; + struct _PyInterpreterFrame* current_frame; + struct _PyInterpreterFrame* base_frame; + struct _PyInterpreterFrame* last_profiled_frame; + + Py_tracefunc c_profilefunc; + Py_tracefunc c_tracefunc; + PyObject* c_profileobj; + PyObject* c_traceobj; + PyObject* current_exception; + Python3_13::_PyErr_StackItem* exc_info; + PyObject* dict; + int gilstate_counter; + PyObject* async_exc; + unsigned long thread_id; + unsigned long native_thread_id; + PyObject* delete_later; + uintptr_t critical_section; + int coroutine_origin_tracking_depth; + PyObject* async_gen_firstiter; + PyObject* async_gen_finalizer; + PyObject* context; + uint64_t context_ver; + uint64_t id; + void* datastack_chunk; + PyObject** datastack_top; + PyObject** datastack_limit; + void* datastack_cached_chunk; + Python3_13::_PyErr_StackItem exc_state; + PyObject* current_executor; + uint64_t dict_global_version; + PyObject* threading_local_key; + PyObject* threading_local_sentinel; + _PyRemoteDebuggerSupport remote_debugger_support; + +} PyThreadState; + +} // namespace Python3_15 + } // namespace pystack diff --git a/src/pystack/_pystack/version.cpp b/src/pystack/_pystack/version.cpp index f58ff878..975b5446 100644 --- a/src/pystack/_pystack/version.cpp +++ b/src/pystack/_pystack/version.cpp @@ -746,6 +746,32 @@ python_v python_v3_14 = { // ----------------------------------------------------------------------------- +// ---- Python 3.15 ------------------------------------------------------------ + +python_v python_v3_15 = { + py_tuple(), + py_list(), + py_dict(), + py_dictkeys(), + py_dictvalues(), + py_float(), + py_long<_PyLongObject>(), + py_bytes(), + py_unicode(), + py_object(), + py_type(), + py_codev311(), + py_framev314(), + py_threadv313(), + py_isv312(), + py_runtimev313(), + py_gc(), + py_cframe(), + py_gilruntimestate(), +}; + +// ----------------------------------------------------------------------------- + const python_v* getCPythonOffsets(int major, int minor) { @@ -811,11 +837,14 @@ getCPythonOffsets(int major, int minor) case 13: return &python_v3_13; break; + case 14: + return &python_v3_14; + break; default: warnAboutUnsuportedVersion(major, minor); // fallthrough to latest - case 14: - return &python_v3_14; + case 15: + return &python_v3_15; break; } break; diff --git a/src/pystack/types.py b/src/pystack/types.py index fbd1eb13..c0f72011 100644 --- a/src/pystack/types.py +++ b/src/pystack/types.py @@ -52,6 +52,9 @@ def _is_eval_frame(symbol: str, python_version: Tuple[int, int]) -> bool: # Python 3.14 tail call interpreter uses LLVM-generated functions if symbol.startswith("_TAIL_CALL_") and ".llvm." in symbol: return True + # Python 3.15+ tail call interpreter drops the .llvm. suffix + if python_version >= (3, 15) and symbol.startswith("_TAIL_CALL_"): + return True return False diff --git a/tests/integration/test_relocatable_cores.py b/tests/integration/test_relocatable_cores.py index f19265ed..3c79111f 100644 --- a/tests/integration/test_relocatable_cores.py +++ b/tests/integration/test_relocatable_cores.py @@ -3,6 +3,7 @@ import sys from pathlib import Path +import pytest from pytest import LogCaptureFixture from pystack.engine import CoreFileAnalyzer @@ -17,6 +18,10 @@ TEST_MULTIPLE_THREADS_FILE = Path(__file__).parent / "multiple_thread_program.py" +@pytest.mark.skipif( + sys.version_info >= (3, 15), + reason="PyInstaller does not support Python 3.15 yet", +) def test_single_thread_stack_for_relocated_core( tmpdir: Path, caplog: LogCaptureFixture ) -> None: @@ -25,7 +30,6 @@ def test_single_thread_stack_for_relocated_core( that we can inspect and obtain all the information we need from the frame stack, including the native frames by using symbols only. """ - # GIVEN target_bundle = Path(tmpdir / "bundle") diff --git a/tests/utils.py b/tests/utils.py index 1963675a..cdaa2f04 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -23,6 +23,8 @@ PythonVersion = Tuple[Tuple[int, int], pathlib.Path] ALL_VERSIONS = [ + ((3, 15), "python3.15t"), + ((3, 15), "python3.15"), ((3, 14), "python3.14t"), ((3, 14), "python3.14"), ((3, 13), "python3.13t"),