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
11 changes: 11 additions & 0 deletions relenv/build/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ def populate_env(env: EnvMapping, dirs: Dirs) -> None:
"""
env["MSBUILDDISABLENODEREUSE"] = "1"

# CPython's PCbuild\find_python.bat only looks for a pre-installed
# bootstrap interpreter via specific "py -X.Y" versions (e.g. 3.10's
# copy of the script only tries 3.9/3.8), which age out of GitHub's
# hosted Windows runner images as those versions go EOL. When none of
# those match, it falls back to downloading a bootstrap Python via
# NuGet, which is an extra, flaky, network-dependent step. Point
# find_python.bat at the interpreter already running this build (it
# checks HOST_PYTHON before either of those) so it never needs to do
# either.
env["HOST_PYTHON"] = sys.executable


def find_vcvarsall(env: EnvMapping) -> pathlib.Path | None:
"""
Expand Down
172 changes: 29 additions & 143 deletions relenv/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import ctypes as _ctypes
import functools
import importlib as _importlib
import inspect
import json as _json
import os
import pathlib
Expand Down Expand Up @@ -347,27 +348,16 @@ def wrapper(
wheel_path: PathType,
scheme: Any,
req_description: str,
pycompile: Any,
warn_script_location: Any,
direct_url: Any,
requested: Any,
*args: Any,
**kwargs: Any,
) -> Any:
from zipfile import ZipFile

from pip._internal.utils.wheel import parse_wheel

with ZipFile(wheel_path) as zf:
info_dir, metadata = parse_wheel(zf, name)
func(
name,
wheel_path,
scheme,
req_description,
pycompile,
warn_script_location,
direct_url,
requested,
)
func(name, wheel_path, scheme, req_description, *args, **kwargs)
if "RELENV_BUILDENV" in os.environ:
plat = pathlib.Path(scheme.platlib)
rootdir = relenv_root()
Expand Down Expand Up @@ -825,141 +815,37 @@ def wrapper(

def wrap_req_install(name: str) -> ModuleType:
"""
Honor ignore installed option from pip cli.
Ensure installs honor the TARGET home directory, staying compatible with
whatever shape pip's InstallRequirement.install currently has (pip has
changed this signature several times across releases, most recently
adding `script_executable` in pip 26.2).
"""
module: ModuleType = importlib.import_module(name)
mod = cast("Any", module)

original = mod.InstallRequirement.install
argcount = original.__code__.co_argcount

if argcount == 7:

@functools.wraps(original)
def install_wrapper_pep517(
self: Any,
root: PathType | None = None,
home: PathType | None = None,
prefix: PathType | None = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
) -> Any:
try:
if TARGET.TARGET:
TARGET.INSTALL = True
home = _ensure_target_path()
return original(
self,
root,
home,
prefix,
warn_script_location,
use_user_site,
pycompile,
)
finally:
TARGET.INSTALL = False

mod.InstallRequirement.install = install_wrapper_pep517

elif argcount == 8:

@functools.wraps(original)
def install_wrapper_pep517_opts(
self: Any,
global_options: Any = None,
root: PathType | None = None,
home: PathType | None = None,
prefix: PathType | None = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
) -> Any:
try:
if TARGET.TARGET:
TARGET.INSTALL = True
home = _ensure_target_path()
return original(
self,
global_options,
root,
home,
prefix,
warn_script_location,
use_user_site,
pycompile,
)
finally:
TARGET.INSTALL = False

mod.InstallRequirement.install = install_wrapper_pep517_opts

elif argcount == 9:

@functools.wraps(original)
def install_wrapper_legacy(
self: Any,
install_options: Any,
global_options: Any = None,
root: PathType | None = None,
home: PathType | None = None,
prefix: PathType | None = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
) -> Any:
try:
if TARGET.TARGET:
TARGET.INSTALL = True
home = _ensure_target_path()
return original(
self,
install_options,
global_options,
root,
home,
prefix,
warn_script_location,
use_user_site,
pycompile,
)
finally:
TARGET.INSTALL = False

mod.InstallRequirement.install = install_wrapper_legacy

else:

@functools.wraps(original)
def install_wrapper_generic(
self: Any,
global_options: Any = None,
root: PathType | None = None,
home: PathType | None = None,
prefix: PathType | None = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
) -> Any:
try:
if TARGET.TARGET:
TARGET.INSTALL = True
home = _ensure_target_path()
return original(
self,
global_options,
root,
home,
prefix,
warn_script_location,
use_user_site,
pycompile,
)
finally:
TARGET.INSTALL = False
sig = inspect.signature(original)
if "home" not in sig.parameters:
raise RuntimeError(
"pip's InstallRequirement.install no longer has a 'home' "
f"parameter (signature is now {sig}); relenv's TARGET-install "
"support needs to be updated for this pip version."
)

mod.InstallRequirement.install = install_wrapper_generic
@functools.wraps(original)
def install_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
try:
if TARGET.TARGET:
TARGET.INSTALL = True
bound = sig.bind_partial(self, *args, **kwargs)
bound.apply_defaults()
bound.arguments["home"] = _ensure_target_path()
return original(*bound.args, **bound.kwargs)
return original(self, *args, **kwargs)
finally:
TARGET.INSTALL = False

mod.InstallRequirement.install = install_wrapper
return module


Expand Down
19 changes: 19 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import logging
import pathlib
import sys

import pytest

Expand Down Expand Up @@ -475,6 +476,24 @@ def _make_layout(
return source, build_dir, dest_dir


def test_populate_env_sets_host_python() -> None:
"""
CPython's PCbuild/find_python.bat only looks for a pre-installed
bootstrap interpreter via specific "py -X.Y" versions (e.g. 3.10's copy
of the script only tries 3.9/3.8), both of which are EOL and age out of
CI images over time; when neither is found it falls back to an
unreliable NuGet download. HOST_PYTHON is checked before either of
those, so populate_env should point it at the interpreter already
running the build.
"""
from relenv.build.windows import populate_env

dirs = Dirs(work_dirs(), "python", "amd64", "3.10.20")
env: dict[str, str] = {}
populate_env(env, dirs)
assert env["HOST_PYTHON"] == sys.executable


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
Expand Down
111 changes: 111 additions & 0 deletions tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,58 @@ def original_install(*_args: object, **_kwargs: object) -> str:
assert handled and handled[0][0].name == "libdemo.so"


def test_install_wheel_wrapper_forwards_new_kwargs(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Pip 26.2 added a `script_executable` parameter to install_wheel; the
wrapper must forward it (and anything else pip adds) through untouched
instead of rejecting it as an unexpected keyword argument.
"""
wheel_utils = ModuleType("pip._internal.utils.wheel")
wheel_utils.parse_wheel = lambda _zf, _name: ("demo.dist-info", {})
monkeypatch.setitem(sys.modules, wheel_utils.__name__, wheel_utils)

class DummyZip:
def __init__(self, path: pathlib.Path) -> None:
self.path = path

def __enter__(self) -> DummyZip:
return self

def __exit__(self, *exc: object) -> bool:
return False

monkeypatch.setattr("zipfile.ZipFile", DummyZip)

install_module = ModuleType("pip._internal.operations.install.wheel")
received: dict[str, object] = {}

def original_install(
_name: str, _wheel_path: object, _scheme: object, _req_description: str, **kwargs: object
) -> str:
received.update(kwargs)
return "original"

install_module.install_wheel = original_install # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, install_module.__name__, install_module)

wrapped_module = relenv.runtime.wrap_pip_install_wheel(install_module.__name__)

scheme = SimpleNamespace(platlib=str(tmp_path))
wrapped_module.install_wheel(
"demo",
tmp_path / "wheel.whl",
scheme,
"desc",
pycompile=True,
warn_script_location=True,
direct_url=None,
requested=False,
script_executable="/target/bin/python3",
)

assert received["script_executable"] == "/target/bin/python3"


def test_install_wheel_wrapper_missing_file(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("RELENV_BUILDENV", "1")
plat_dir = tmp_path / "plat"
Expand Down Expand Up @@ -1253,6 +1305,65 @@ def install(
assert result == "installed"


def test_wrap_req_install_pip_26_2_signature(monkeypatch: pytest.MonkeyPatch) -> None:
"""
Pip 26.2 added a `script_executable` keyword argument to
InstallRequirement.install. Ensure the wrapper still overrides `home`
when a TARGET install is active, and forwards script_executable through
untouched instead of rejecting it as an unexpected keyword argument.
"""
relenv.runtime.TARGET.TARGET = True
relenv.runtime.TARGET.PATH = "/pip26/target"

module_name = "pip._internal.req.req_install.pip262"
module = ModuleType(module_name)

class InstallRequirement:
def install(
self,
root: object = None,
home: object = None,
prefix: object = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
script_executable: object = None,
) -> tuple[object, object]:
return script_executable, home

module.InstallRequirement = InstallRequirement
monkeypatch.setitem(sys.modules, module_name, module)
monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)

wrapped = relenv.runtime.wrap_req_install(module_name)
installer = wrapped.InstallRequirement()
script_executable, home = installer.install(script_executable="/target/bin/python3")

assert home == relenv.runtime.TARGET.PATH
assert script_executable == "/target/bin/python3"


def test_wrap_req_install_missing_home_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""
If a future pip ever drops the `home` parameter entirely, relenv's
TARGET-install support can no longer work correctly. Fail loudly at
bootstrap time instead of silently doing nothing.
"""
module_name = "pip._internal.req.req_install.nohome"
module = ModuleType(module_name)

class InstallRequirement:
def install(self, root: object = None, prefix: object = None) -> None:
return None

module.InstallRequirement = InstallRequirement
monkeypatch.setitem(sys.modules, module_name, module)
monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)

with pytest.raises(RuntimeError, match="home"):
relenv.runtime.wrap_req_install(module_name)


def test_wrapsitecustomize_sanitizes_sys_path(monkeypatch: pytest.MonkeyPatch) -> None:
sanitized = ["sanitized/path"]
monkeypatch.setattr(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_verify_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2044,7 +2044,7 @@ def test_import_ssl_module(pyexec):


@pytest.mark.skip_unless_on_linux
@pytest.mark.parametrize("pip_version", ["25.2", "25.3"])
@pytest.mark.parametrize("pip_version", ["25.2", "25.3", "26.2"])
def test_install_setuptools_25_2_to_25_3(pipexec, build, minor_version, pip_version):
"""
Validate we handle the changes to pip._internal.req.InstallRequirement.install signature.
Expand Down
Loading