Skip to content
Merged
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
56 changes: 43 additions & 13 deletions relenv/build/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def update_sqlite(dirs: Dirs, env: EnvMapping) -> None:
sqliteversion = sqlite_info.get("sqliteversion", "3500400")
url = sqlite_info["url"].format(version=sqliteversion)
sha256 = sqlite_info["sha256"]
ref_loc = f"cpe:2.3:a:sqlite:sqlite:{version}:*:*:*:*:*:*:*"
ref_loc = f"cpe:2.3:a:sqlite:sqlite:{version}:*:*:*:*:*:*:*" # noqa: E231

target_dir = dirs.source / "externals" / f"sqlite-{version}"
update_props(dirs.source, r"sqlite-\d+(\.\d+)*", f"sqlite-{version}")
Expand Down Expand Up @@ -268,7 +268,7 @@ def update_xz(dirs: Dirs, env: EnvMapping) -> None:
version = xz_info["version"]
url = xz_info["url"].format(version=version)
sha256 = xz_info["sha256"]
ref_loc = f"cpe:2.3:a:tukaani:xz:{version}:*:*:*:*:*:*:*"
ref_loc = f"cpe:2.3:a:tukaani:xz:{version}:*:*:*:*:*:*:*" # noqa: E231

target_dir = dirs.source / "externals" / f"xz-{version}"
update_props(dirs.source, r"xz-\d+(\.\d+)*", f"xz-{version}")
Expand Down Expand Up @@ -372,7 +372,7 @@ def update_openssl(dirs: Dirs, env: EnvMapping) -> None:
version = openssl_info["version"]
url = openssl_info["url"].format(version=version)
sha256 = openssl_info["sha256"]
ref_loc = f"cpe:2.3:a:openssl:openssl:{version}:*:*:*:*:*:*:*"
ref_loc = f"cpe:2.3:a:openssl:openssl:{version}:*:*:*:*:*:*:*" # noqa: E231

