diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index eba04c35d..d649547c0 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -33,9 +33,7 @@ ) from ..util.helpers import prepare_command, unwrap_preserving_paragraphs from ..util.packaging import ( - combine_constraints, find_compatible_wheel, - get_pip_version, ) from ..venv import constraint_flags, virtualenv from .macos import install_cpython as install_build_cpython @@ -461,7 +459,6 @@ def build(options: Options, tmp_path: Path) -> None: build_frontend=build_frontend.name, xbuild_tools=build_options.xbuild_tools, ) - pip_version = get_pip_version(env) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) if compatible_wheel: @@ -489,11 +486,6 @@ def build(options: Options, tmp_path: Path) -> None: build_frontend, build_options.build_verbosity, build_options.config_settings ) - build_env = env.copy() - build_env["VIRTUALENV_PIP"] = pip_version - if constraints_path: - combine_constraints(build_env, constraints_path, None) - match build_frontend.name: case "pip": # Path.resolve() is needed. Without it pip wheel may try to @@ -508,7 +500,7 @@ def build(options: Options, tmp_path: Path) -> None: f"--wheel-dir={built_wheel_dir}", "--no-deps", *extra_flags, - env=build_env, + env=env, ) case "build": call( @@ -519,7 +511,7 @@ def build(options: Options, tmp_path: Path) -> None: "--wheel", f"--outdir={built_wheel_dir}", *extra_flags, - env=build_env, + env=env, ) case _: assert_never(build_frontend) @@ -537,9 +529,7 @@ def build(options: Options, tmp_path: Path) -> None: elif config.arch != os.uname().machine: log.step("Skipping tests on non-native simulator architecture") else: - test_env = build_options.test_environment.as_dictionary( - prev_environment=build_env - ) + test_env = build_options.test_environment.as_dictionary(prev_environment=env) if build_options.before_test: before_test_prepared = prepare_command( diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py index 05d5d1623..5f47bee63 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -32,7 +32,7 @@ move_file, ) from ..util.helpers import prepare_command, unwrap -from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version +from ..util.packaging import find_compatible_wheel, get_pip_version from ..venv import constraint_flags, find_uv, virtualenv @@ -461,12 +461,6 @@ def build(options: Options, tmp_path: Path) -> None: ) build_env = env.copy() - if pip_version is not None: - build_env["VIRTUALENV_PIP"] = pip_version - if constraints_path: - combine_constraints( - build_env, constraints_path, identifier_tmp_dir if use_uv else None - ) match build_frontend.name: case "pip": diff --git a/cibuildwheel/platforms/pyodide.py b/cibuildwheel/platforms/pyodide.py index 60d707078..09de6a441 100644 --- a/cibuildwheel/platforms/pyodide.py +++ b/cibuildwheel/platforms/pyodide.py @@ -32,7 +32,7 @@ move_file, ) from ..util.helpers import prepare_command, unwrap, unwrap_preserving_paragraphs -from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version +from ..util.packaging import find_compatible_wheel, get_pip_version from ..util.python_build_standalone import ( PythonBuildStandaloneError, create_python_build_standalone_environment, @@ -419,17 +419,13 @@ def build(options: Options, tmp_path: Path) -> None: build_frontend, build_options.build_verbosity, build_options.config_settings ) - build_env = env.copy() - if constraints_path: - combine_constraints(build_env, constraints_path, identifier_tmp_dir) - build_env["VIRTUALENV_PIP"] = pip_version call( "pyodide", "build", build_options.package_dir, f"--outdir={built_wheel_dir}", *extra_flags, - env=build_env, + env=env, ) built_wheel = next(built_wheel_dir.glob("*.whl")) diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index f9c85be89..2bc20efc8 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -23,7 +23,7 @@ from ..util.cmd import call, shell from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, extract_zip, move_file from ..util.helpers import prepare_command, unwrap -from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version +from ..util.packaging import find_compatible_wheel, get_pip_version from ..venv import constraint_flags, find_uv, virtualenv @@ -461,13 +461,6 @@ def build(options: Options, tmp_path: Path) -> None: shell("graalpy -m pip install setuptools wheel", env=env) extra_flags = [*extra_flags, "-n"] - build_env = env.copy() - if pip_version is not None: - build_env["VIRTUALENV_PIP"] = pip_version - - if constraints_path: - combine_constraints(build_env, constraints_path, identifier_tmp_dir) - match build_frontend.name: case "pip": # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org @@ -481,7 +474,7 @@ def build(options: Options, tmp_path: Path) -> None: f"--wheel-dir={built_wheel_dir}", "--no-deps", *extra_flags, - env=build_env, + env=env, ) case "build" | "build[uv]": if ( @@ -499,7 +492,7 @@ def build(options: Options, tmp_path: Path) -> None: "--wheel", f"--outdir={built_wheel_dir}", *extra_flags, - env=build_env, + env=env, ) case _: assert_never(build_frontend) diff --git a/cibuildwheel/util/packaging.py b/cibuildwheel/util/packaging.py index 578f82e44..c3d8c21dd 100644 --- a/cibuildwheel/util/packaging.py +++ b/cibuildwheel/util/packaging.py @@ -1,5 +1,5 @@ import shlex -from collections.abc import Mapping, MutableMapping, Sequence +from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from pathlib import Path, PurePath from typing import Any, Literal, Self, TypeVar @@ -178,30 +178,3 @@ def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None: return wheel return None - - -def combine_constraints( - env: MutableMapping[str, str], /, constraints_path: Path, tmp_dir: Path | None -) -> None: - """ - This will workaround a bug in pip<=21.1.1 or uv<=0.2.0 if a tmp_dir is given. - If set to None, this will use the modern URI method. - """ - - if tmp_dir: - if " " in str(constraints_path): - assert " " not in str(tmp_dir) - tmp_file = tmp_dir / "constraints.txt" - tmp_file.write_bytes(constraints_path.read_bytes()) - constraints_path = tmp_file - our_constraints = str(constraints_path) - else: - our_constraints = ( - constraints_path.as_uri() if " " in str(constraints_path) else str(constraints_path) - ) - - user_constraints = env.get("PIP_CONSTRAINT") - - env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join( - c for c in [our_constraints, user_constraints] if c - ) diff --git a/test/test_dependency_versions.py b/test/test_dependency_versions.py index 5aa84a506..bec96cb3b 100644 --- a/test/test_dependency_versions.py +++ b/test/test_dependency_versions.py @@ -1,5 +1,7 @@ +import json import platform import re +import subprocess import textwrap from pathlib import Path @@ -9,40 +11,62 @@ from . import test_projects, utils -project_with_expected_version_checks = test_projects.new_c_project( - setup_py_add=textwrap.dedent( - r""" - import subprocess - import os - import sys +VERSION_REGEX = r"([\w-]+)==([^\s]+)" - versions_output_text = subprocess.check_output( - [sys.executable, '-m', 'pip', 'freeze', '--all', '-qq'], - universal_newlines=True, - ) - versions = versions_output_text.strip().splitlines() +CHECK_VERSIONS_SCRIPT = """\ +''' +Checks that the versions in the env var EXPECTED_VERSIONS match those +installed in the active venv. +''' +import os, subprocess, sys, json + +versions_raw = json.loads( + subprocess.check_output([ + sys.executable, '-m', 'pip', 'list', '--format=json', + ], text=True) +) +versions = {item['name']: item['version'] for item in versions_raw} +expected_versions = json.loads(os.environ['EXPECTED_VERSIONS']) + +for name, expected_version in expected_versions.items(): + if name not in versions: + continue + if versions[name] != expected_version: + raise SystemExit(f'error: {name} version should equal {expected_version}. Versions: {versions}') +""" - # `versions` now looks like: - # ['pip==x.x.x', 'setuptools==x.x.x', 'wheel==x.x.x'] - print('Gathered versions', versions) +def test_check_versions_script(tmp_path, build_frontend_env_nouv, capfd): + if utils.get_platform() == "linux": + pytest.skip("we don't test dependency versions on linux, refer to other tests") - expected_version = os.environ['EXPECTED_PIP_VERSION'] + # sanity check that the CHECK_VERSIONS_SCRIPT fails when it should + project_dir = tmp_path / "project" + test_projects.new_c_project().generate(project_dir) - assert f'pip=={expected_version}' in versions, ( - f'error: pip version should equal {expected_version}' + expected_versions = { + "pip": "0.0.1", + "build": "0.0.2", + } + script = project_dir / "check_versions.py" + script.write_text(CHECK_VERSIONS_SCRIPT) + + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run( + project_dir, + add_env={ + "CIBW_BEFORE_BUILD": f"python {script.name}", + "EXPECTED_VERSIONS": json.dumps(expected_versions), + **build_frontend_env_nouv, + }, ) - """ - ) -) -project_with_expected_version_checks.files["pyproject.toml"] = r""" -[build-system] -requires = ["setuptools", "pip"] -build-backend = "setuptools.build_meta" -""" + captured = capfd.readouterr() -VERSION_REGEX = r"([\w-]+)==([^\s]+)" + assert ( + "error: pip version should equal 0.0.1" in captured.err + or "error: build version should equal 0.0.2" in captured.err + ) def get_versions_from_constraint_file(constraint_file: Path) -> dict[str, str]: @@ -65,11 +89,15 @@ def test_pinned_versions(tmp_path, python_version, build_frontend_env_nouv): pytest.skip(f"Windows ARM64 does not support Python {python_version}") project_dir = tmp_path / "project" - project_with_expected_version_checks.generate(project_dir) + test_projects.new_c_project().generate(project_dir) + # read the expected versions from the appropriate constraint file version_no_dot = python_version.replace(".", "") - build_environment = {} - build_pattern = f"[cp]p{version_no_dot}-*" + + # create cross-platform Python before-build script to verify versions pre-build + before_build_script = project_dir / "check_versions.py" + before_build_script.write_text(CHECK_VERSIONS_SCRIPT) + if utils.get_platform() == "pyodide": constraint_filename = f"constraints-pyodide{version_no_dot}.txt" else: @@ -77,16 +105,13 @@ def test_pinned_versions(tmp_path, python_version, build_frontend_env_nouv): constraint_file = resources.PATH / constraint_filename constraint_versions = get_versions_from_constraint_file(constraint_file) - build_environment["EXPECTED_PIP_VERSION"] = constraint_versions["pip"] - - cibw_environment_option = " ".join(f"{k}={v}" for k, v in build_environment.items()) - - # build and test the wheels + # build and test the wheels (dependency version check occurs before-build) actual_wheels = utils.cibuildwheel_run( project_dir, add_env={ - "CIBW_BUILD": build_pattern, - "CIBW_ENVIRONMENT": cibw_environment_option, + "CIBW_BUILD": f"[cp]p{version_no_dot}-*", + "CIBW_BEFORE_BUILD": f"python {before_build_script.name}", + "EXPECTED_VERSIONS": json.dumps(constraint_versions), **build_frontend_env_nouv, }, ) @@ -107,10 +132,11 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv): pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead") project_dir = tmp_path / "project" - project_with_expected_version_checks.generate(project_dir) + test_projects.new_c_project().generate(project_dir) tool_versions = { "pip": "23.1.2", + "build": "1.2.2", "delocate": "0.10.3", } @@ -120,6 +146,7 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv): textwrap.dedent( """ pip=={pip} + build=={build} delocate=={delocate} """.format(**tool_versions) ) @@ -133,7 +160,7 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv): msg = f"Unknown method: {method}" raise ValueError(msg) - build_environment = {} + skip = "" if ( utils.get_platform() == "windows" @@ -144,32 +171,29 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv): # from a virtualenv seeded executable. See # https://github.com/oracle/graalpython/issues/491 and remove this once # fixed upstream. - build_frontend_env_nouv["CIBW_SKIP"] = "gp*" - - for package_name, version in tool_versions.items(): - env_name = f"EXPECTED_{package_name.upper()}_VERSION" - build_environment[env_name] = version + skip = "gp*" - cibw_environment_option = " ".join(f"{k}={v}" for k, v in build_environment.items()) + # cross-platform Python script for dependency constraint checks + before_build_script = project_dir / "check_versions.py" + before_build_script.write_text(CHECK_VERSIONS_SCRIPT) - # build and test the wheels + # build and test the wheels (dependency version check occurs pre-build) actual_wheels = utils.cibuildwheel_run( project_dir, add_env={ - "CIBW_ENVIRONMENT": cibw_environment_option, + "CIBW_SKIP": skip, "CIBW_DEPENDENCY_VERSIONS": dependency_version_option, + "CIBW_BEFORE_BUILD": f"python {before_build_script.name}", + "EXPECTED_VERSIONS": json.dumps(tool_versions), **build_frontend_env_nouv, }, + single_python=True, ) # also check that we got the right wheels - expected_wheels = utils.expected_wheels("spam", "0.1.0") + expected_wheels = utils.expected_wheels("spam", "0.1.0", single_python=True) - if ( - utils.get_platform() == "windows" - and method == "file" - and build_frontend_env_nouv["CIBW_BUILD_FRONTEND"] == "build" - ): + if skip == "gp*": # See reference to https://github.com/oracle/graalpython/issues/491 # above expected_wheels = [w for w in expected_wheels if "graalpy" not in w]