Skip to content

Add debug support for MSVC builds - full change PR - #1262

Open
vadikmironov wants to merge 3 commits into
astral-sh:mainfrom
vadikmironov:windows-debug-option
Open

vadikmironov wants to merge 3 commits into
astral-sh:mainfrom
vadikmironov:windows-debug-option

Conversation

@vadikmironov

@vadikmironov vadikmironov commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

This PR is a full implementation of debug support for Windows build (addresses #26 in full) and combines three commits together only for the purposes of review. Should maintainers have a preference to proceed with it, happy to assist, but otherwise I will be raising a set of three separate PRs for each commit to be discussed and merged.

1. Add a debug build option for Windows (cpython-windows/build.py, docs/building.rst)

  • debug is added to the option set beside noopt and pgo, with the same ordering as Unix build already uses. Same as with Unix, this removes illegal combinations like debug+pgo. The set is renamed from optimizations to options also inline with the Unix build.
  • configuration="Debug" is passed to msbuild and artifact_config accordingly. PC/layout gets --debug and that drives the _d artifact suffixes.
  • executables, extension modules and dependency libraries pick up the _d suffix $(PyDebugExt) produces
  • the tail-calling interpreter is not enabled for Debug (first finding below, comments embedded in the patch)
  • docs/building.rst updated with the debug option and the debug CRT requirement

2. Accept a Windows debug distribution in the validator and disttests (src/validation.rs, pythonbuild/disttests/__init__.py)

  • the PE allow list gains the debug CRT (ucrtbased.dll, VCRUNTIME140D.dll, VCRUNTIME140_1D.dll), the _d interpreter DLLs and sqlite3_d.dll
  • the abiflags check consults EXT_SUFFIX on Windows (comments as to why are embedded inn the patch)
  • test_ssl_with_keylogfile is skipped on Windows debug builds following CPython's suite skipping keylog there

3. Build the Windows debug configurations in CI (ci-targets.yaml)

  • each Windows target has pgo and freethreaded+pgo; each gains debug and freethreaded+debug

Evidence

Every configuration added in the third patch was built, validated and tested: 26 jobs, VS 2022 everywhere except x64 3.15 on VS 2026, arm64 on windows-11-arm through vcvarsamd64_arm64.bat (I see #1261 raised and I will try my testbed on it too as it may be relevant for my changes). debug for 3.10 to 3.15 on x64, x86 and (from 3.11) arm64; freethreaded+debug for 3.13 to 3.15 on all three. All green: https://github.com/vadikmironov/omniglot-bazel-starter/actions/runs/34095975591

Each job ran exactly what windows.yml runs: validate-distribution and then test-distribution.py through the distribution's own interpreter (22 tests, 12 skipped, OK on every job). The archives carry python314_d.dll and libs/python314_d.lib, python_d.exe next to the canonical python.exe copy the existing code makes, _d.cp314-win_amd64.pyd as EXT_SUFFIX, and Py_DEBUG=1 in the config vars; the free-threaded variant adds the t as expected. An embedder compiled /MDd against each links with only a /LIBPATH, and sys.gettotalrefcount() works at runtime.

Four gotchas that are commented in the patches

The tail-calling interpreter cannot be built in Debug. A guaranteed tail call needs optimisations which are removed by /Od. Microsoft documents [[msvc::musttail]] as requiring /O2 (https://learn.microsoft.com/en-us/cpp/cpp/attributes#msvcmusttail), and CPython's gh-148047 shows the same flood of errors for build.bat -c debug --tail-call-interp. It is off for Debug.

Py_DEBUG and ABIFLAGS reach Windows config vars only from 3.14. 3.13's _sysconfig.config_vars() supplies EXT_SUFFIX, SOABI and Py_GIL_DISABLED; 3.14 adds Py_DEBUG and emulates ABIFLAGS while keeping lowercase abiflags empty on Windows (https://github.com/python/cpython/blob/v3.14.7/Lib/sysconfig/__init__.py#L407). Validator reads EXT_SUFFIX instead.

From 3.14 a free-threaded Windows install no longer declares itself. 3.14 ships a static PC/pyconfig.h that only normalises an externally supplied Py_GIL_DISABLED, so an embedder that does not pass /DPy_GIL_DISABLED=1 gets a #pragma comment(lib, ...) naming the non-free-threaded import library. No changes per se and it applies to existing free-threaded builds already, but I will investigate the right course of actions separately.

SSLContext.keylog_filename is unsupported on Windows debug builds. CPython's suite skips every keylog test there (https://github.com/python/cpython/blob/v3.11.16/Lib/test/test_ssl.py#L4827), and from 3.12 the call raises NotImplementedError under _DEBUG (https://github.com/python/cpython/blob/v3.12.14/Modules/_ssl/debughelpers.c#L168, from python/cpython#131839). test_ssl_with_keylogfile is skipped on every Windows debug build same CPython.

Limitations

  • a debug distribution needs the debug CRT: ucrtbased.dll and vcruntime140d.dll are not redistributable by Microsoft. It only runs where Visual Studio is installed and it's exactly the same for CPython's debug binarie.
  • 3.10 has no arm64 configuration, following what ci-targets.yaml lists for that version
  • release-ABI wheels install into a debug interpreter but do not load, since Windows extension suffixes are _d-prefixed; CPython behaviour
  • crt_features stays vcruntime:140, though the debug binaries link the non-redistributable runtime. This is another open point and happy to tweak it further.

CI

Once the third commit is merged, it will add 26 jobs to every push to main, 15 to 26 minutes each on free runners. Again, happy to drop or amend it if that is too much.

cpython-unix/build.py offers `debug` alongside noopt and pgo, and every
Unix target declares it in ci-targets.yaml. Windows accepts only
noopt/pgo and hardcodes configuration="Release", so there is no way to
produce a debug interpreter.

That matters for embedding. MSVC's pyconfig.h selects the import library
from _DEBUG, which the debug CRT defines, so a consumer building with
/MDd asks for python3XX_d.lib and cannot link against a release
distribution.

Follows the Unix spelling: `debug` is a peer of noopt and pgo in the
option set, so debug+pgo is simply not a valid combination. The set is
renamed to `options` to match, since debug is not an optimization.

The rest is naming. A Debug configuration suffixes its artifacts with
$(PyDebugExt), so PC/layout is passed --debug, and the executables,
extension libraries and dependency libraries pick up the _d suffix.

The tail-calling interpreter is disabled for Debug. PBS turns it on for
3.15 on x64, but [[msvc::musttail]] requires /O2 and under /Od MSVC
reports C4737 for each dispatch site. CPython's early check for this was
reverted and the build still fails as of September 2026.

  https://learn.microsoft.com/en-us/cpp/cpp/attributes#msvcmusttail
  python/cpython#148047

building.rst documents the option and the debug CRT requirement: the
binaries import ucrtbased.dll and vcruntime*d.dll, which ship with
Visual Studio and are not redistributable.
validate-distribution rejects a Windows `debug` build on two counts.

The PE allow list carries only release names:

  error: python/install/python_d.exe loads illegal library python314_d.dll
  error: python/install/python_d.exe loads illegal library VCRUNTIME140D.dll
  error: python/install/python_d.exe loads illegal library ucrtbased.dll
  error: python/install/DLLs/_sqlite3_d.pyd loads illegal library sqlite3_d.dll

The other two formats already accommodate this. The Mach-O list pairs
every release name with its debug counterpart (@rpath/libpython3.14.dylib
next to @rpath/libpython3.14d.dylib, and td for free-threaded debug), and
the ELF list pushes libpython{ver}d.so.1.0 and libpython{ver}td.so.1.0
unconditionally. Neither gates on build options, so neither does this.

Windows spells the suffix _d, after $(PyDebugExt), which combines with
the free-threaded t as python314t_d.dll.

ucrtbased.dll sits in alphabetical position rather than beside a
counterpart because it has none: a release build reaches the UCRT
through the api-ms-win-crt-* forwarders, while a debug build imports it
directly.

The abiflags check is also POSIX-shaped:

  error: abiflags does not contain 'd'

CPython deliberately keeps the lowercase `abiflags` empty on Windows,
because it is widely used to calculate paths there. The uppercase
`ABIFLAGS` carries the marker, but only from 3.14:

  https://github.com/python/cpython/blob/v3.14.7/Lib/sysconfig/__init__.py#L407

So consult EXT_SUFFIX instead, which every supported version derives from
the importer and which a debug build suffixes with _d. That also drops an
unwrap() that would panic on a distribution missing the key.

The disttests trip on one more count: test_ssl_with_keylogfile. CPython's
own test suite skips keylog on Windows debug builds to avoid mixing the
debug and release CRT, and from 3.12 set_keylog_filename refuses the
call with NotImplementedError. On 3.11 the unguarded call crashed the
interpreter on arm64. So the disttest is skipped for every Windows
debug distribution, as CPython does.

  https://github.com/python/cpython/blob/v3.11.16/Lib/test/test_ssl.py#L4827
  https://github.com/python/cpython/blob/v3.12.14/Modules/_ssl/debughelpers.c#L168
  python/cpython#131839
The debug option is only useful if something builds it. Unix targets
already carry debug and freethreaded+debug beside their optimised
options; Windows carries neither.

Each Windows target has pgo and freethreaded+pgo today, so each gains
the matching debug pair, leaving the three in step with each other as
they already were.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant