Skip to content

Commit a41924e

Browse files
committed
Implement build constraints
1 parent b73fc04 commit a41924e

File tree

7 files changed

+156
-17
lines changed

7 files changed

+156
-17
lines changed

src/pip/_internal/build_env.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
from collections import OrderedDict
1212
from collections.abc import Iterable
1313
from types import TracebackType
14-
from typing import TYPE_CHECKING, Protocol
14+
from typing import TYPE_CHECKING, Protocol, TypedDict
1515

1616
from pip._vendor.packaging.version import Version
1717

1818
from pip import __file__ as pip_location
1919
from pip._internal.cli.spinners import open_spinner
2020
from pip._internal.locations import get_platlib, get_purelib, get_scheme
2121
from pip._internal.metadata import get_default_environment, get_environment
22+
from pip._internal.utils.deprecation import deprecated
2223
from pip._internal.utils.logging import VERBOSE
2324
from pip._internal.utils.packaging import get_requirement
2425
from pip._internal.utils.subprocess import call_subprocess
@@ -31,6 +32,10 @@
3132
logger = logging.getLogger(__name__)
3233

3334

35+
class ExtraEnviron(TypedDict, total=False):
36+
extra_environ: dict[str, str]
37+
38+
3439
def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]:
3540
return (a, b) if a != b else (a,)
3641

