diff --git a/ci-targets.yaml b/ci-targets.yaml index d75eb60cf..459aba1ac 100644 --- a/ci-targets.yaml +++ b/ci-targets.yaml @@ -387,9 +387,11 @@ windows: - "3.15" vs_version: "2022" build_options: + - debug - pgo build_options_conditional: - options: + - freethreaded+debug - freethreaded+pgo minimum-python-version: "3.13" @@ -408,9 +410,11 @@ windows: vs_version: "2026" minimum-python-version: "3.15" build_options: + - debug - pgo build_options_conditional: - options: + - freethreaded+debug - freethreaded+pgo minimum-python-version: "3.13" @@ -427,8 +431,10 @@ windows: - "3.15" vs_version: "2022" build_options: + - debug - pgo build_options_conditional: - options: + - freethreaded+debug - freethreaded+pgo minimum-python-version: "3.13" diff --git a/cpython-windows/build.py b/cpython-windows/build.py index 9b36600c3..35e958b04 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -777,8 +777,15 @@ def run_msbuild( if freethreaded: args.append("/property:DisableGil=true") - # Build tail-calling Python for 3.15+ - if python_version.startswith("3.15") and platform == "x64": + # Build tail-calling Python for 3.15+, but not in Debug. [[msvc::musttail]] + # requires /O2 and under /Od MSVC reports C4737 at every dispatch site. + # See https://learn.microsoft.com/en-us/cpp/cpp/attributes#msvcmusttail + # and https://github.com/python/cpython/issues/148047 + if ( + python_version.startswith("3.15") + and platform == "x64" + and configuration != "Debug" + ): args.append("/property:UseTailCallInterp=true") exec_and_log(args, str(pcbuild_path), os.environ) @@ -1211,12 +1218,13 @@ def find_additional_dependencies(project: pathlib.Path): else: raise Exception("unhandled architecture: %s" % arch) + debug_suffix = "_d" if config == "Debug" else "" if freethreaded: - abi_tag = ".cp%st-%s" % (python_majmin, abi_platform) - lib_suffix = "t" + abi_tag = f"{debug_suffix}.cp{python_majmin}t-{abi_platform}" + lib_suffix = f"t{debug_suffix}" else: - abi_tag = "" - lib_suffix = "" + abi_tag = debug_suffix + lib_suffix = debug_suffix # Copy object files for core sources into their own directory. core_dir = out_dir / "build" / "core" @@ -1345,15 +1353,15 @@ def find_additional_dependencies(project: pathlib.Path): # Copy libraries for dependencies into the lib directory. for depend in sorted(depends_projects): - static_source = outputs_path / ("%s.lib" % depend) - static_dest = lib_dir / ("%s.lib" % depend) + static_source = outputs_path / f"{depend}{debug_suffix}.lib" + static_dest = lib_dir / f"{depend}{debug_suffix}.lib" log("copying link library %s" % static_source) shutil.copyfile(static_source, static_dest) - shared_source = outputs_path / ("%s.dll" % depend) + shared_source = outputs_path / f"{depend}{debug_suffix}.dll" if shared_source.exists(): - shared_dest = lib_dir / ("%s.dll" % depend) + shared_dest = lib_dir / f"{depend}{debug_suffix}.dll" log("copying shared library %s" % shared_source) shutil.copyfile(shared_source, shared_dest) @@ -1372,6 +1380,7 @@ def build_cpython( openssl_entry: str, ) -> pathlib.Path: parsed_build_options = set(build_options.split("+")) + debug = "debug" in parsed_build_options pgo = "pgo" in parsed_build_options freethreaded = "freethreaded" in parsed_build_options @@ -1426,13 +1435,14 @@ def build_cpython( # as we do for Unix builds. mpdecimal_archive = None + debug_suffix = "_d" if debug else "" if freethreaded: (major, minor, _) = python_version.split(".") - python_exe = f"python{major}.{minor}t.exe" - pythonw_exe = f"pythonw{major}.{minor}t.exe" + python_exe = f"python{major}.{minor}t{debug_suffix}.exe" + pythonw_exe = f"pythonw{major}.{minor}t{debug_suffix}.exe" else: - python_exe = "python.exe" - pythonw_exe = "pythonw.exe" + python_exe = f"python{debug_suffix}.exe" + pythonw_exe = f"pythonw{debug_suffix}.exe" # Python 3.15 uses the default name for the executable in a suffixed directory instrumented_python_exe = python_exe @@ -1635,13 +1645,13 @@ def build_cpython( run_msbuild( msbuild, pcbuild_path, - configuration="Release", + configuration="Debug" if debug else "Release", platform=build_platform, python_version=python_version, windows_sdk_version=windows_sdk_version, freethreaded=freethreaded, ) - artifact_config = "Release" + artifact_config = "Debug" if debug else "Release" install_dir = out_dir / "python" / "install" @@ -1675,6 +1685,9 @@ def build_cpython( if freethreaded: args.append("--include-freethreaded") + if debug: + args.append("--debug") + # CPython 3.12 removed distutils. if not meets_python_minimum_version(python_version, "3.12"): args.append("--include-distutils") @@ -1946,10 +1959,10 @@ def main() -> None: default="cpython-3.11", help="Python distribution to build", ) - optimizations = {"noopt", "pgo"} + options = {"debug", "noopt", "pgo"} parser.add_argument( "--options", - choices=optimizations.union({f"freethreaded+{o}" for o in optimizations}), + choices=options.union({f"freethreaded+{o}" for o in options}), default="noopt", help="Build options to apply when compiling Python", ) diff --git a/docs/building.rst b/docs/building.rst index 5ea5ba9a0..ed5e8f5fd 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -148,5 +148,16 @@ with Visual Studio 2026:: $ uv run --no-dev build.py --sh c:\cygwin\bin\sh.exe --vs 2026 --python cpython-3.15 +To produce a debug build, pass ``--options debug`` or ``freethreaded+debug``:: + + $ uv run --no-dev build.py --sh c:\cygwin\bin\sh.exe --options debug + +The artifacts carry the ``_d`` suffix of CPython's Debug configuration +(``python_d.exe``, ``python314_d.dll``, ``_asyncio_d.pyd``) and link against +the debug C runtime (``ucrtbased.dll`` and ``vcruntime*d.dll``). That +runtime ships with Visual Studio and `is not redistributable +`_, +so a debug distribution only runs on a machine with Visual Studio installed. + To build a 32-bit x86 binary, simply use an ``x86 Native Tools Command Prompt`` instead of ``x64``. diff --git a/pythonbuild/disttests/__init__.py b/pythonbuild/disttests/__init__.py index 10df91f3d..b366e2ec4 100644 --- a/pythonbuild/disttests/__init__.py +++ b/pythonbuild/disttests/__init__.py @@ -218,6 +218,14 @@ def test_ssl(self): ssl.create_default_context() @unittest.skipIf(os.name != "nt", "Windows-specific OpenSSL uplink regression") + # CPython's own suite skips keylog tests on Windows debug builds, to avoid + # mixing the debug and release CRT; 3.12+ refuses the call outright. + # 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 + @unittest.skipIf( + os.name == "nt" and "debug" in os.environ["BUILD_OPTIONS"].split("+"), + "keylog_filename is unsupported on Windows debug builds", + ) def test_ssl_with_keylogfile(self): # Validate that a SSLContext can be created when SSLKEYLOGFILE is set # https://github.com/astral-sh/python-build-standalone/issues/640 diff --git a/src/validation.rs b/src/validation.rs index 7cdabb378..05da9a58d 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -104,11 +104,14 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[ "RPCRT4.dll", "SHELL32.dll", "SHLWAPI.dll", + "ucrtbased.dll", "USER32.dll", "USERENV.dll", "VERSION.dll", "VCRUNTIME140.dll", + "VCRUNTIME140D.dll", "VCRUNTIME140_1.dll", + "VCRUNTIME140_1D.dll", "WINMM.dll", "WS2_32.dll", // Our libraries. @@ -124,18 +127,31 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[ "libssl-3-arm64.dll", "libssl-3-x64.dll", "python3.dll", + "python3_d.dll", "python3t.dll", + "python3t_d.dll", "python39.dll", + "python39_d.dll", "python310.dll", + "python310_d.dll", "python311.dll", + "python311_d.dll", "python312.dll", + "python312_d.dll", "python313.dll", + "python313_d.dll", "python313t.dll", + "python313t_d.dll", "python314.dll", + "python314_d.dll", "python314t.dll", + "python314t_d.dll", "python315.dll", + "python315_d.dll", "python315t.dll", + "python315t_d.dll", "sqlite3.dll", + "sqlite3_d.dll", "tcl86t.dll", "tk86t.dll", ]; @@ -1931,14 +1947,23 @@ fn validate_json(json: &PythonJsonMain, triple: &str, is_debug: bool) -> Result< )); } - if is_debug - && !json - .python_config_vars - .get("abiflags") - .unwrap() - .contains('d') - { - errors.push("abiflags does not contain 'd'".to_string()); + if is_debug { + // Windows keeps `abiflags` empty on purpose and only emulates `ABIFLAGS` + // from 3.14, so consult EXT_SUFFIX, which every version suffixes with `_d`. + // See https://github.com/python/cpython/blob/v3.14.7/Lib/sysconfig/__init__.py#L407 + let (key, marker) = if triple.contains("-windows-") { + ("EXT_SUFFIX", "_d") + } else { + ("abiflags", "d") + }; + + match json.python_config_vars.get(key) { + Some(value) if value.contains(marker) => {} + Some(value) => { + errors.push(format!("{key} is {value:?}, expected to contain {marker:?}")) + } + None => errors.push(format!("{key} is not set")), + } } for extension in json.build_info.extensions.keys() {