Skip to content

Commit 16d50e9

Browse files
authored
Merge pull request #13534 from notatallshaw/add-build-constraints
Add build constraints
2 parents 5a36b1c + 035396d commit 16d50e9

File tree

12 files changed

+525
-17
lines changed

12 files changed

+525
-17
lines changed

docs/html/user_guide.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,47 @@ e.g. http://example.com/constraints.txt, so that your organization can store and
257257
serve them in a centralized place.
258258

259259

260+
.. _`Build Constraints`:
261+
262+
Build Constraints
263+
-----------------
264+
265+
.. versionadded:: 25.3
266+
267+
Build constraints are a type of constraints file that applies only to isolated
268+
build environments used for building packages from source. Unlike regular
269+
constraints, which affect the packages installed in your environment, build
270+
constraints only influence the versions of packages available during the
271+
build process.
272+
273+
This is useful when you need to constrain build dependencies
274+
(such as ``setuptools``, ``cython``, etc.) without affecting the
275+
final installed environment.
276+
277+
Use build constraints like so:
278+
279+
.. tab:: Unix/macOS
280+
281+
.. code-block:: shell
282+
283+
python -m pip install --build-constraint build-constraints.txt SomePackage
284+
285+
.. tab:: Windows
286+
287+
.. code-block:: shell
288+
289+
py -m pip install --build-constraint build-constraints.txt SomePackage
290+
291+
Example build constraints file (``build-constraints.txt``):
292+
293+
.. code-block:: text
294+
295+
# Constrain setuptools version during build
296+
setuptools>=45,<80
297+
# Pin Cython for packages that use it to build
298+
cython==0.29.24
299+
300+
260301
.. _`Dependency Groups`:
261302

262303

news/13534.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support for build constraints via the ``--build-constraint`` option. This
2+
allows constraining the versions of packages used during the build process
3+
(e.g., setuptools) without affecting the final installation.

news/13534.removal.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Deprecate the ``PIP_CONSTRAINT`` environment variable for specifying build
2+
constraints.
3+
4+
Build constraints should now be specified using the ``--build-constraint``
5+
option or the ``PIP_BUILD_CONSTRAINT`` environment variable. When using build
6+
constraints, ``PIP_CONSTRAINT`` no longer affects isolated build environments.
7+
To opt in to this behavior without specifying any build constraints, use
8+
``--use-feature=build-constraint``.

src/pip/_internal/build_env.py

Lines changed: 66 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
@@ -28,6 +29,10 @@
2829
from pip._internal.index.package_finder import PackageFinder
2930
from pip._internal.req.req_install import InstallRequirement
3031

32+
class ExtraEnviron(TypedDict, total=False):
33+
extra_environ: dict[str, str]
34+
35+
3136
logger = logging.getLogger(__name__)
3237

3338

@@ -101,8 +106,44 @@ 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+
) -> None:
105115
self.finder = finder
116+
self._build_constraints = build_constraints or []
117+
self._build_constraint_feature_enabled = build_constraint_feature_enabled
118+
119+
def _deprecation_constraint_check(self) -> None:
120+
"""
121+
Check for deprecation warning: PIP_CONSTRAINT affecting build environments.
122+
123+
This warns when build-constraint feature is NOT enabled and PIP_CONSTRAINT
124+
is not empty.
125+
"""
126+
if self._build_constraint_feature_enabled or self._build_constraints:
127+
return
128+
129+
pip_constraint = os.environ.get("PIP_CONSTRAINT")
130+
if not pip_constraint or not pip_constraint.strip():
131+
return
132+
133+
deprecated(
134+
reason=(
135+
"Setting PIP_CONSTRAINT will not affect "
136+
"build constraints in the future,"
137+
),
138+
replacement=(
139+
"to specify build constraints using --build-constraint or "
140+
"PIP_BUILD_CONSTRAINT. To disable this warning without "
141+
"any build constraints set --use-feature=build-constraint or "
142+
'PIP_USE_FEATURE="build-constraint"'
143+
),
144+
gone_in="26.2",
145+
issue=None,
146+
)
106147

