-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add build constraints #13534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add build constraints #13534
Changes from all commits
2e4086e
1aa1d32
db4fcf7
d9d5f5d
8d170b5
9f9032c
a9e81d7
4fbafeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
Add support for build constraints via the ``--build-constraint`` option. This | ||
allows constraining the versions of packages used during the build process | ||
(e.g., setuptools). | ||
|
||
When using ``--build-constraint``, you can no longer pass constraints to | ||
isolated build environments via the ``PIP_CONSTRAINT`` environment variable. | ||
To opt in to this behavior without specifying any build constraints, use | ||
``--use-feature=build-constraint``. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,14 +11,15 @@ | |
from collections import OrderedDict | ||
from collections.abc import Iterable | ||
from types import TracebackType | ||
from typing import TYPE_CHECKING, Protocol | ||
from typing import TYPE_CHECKING, Protocol, TypedDict | ||
|
||
from pip._vendor.packaging.version import Version | ||
|
||
from pip import __file__ as pip_location | ||
from pip._internal.cli.spinners import open_spinner | ||
from pip._internal.locations import get_platlib, get_purelib, get_scheme | ||
from pip._internal.metadata import get_default_environment, get_environment | ||
from pip._internal.utils.deprecation import deprecated | ||
from pip._internal.utils.logging import VERBOSE | ||
from pip._internal.utils.packaging import get_requirement | ||
from pip._internal.utils.subprocess import call_subprocess | ||
|
@@ -31,6 +32,10 @@ | |
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ExtraEnviron(TypedDict, total=False): | ||
extra_environ: dict[str, str] | ||
|
||
|
||
def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]: | ||
return (a, b) if a != b else (a,) | ||
|
||
|
@@ -101,8 +106,55 @@ class SubprocessBuildEnvironmentInstaller: | |
Install build dependencies by calling pip in a subprocess. | ||
""" | ||
|
||
def __init__(self, finder: PackageFinder) -> None: | ||
def __init__( | ||
self, | ||
finder: PackageFinder, | ||
build_constraints: list[str] | None = None, | ||
build_constraint_feature_enabled: bool = False, | ||
constraints: list[str] | None = None, | ||
) -> None: | ||
self.finder = finder | ||
self._build_constraints = build_constraints or [] | ||
self._build_constraint_feature_enabled = build_constraint_feature_enabled | ||
self._constraints = constraints or [] | ||
|
||
def _deprecation_constraint_check(self) -> None: | ||
""" | ||
Check for deprecation warning: PIP_CONSTRAINT affecting build environments. | ||
|
||
This warns when build-constraint feature is NOT enabled but regular constraints | ||
match what PIP_CONSTRAINT environment variable points to. | ||
""" | ||
if self._build_constraint_feature_enabled: | ||
return | ||
|
||
if self._build_constraints: | ||
return | ||
|
||
if not self._constraints: | ||
return | ||
|
||
if not os.environ.get("PIP_CONSTRAINT"): | ||
return | ||
|
||
pip_constraint_files = [ | ||
f for f in os.environ["PIP_CONSTRAINT"].split() if f.strip() | ||
] | ||
if pip_constraint_files and pip_constraint_files == self._constraints: | ||
Comment on lines
+134
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume the scenario where the stored constraints do not match |
||
deprecated( | ||
reason=( | ||
"Setting PIP_CONSTRAINT will not affect " | ||
"build constraints in the future," | ||
), | ||
replacement=( | ||
"to specify build constraints using --build-constraint or " | ||
"PIP_BUILD_CONSTRAINT. To disable this warning without " | ||
"any build constraints set --use-feature=build-constraint or " | ||
'PIP_USE_FEATURE="build-constraint"' | ||
), | ||
gone_in="26.2", | ||
issue=None, | ||
) | ||
|
||
def install( | ||
self, | ||
|
@@ -112,6 +164,8 @@ def install( | |
kind: str, | ||
for_req: InstallRequirement | None, | ||
) -> None: | ||
self._deprecation_constraint_check() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably issue this deprecation warning only once. In some scenarios, even a single build will trigger this deprecation twice as the build backend can request additional dependencies dynamically. ... although I say that having tried this locally... I can't get the deprecation to be printed twice. Hmm. |
||
|
||
finder = self.finder | ||
args: list[str] = [ | ||
sys.executable, | ||
|
@@ -167,13 +221,34 @@ def install( | |
args.append("--pre") | ||
if finder.prefer_binary: | ||
args.append("--prefer-binary") | ||
|
||
# Handle build constraints | ||
extra_environ: ExtraEnviron = {} | ||
if self._build_constraint_feature_enabled: | ||
args.extend(["--use-feature", "build-constraint"]) | ||
|
||
if self._build_constraints: | ||
# Build constraints must be passed as both constraints | ||
# and build constraints, so that nested builds receive | ||
# build constraints | ||
for constraint_file in self._build_constraints: | ||
args.extend(["--constraint", constraint_file]) | ||
args.extend(["--build-constraint", constraint_file]) | ||
|
||
if self._build_constraint_feature_enabled and not self._build_constraints: | ||
# If there are no build constraints but the build constraint | ||
# feature is enabled then we must ignore regular constraints | ||
# in the isolated build environment | ||
extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}} | ||
|
||
args.append("--") | ||
args.extend(requirements) | ||
with open_spinner(f"Installing {kind}") as spinner: | ||
call_subprocess( | ||
args, | ||
command_desc=f"pip subprocess to install {kind}", | ||
spinner=spinner, | ||
**extra_environ, | ||
) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -101,6 +101,18 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None: | |
) | ||
|
||
|
||
def check_build_constraints(options: Values) -> None: | ||
"""Function for validating build constraint options. | ||
Comment on lines
+104
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good forward thinking here! |
||
:param options: The OptionParser options. | ||
""" | ||
if hasattr(options, "build_constraints") and options.build_constraints: | ||
if not options.build_isolation: | ||
raise CommandError( | ||
"--build-constraint cannot be used with --no-build-isolation." | ||
) | ||
|
||
|
||
def _path_option_check(option: Option, opt: str, value: str) -> str: | ||
return os.path.expanduser(value) | ||
|
||
|
@@ -430,6 +442,21 @@ def constraints() -> Option: | |
) | ||
|
||
|
||
def build_constraint() -> Option: | ||
return Option( | ||
"--build-constraint", | ||
dest="build_constraints", | ||
action="append", | ||
type="str", | ||
default=[], | ||
metavar="file", | ||
help=( | ||
"Constrain build dependencies using the given constraints file. " | ||
"This option can be used multiple times." | ||
), | ||
) | ||
|
||
|
||
def requirements() -> Option: | ||
return Option( | ||
"-r", | ||
|
@@ -1072,6 +1099,7 @@ def check_list_path_option(options: Values) -> None: | |
default=[], | ||
choices=[ | ||
"fast-deps", | ||
"build-constraint", | ||
] | ||
+ ALWAYS_ENABLED_FEATURES, | ||
help="Enable new functionality, that may be backward incompatible.", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of defining this typed dictionary? It seems superfluous IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am all open to suggestions that don't make the code more awkward (e.g writing a much larger if block) and keep mypy happy.
I could move it into the if type checking block though to eliminate any run time construction.