Skip to content
Open
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
6 changes: 6 additions & 0 deletions ci-targets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"

Expand All @@ -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"
49 changes: 31 additions & 18 deletions cpython-windows/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = "%s.cp%st-%s" % (debug_suffix, python_majmin, abi_platform)
lib_suffix = "t%s" % debug_suffix
Comment on lines +1223 to +1224

@jjhelmus jjhelmus Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would f-strings be better here and in similar changes?

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"
Expand Down Expand Up @@ -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 / ("%s%s.lib" % (depend, debug_suffix))
static_dest = lib_dir / ("%s%s.lib" % (depend, debug_suffix))

log("copying link library %s" % static_source)
shutil.copyfile(static_source, static_dest)

shared_source = outputs_path / ("%s.dll" % depend)
shared_source = outputs_path / ("%s%s.dll" % (depend, debug_suffix))
if shared_source.exists():
shared_dest = lib_dir / ("%s.dll" % depend)
shared_dest = lib_dir / ("%s%s.dll" % (depend, debug_suffix))
log("copying shared library %s" % shared_source)
shutil.copyfile(shared_source, shared_dest)

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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",
)
Expand Down
11 changes: 11 additions & 0 deletions docs/building.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://learn.microsoft.com/en-us/cpp/windows/determining-which-dlls-to-redistribute>`_,
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``.
8 changes: 8 additions & 0 deletions pythonbuild/disttests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 33 additions & 8 deletions src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
];
Expand Down Expand Up @@ -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() {
Expand Down