@@ -101,8 +106,49 @@ class SubprocessBuildEnvironmentInstaller:
101106
Install build dependencies by calling pip in a subprocess.
102107
"""
103108

104-
def __init__(self, finder: PackageFinder) -> None:
109+
def __init__(
110+
self,
111+
finder: PackageFinder,
112+
build_constraints: list[str] | None = None,
113+
build_constraint_feature_enabled: bool = False,
114+
constraints: list[str] | None = None,
115+
) -> None:
105116
self.finder = finder
117+
self._build_constraints = build_constraints or []
118+
self._build_constraint_feature_enabled = build_constraint_feature_enabled
119+
self._constraints = constraints or []
120+
121+
def _deprecation_constraint_check(self) -> None:
122+
"""
123+
Check for deprecation warning: PIP_CONSTRAINT affecting build environments.
124+
125+
This warns when build-constraint feature is NOT enabled but regular constraints
126+
match what PIP_CONSTRAINT environment variable points to.
127+
"""
128+
if self._build_constraint_feature_enabled:
129+
return
130+
131+
if not self._constraints:
132+
return
133+
134+
if not os.environ.get("PIP_CONSTRAINT"):
135+
return
136+
137+
pip_constraint_files = [
138+
f.strip() for f in os.environ["PIP_CONSTRAINT"].split() if f.strip()
139+
]
140+
if pip_constraint_files and set(pip_constraint_files) == set(self._constraints):
141+
deprecated(
142+
reason=(
143+
"Setting PIP_CONSTRAINT will not affect "
144+
"build constraints in the future,"
145+
),
146+
replacement=(
147+
'PIP_BUILD_CONSTRAINT with PIP_USE_FEATURE="build-constraint"'
148+
),
149+
gone_in="26.2",
150+
issue=None,
151+
)
106152

107153
def install(
108154
self,
@@ -112,6 +158,8 @@ def install(
112158
kind: str,
113159
for_req: InstallRequirement | None,
114160
) -> None:
161+
self._deprecation_constraint_check()
162+
115163
finder = self.finder
116164
args: list[str] = [
117165
sys.executable,
@@ -167,13 +215,32 @@ def install(
167215
args.append("--pre")
168216
if finder.prefer_binary:
169217
args.append("--prefer-binary")
218+
219+
# Handle build constraints
220+
extra_environ: ExtraEnviron = {}
221+
if self._build_constraint_feature_enabled:
222+
# Build constraints must be passed as both constraints
223+
# and build constraints to the subprocess
224+
for constraint_file in self._build_constraints:
225+
args.extend(["--constraint", constraint_file])
226+
args.extend(["--build-constraint", constraint_file])
227+
args.extend(["--use-feature", "build-constraint"])
228+
229+
# If there are no build constraints but the build constraint
230+
# process is enabled then we must ignore regular constraints
231+
if not self._build_constraints:
232+
extra_environ = {
233+
"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}
234+
}
235+
170236
args.append("--")
171237
args.extend(requirements)
172238
with open_spinner(f"Installing {kind}") as spinner:
173239
call_subprocess(
174240
args,
175241
command_desc=f"pip subprocess to install {kind}",
176242
spinner=spinner,
243+
**extra_environ,
177244
)
178245

179246

src/pip/_internal/cli/cmdoptions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None:
101101
)
102102

103103

104+
def check_build_constraints(options: Values) -> None:
105+
"""Function for validating build constraint options.
106+
107+
:param options: The OptionParser options.
108+
"""
109+
if hasattr(options, "build_constraints") and options.build_constraints:
110+
if "build-constraint" not in options.features_enabled:
111+
raise CommandError(
112+
"To use --build-constraint, you must enable this feature with "
113+
"--use-feature=build-constraint."
114+
)
115+
if not options.build_isolation:
116+
raise CommandError(
117+
"--build-constraint cannot be used with --no-build-isolation."
118+
)
119+
120+
104121
def _path_option_check(option: Option, opt: str, value: str) -> str:
105122
return os.path.expanduser(value)
106123

@@ -430,6 +447,22 @@ def constraints() -> Option:
430447
)
431448

432449

450+
def build_constraint() -> Option:
451+
return Option(
452+
"--build-constraint",
453+
dest="build_constraints",
454+
action="append",
455+
type="str",
456+
default=[],
457+
metavar="file",
458+
help=(
459+
"Constrain build dependencies using the given constraints file. "
460+
"This option can be used multiple times. "
461+
"Requires --use-feature=build-constraint."
462+
),
463+
)
464+
465+
433466
def requirements() -> Option:
434467
return Option(
435468
"-r",
@@ -1072,6 +1105,7 @@ def check_list_path_option(options: Values) -> None:
10721105
default=[],
10731106
choices=[
10741107
"fast-deps",
1108+
"build-constraint",
10751109
]
10761110
+ ALWAYS_ENABLED_FEATURES,
10771111
help="Enable new functionality, that may be backward incompatible.",

src/pip/_internal/cli/req_command.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import logging
11+
import os
1112
from functools import partial
1213
from optparse import Values
1314
from typing import Any
@@ -44,6 +45,16 @@
4445
logger = logging.getLogger(__name__)
4546

4647

48+
def should_ignore_regular_constraints(options: Values) -> bool:
49+
"""
50+
Check if regular constraints should be ignored because
51+
we are in a isolated build process and build constraints
52+
feature is enabled but no build constraints were passed.
53+
"""
54+
55+
return os.environ.get("_PIP_IN_BUILD_IGNORE_CONSTRAINTS") == "1"
56+
57+
4758
KEEPABLE_TEMPDIR_TYPES = [
4859
tempdir_kinds.BUILD_ENV,
4960
tempdir_kinds.EPHEM_WHEEL_CACHE,
@@ -132,12 +143,26 @@ def make_requirement_preparer(
132143
"fast-deps has no effect when used with the legacy resolver."
133144
)
134145

146+
# Handle build constraints
147+
build_constraints = getattr(options, "build_constraints", [])
148+
constraints = getattr(options, "constraints", [])
149+
build_constraint_feature_enabled = (
150+
hasattr(options, "features_enabled")
151+
and options.features_enabled
152+
and "build-constraint" in options.features_enabled
153+
)
154+
135155
return RequirementPreparer(
136156
build_dir=temp_build_dir_path,
137157
src_dir=options.src_dir,
138158
download_dir=download_dir,
139159
build_isolation=options.build_isolation,
140-
build_isolation_installer=SubprocessBuildEnvironmentInstaller(finder),
160+
build_isolation_installer=SubprocessBuildEnvironmentInstaller(
161+
finder,
162+
build_constraints=build_constraints,
163+
build_constraint_feature_enabled=build_constraint_feature_enabled,
164+
constraints=constraints,
165+
),
141166
check_build_deps=options.check_build_deps,
142167
build_tracker=build_tracker,
143168
session=session,
@@ -221,20 +246,22 @@ def get_requirements(
221246
Parse command-line arguments into the corresponding requirements.
222247
"""
223248
requirements: list[InstallRequirement] = []
224-
for filename in options.constraints:
225-
for parsed_req in parse_requirements(
226-
filename,
227-
constraint=True,
228-
finder=finder,
229-
options=options,
230-
session=session,
231-
):
232-
req_to_add = install_req_from_parsed_requirement(
233-
parsed_req,
234-
isolated=options.isolated_mode,
235-
user_supplied=False,
236-
)
237-
requirements.append(req_to_add)
249+
250+
if not should_ignore_regular_constraints(options):
251+
for filename in options.constraints:
252+
for parsed_req in parse_requirements(
253+
filename,
254+
constraint=True,
255+
finder=finder,
256+
options=options,
257+
session=session,
258+
):
259+
req_to_add = install_req_from_parsed_requirement(
260+
parsed_req,
261+
isolated=options.isolated_mode,
262+
user_supplied=False,
263+
)
264+
requirements.append(req_to_add)
238265