107148
def install(
108149
self,
@@ -112,6 +153,8 @@ def install(
112153
kind: str,
113154
for_req: InstallRequirement | None,
114155
) -> None:
156+
self._deprecation_constraint_check()
157+
115158
finder = self.finder
116159
args: list[str] = [
117160
sys.executable,
@@ -167,6 +210,26 @@ def install(
167210
args.append("--pre")
168211
if finder.prefer_binary:
169212
args.append("--prefer-binary")
213+
214+
# Handle build constraints
215+
if self._build_constraint_feature_enabled:
216+
args.extend(["--use-feature", "build-constraint"])
217+
218+
if self._build_constraints:
219+
# Build constraints must be passed as both constraints
220+
# and build constraints, so that nested builds receive
221+
# build constraints
222+
for constraint_file in self._build_constraints:
223+
args.extend(["--constraint", constraint_file])
224+
args.extend(["--build-constraint", constraint_file])
225+
226+
extra_environ: ExtraEnviron = {}
227+
if self._build_constraint_feature_enabled and not self._build_constraints:
228+
# If there are no build constraints but the build constraints
229+
# feature is enabled then we must ignore regular constraints
230+
# in the isolated build environment
231+
extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}}
232+
170233
args.append("--")
171234
args.extend(requirements)
172235

@@ -178,6 +241,7 @@ def install(
178241
args,
179242
command_desc=f"installing {kind}{identify_requirement}",
180243
spinner=spinner,
244+
**extra_environ,
181245
)
182246

183247

src/pip/_internal/cli/cmdoptions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ 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 constraints options.
106+
107+
:param options: The OptionParser options.
108+
"""
109+
if hasattr(options, "build_constraints") and options.build_constraints:
110+
if not options.build_isolation:
111+
raise CommandError(
112+
"--build-constraint cannot be used with --no-build-isolation."
113+
)
114+
115+
# Import here to avoid circular imports
116+
from pip._internal.network.session import PipSession
117+
from pip._internal.req.req_file import get_file_content
118+
119+
# Eagerly check build constraints file contents
120+
# is valid so that we don't fail in when trying
121+
# to check constraints in isolated build process
122+
with PipSession() as session:
123+
for constraint_file in options.build_constraints:
124+
get_file_content(constraint_file, session)
125+
126+
104127
def _path_option_check(option: Option, opt: str, value: str) -> str:
105128
return os.path.expanduser(value)
106129

@@ -430,6 +453,21 @@ def constraints() -> Option:
430453
)
431454

432455

456+
def build_constraints() -> Option:
457+
return Option(
458+
"--build-constraint",
459+
dest="build_constraints",
460+
action="append",
461+
type="str",
462+
default=[],
463+
metavar="file",
464+
help=(
465+
"Constrain build dependencies using the given constraints file. "
466+
"This option can be used multiple times."
467+
),
468+
)
469+
470+
433471
def requirements() -> Option:
434472
return Option(
435473
"-r",
@@ -1072,6 +1110,7 @@ def check_list_path_option(options: Values) -> None:
10721110
default=[],
10731111
choices=[
10741112
"fast-deps",
1113+
"build-constraint",
10751114
]
10761115
+ ALWAYS_ENABLED_FEATURES,
10771116
help="Enable new functionality, that may be backward incompatible.",

src/pip/_internal/cli/req_command.py

Lines changed: 38 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,22 @@ 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+
build_constraint_feature_enabled = (
149+
"build-constraint" in options.features_enabled
150+
)
151+
135152
return RequirementPreparer(
136153
build_dir=temp_build_dir_path,
137154
src_dir=options.src_dir,
138155
download_dir=download_dir,
139156
build_isolation=options.build_isolation,
140-
build_isolation_installer=SubprocessBuildEnvironmentInstaller(finder),
157+
build_isolation_installer=SubprocessBuildEnvironmentInstaller(
158+
finder,
159+
build_constraints=build_constraints,
160+
build_constraint_feature_enabled=build_constraint_feature_enabled,
161+
),
141162
check_build_deps=options.check_build_deps,
142163
build_tracker=build_tracker,
143164
session=session,
@@ -221,20 +242,22 @@ def get_requirements(
221242
Parse command-line arguments into the corresponding requirements.
222243
"""
223244
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)
245+
246+
if not should_ignore_regular_constraints(options):
247+
for filename in options.constraints:
248+
for parsed_req in parse_requirements(
249+
filename,
250+
constraint=True,
251+
finder=finder,
252+
options=options,
253+
session=session,
254+
):
255+
req_to_add = install_req_from_parsed_requirement(
256+
parsed_req,
257+
isolated=options.isolated_mode,
258+
user_supplied=False,
259+
)
260+
requirements.append(req_to_add)
238261

239262
for req in args:
240263
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_constraints())
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_constraints())
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: 3 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_constraints())
6263
self.cmd_opts.add_option(cmdoptions.no_deps())
6364
self.cmd_opts.add_option(cmdoptions.pre())
6465

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

102+
cmdoptions.check_build_constraints(options)
103+
101104
session = self.get_default_session(options)
102105

103106
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_constraints())
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)