is_binary = "cpython-bin-deps" in url
target_dir = (
Expand Down Expand Up @@ -456,7 +456,9 @@ def update_openssl(dirs: Dirs, env: EnvMapping) -> None:

env_path = os.environ.get("PATH", "")
build_env = env.copy()
build_env["PATH"] = f"{perl_bin.parent};{nasm_exe[0].parent};{env_path}"
build_env[
"PATH"
] = f"{perl_bin.parent};{nasm_exe[0].parent};{env_path}" # noqa: E231,E702

prefix = target_dir / "build"
openssldir = prefix / "ssl"
Expand Down Expand Up @@ -607,7 +609,7 @@ def update_bzip2(dirs: Dirs, env: EnvMapping) -> None:
version = bzip2_info["version"]
url = bzip2_info["url"].format(version=version)
sha256 = bzip2_info["sha256"]
ref_loc = f"cpe:2.3:a:bzip:bzip2:{version}:*:*:*:*:*:*:*"
ref_loc = f"cpe:2.3:a:bzip:bzip2:{version}:*:*:*:*:*:*:*" # noqa: E231

target_dir = dirs.source / "externals" / f"bzip2-{version}"
update_props(dirs.source, r"bzip2-\d+(\.\d+)*", f"bzip2-{version}")
Expand Down Expand Up @@ -642,7 +644,7 @@ def update_libffi(dirs: Dirs, env: EnvMapping) -> None:
version = libffi_info["version"]
url = libffi_info["url"].format(version=version)
sha256 = libffi_info["sha256"]
ref_loc = f"cpe:2.3:a:libffi_project:libffi:{version}:*:*:*:*:*:*:*"
ref_loc = f"cpe:2.3:a:libffi_project:libffi:{version}:*:*:*:*:*:*:*" # noqa: E231

target_dir = dirs.source / "externals" / f"libffi-{version}"
update_props(dirs.source, r"libffi-\d+(\.\d+)*", f"libffi-{version}")
Expand Down Expand Up @@ -695,7 +697,7 @@ def update_zlib(dirs: Dirs, env: EnvMapping) -> None:
version = zlib_info["version"]
url = zlib_info["url"].format(version=version)
sha256 = zlib_info["sha256"]
ref_loc = f"cpe:2.3:a:gnu:zlib:{version}:*:*:*:*:*:*:*"
ref_loc = f"cpe:2.3:a:gnu:zlib:{version}:*:*:*:*:*:*:*" # noqa: E231

target_dir = dirs.source / "externals" / f"zlib-{version}"
update_props(dirs.source, r"zlib-\d+(\.\d+)*", f"zlib-{version}")
Expand Down Expand Up @@ -773,6 +775,37 @@ def update_perl(dirs: Dirs, env: EnvMapping) -> pathlib.Path:
return target_dir


def copy_pyconfig_h(
source: pathlib.Path, build_dir: pathlib.Path, dest_dir: pathlib.Path
) -> pathlib.Path:
"""
Copy ``pyconfig.h`` into the onedir's ``Include`` directory.

Python <= 3.12 ships a checked-in ``PC/pyconfig.h``. Python 3.13+ replaced
that with ``PC/pyconfig.h.in`` and MSBuild generates the real header into
the build output directory. This mirrors the logic in CPython's
``PC/layout/main.py`` so the onedir's ``Include/`` always ends up with a
``pyconfig.h`` next to ``Python.h`` -- without it C extensions cannot
locate the configuration macros and fail with
``fatal error C1083: Cannot open include file: 'pyconfig.h'``.
"""
pc_dir = source / "PC"
if (pc_dir / "pyconfig.h.in").is_file():
pyconfig_src = build_dir / "pyconfig.h"
else:
pyconfig_src = pc_dir / "pyconfig.h"

if not pyconfig_src.is_file():
raise RuntimeError(
f"Expected pyconfig.h at {pyconfig_src} but CPython build did "
"not produce it. Check that the MSBuild step ran successfully."
)

dest = dest_dir / "pyconfig.h"
shutil.copy(src=str(pyconfig_src), dst=str(dest))
return dest


def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None:
"""
Run the commands to build Python.
Expand Down Expand Up @@ -843,7 +876,7 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None:
"python.exe",
"pythonw.exe",
"python3.dll",
f"python{ env['RELENV_PY_MAJOR_VERSION'].replace('.', '') }.dll",
f"python{env['RELENV_PY_MAJOR_VERSION'].replace('.', '')}.dll",
"vcruntime140.dll",
"venvlauncher.exe",
"venvwlauncher.exe",
Expand All @@ -862,10 +895,7 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None:
dst=str(dirs.prefix / "Include"),
dirs_exist_ok=True,
)
if "3.13" not in env["RELENV_PY_MAJOR_VERSION"]:
shutil.copy(
src=str(dirs.source / "PC" / "pyconfig.h"), dst=str(dirs.prefix / "Include")
)
copy_pyconfig_h(dirs.source, build_dir, dirs.prefix / "Include")

shutil.copytree(
src=str(dirs.source / "Lib"), dst=str(dirs.prefix / "Lib"), dirs_exist_ok=True
Expand All @@ -877,7 +907,7 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None:
src=str(build_dir / "python3.lib"),
dst=str(dirs.prefix / "libs" / "python3.lib"),
)
pylib = f"python{ env['RELENV_PY_MAJOR_VERSION'].replace('.', '') }.lib"
pylib = f"python{env['RELENV_PY_MAJOR_VERSION'].replace('.', '')}.lib"
shutil.copy(src=str(build_dir / pylib), dst=str(dirs.prefix / "libs" / pylib))


Expand Down
96 changes: 96 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,99 @@ def test_download_destination_setter() -> None:
# Set to None
d.destination = None
assert d.destination == pathlib.Path()


# copy_pyconfig_h tests
#
# Regression coverage for the bug where Windows onedirs for Python 3.13+ were
# shipped without Include/pyconfig.h, breaking every C extension build.
# Python <= 3.12 has a checked-in PC/pyconfig.h; Python 3.13+ replaced it
# with PC/pyconfig.h.in and MSBuild generates the real header into the
# PCbuild output directory. copy_pyconfig_h must pick the right one.


def _make_layout(
tmp_path: pathlib.Path,
*,
has_in: bool,
pc_content: str = "",
build_content: str = "",
) -> tuple[pathlib.Path, pathlib.Path, pathlib.Path]:
"""Create source/, build_dir/, dest_dir/ trees mimicking a CPython tree."""
source = tmp_path / "cpython"
build_dir = tmp_path / "PCbuild" / "amd64"
dest_dir = tmp_path / "prefix" / "Include"
(source / "PC").mkdir(parents=True)
build_dir.mkdir(parents=True)
dest_dir.mkdir(parents=True)
if has_in:
(source / "PC" / "pyconfig.h.in").write_text("/* template */\n")
if build_content:
(build_dir / "pyconfig.h").write_text(build_content)
else:
if pc_content:
(source / "PC" / "pyconfig.h").write_text(pc_content)
return source, build_dir, dest_dir


def test_copy_pyconfig_h_legacy(tmp_path: pathlib.Path) -> None:
"""Python <= 3.12: copies PC/pyconfig.h (no pyconfig.h.in template)."""
from relenv.build.windows import copy_pyconfig_h

source, build_dir, dest_dir = _make_layout(
tmp_path, has_in=False, pc_content="/* checked in 3.12 */\n"
)
# Ensure the build_dir variant would NOT be picked up if it happened to
# exist as well -- the legacy path takes precedence when there's no .in.
(build_dir / "pyconfig.h").write_text("/* should be ignored */\n")

result = copy_pyconfig_h(source, build_dir, dest_dir)

assert result == dest_dir / "pyconfig.h"
assert result.is_file()
assert result.read_text() == "/* checked in 3.12 */\n"


def test_copy_pyconfig_h_generated(tmp_path: pathlib.Path) -> None:
"""Python 3.13+: copies the generated pyconfig.h from build_dir."""
from relenv.build.windows import copy_pyconfig_h

source, build_dir, dest_dir = _make_layout(
tmp_path, has_in=True, build_content="/* generated 3.13 */\n"
)
# A stale PC/pyconfig.h must NOT win when pyconfig.h.in is present.
(source / "PC" / "pyconfig.h").write_text("/* stale legacy */\n")

result = copy_pyconfig_h(source, build_dir, dest_dir)

assert result == dest_dir / "pyconfig.h"
assert result.is_file()
assert result.read_text() == "/* generated 3.13 */\n"


def test_copy_pyconfig_h_missing_generated_raises(tmp_path: pathlib.Path) -> None:
"""3.13+ layout but MSBuild didn't produce pyconfig.h -- must raise.

This is the failure mode that previously slipped through silently and
produced unusable tarballs.
"""
from relenv.build.windows import copy_pyconfig_h

source, build_dir, dest_dir = _make_layout(tmp_path, has_in=True)
# No pyconfig.h written into build_dir.

with pytest.raises(RuntimeError, match="Expected pyconfig.h at"):
copy_pyconfig_h(source, build_dir, dest_dir)
assert not (dest_dir / "pyconfig.h").exists()


def test_copy_pyconfig_h_missing_legacy_raises(tmp_path: pathlib.Path) -> None:
"""Legacy layout but PC/pyconfig.h is absent -- must raise rather than silently no-op."""
from relenv.build.windows import copy_pyconfig_h

source, build_dir, dest_dir = _make_layout(tmp_path, has_in=False)
# No pyconfig.h written into PC/ either.

with pytest.raises(RuntimeError, match="Expected pyconfig.h at"):
copy_pyconfig_h(source, build_dir, dest_dir)
assert not (dest_dir / "pyconfig.h").exists()
Loading