239266
for req in args:
240267
req_to_add = install_req_from_line(

src/pip/_internal/commands/download.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class DownloadCommand(RequirementCommand):
3636

3737
def add_options(self) -> None:
3838
self.cmd_opts.add_option(cmdoptions.constraints())
39+
self.cmd_opts.add_option(cmdoptions.build_constraint())
3940
self.cmd_opts.add_option(cmdoptions.requirements())
4041
self.cmd_opts.add_option(cmdoptions.no_deps())
4142
self.cmd_opts.add_option(cmdoptions.global_options())
@@ -81,6 +82,7 @@ def run(self, options: Values, args: list[str]) -> int:
8182
options.editables = []
8283

8384
cmdoptions.check_dist_restriction(options)
85+
cmdoptions.check_build_constraints(options)
8486

8587
options.download_dir = normalize_path(options.download_dir)
8688
ensure_dir(options.download_dir)

src/pip/_internal/commands/install.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class InstallCommand(RequirementCommand):
8787
def add_options(self) -> None:
8888
self.cmd_opts.add_option(cmdoptions.requirements())
8989
self.cmd_opts.add_option(cmdoptions.constraints())
90+
self.cmd_opts.add_option(cmdoptions.build_constraint())
9091
self.cmd_opts.add_option(cmdoptions.no_deps())
9192
self.cmd_opts.add_option(cmdoptions.pre())
9293

@@ -303,6 +304,7 @@ def run(self, options: Values, args: list[str]) -> int:
303304
if options.upgrade:
304305
upgrade_strategy = options.upgrade_strategy
305306

307+
cmdoptions.check_build_constraints(options)
306308
cmdoptions.check_dist_restriction(options, check_target=True)
307309

308310
logger.verbose("Using %s", get_pip_version())

src/pip/_internal/commands/lock.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def add_options(self) -> None:
5959
)
6060
self.cmd_opts.add_option(cmdoptions.requirements())
6161
self.cmd_opts.add_option(cmdoptions.constraints())
62+
self.cmd_opts.add_option(cmdoptions.build_constraint())
6263
self.cmd_opts.add_option(cmdoptions.no_deps())
6364
self.cmd_opts.add_option(cmdoptions.pre())
6465

@@ -98,6 +99,9 @@ def run(self, options: Values, args: list[str]) -> int:
9899
"without prior warning."
99100
)
100101

102+
cmdoptions.check_build_constraints(options)
103+
104+
101105
session = self.get_default_session(options)
102106

103107
finder = self._build_package_finder(

src/pip/_internal/commands/wheel.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def add_options(self) -> None:
6060
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
6161
self.cmd_opts.add_option(cmdoptions.check_build_deps())
6262
self.cmd_opts.add_option(cmdoptions.constraints())
63+
self.cmd_opts.add_option(cmdoptions.build_constraint())
6364
self.cmd_opts.add_option(cmdoptions.editable())
6465
self.cmd_opts.add_option(cmdoptions.requirements())
6566
self.cmd_opts.add_option(cmdoptions.src())
@@ -101,6 +102,8 @@ def add_options(self) -> None:
101102

102103
@with_cleanup
103104
def run(self, options: Values, args: list[str]) -> int:
105+
cmdoptions.check_build_constraints(options)
106+
104107
session = self.get_default_session(options)
105108

106109
finder = self._build_package_finder(options, session)

0 commit comments

Comments
 (0)