Skip to content

Commit b491f12

Browse files
committed
Implement PEP-517/518 build system locking.
Currently build systems are locked if requested, but the lock data is not yet used at lock use time to set up reproducible sdist builds... Part 1/2. Fixes #2100
1 parent d3af77e commit b491f12

22 files changed

+658
-179
lines changed

pex/build_system/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33

44
from __future__ import absolute_import
55

6+
from pex.typing import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from typing import Tuple
10+
11+
import attr # vendor:skip
12+
else:
13+
from pex.third_party import attr
14+
15+
616
# The split of PEP-517 / PEP-518 is quite awkward. PEP-518 doesn't really work without also
717
# specifying a build backend or knowing a default value for one, but the concept is not defined
818
# until PEP-517. As such, we break this historical? strange division and define the default outside
@@ -11,3 +21,15 @@
1121
# See: https://peps.python.org/pep-0517/#source-trees
1222
DEFAULT_BUILD_BACKEND = "setuptools.build_meta:__legacy__"
1323
DEFAULT_BUILD_REQUIRES = ("setuptools",)
24+
25+
26+
@attr.s(frozen=True)
27+
class BuildSystemTable(object):
28+
requires = attr.ib() # type: Tuple[str, ...]
29+
build_backend = attr.ib(default=DEFAULT_BUILD_BACKEND) # type: str
30+
backend_path = attr.ib(default=()) # type: Tuple[str, ...]
31+
32+
33+
DEFAULT_BUILD_SYSTEM_TABLE = BuildSystemTable(
34+
requires=DEFAULT_BUILD_REQUIRES, build_backend=DEFAULT_BUILD_BACKEND
35+
)

pex/build_system/pep_517.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010

1111
from pex import third_party
1212
from pex.build_system import DEFAULT_BUILD_BACKEND
13-
from pex.build_system.pep_518 import BuildSystem, load_build_system, load_build_system_table
13+
from pex.build_system.pep_518 import BuildSystem, load_build_system
1414
from pex.common import safe_mkdtemp
1515
from pex.dist_metadata import DistMetadata, Distribution, MetadataType
1616
from pex.jobs import Job, SpawnedJob
17-
from pex.orderedset import OrderedSet
1817
from pex.pip.version import PipVersion, PipVersionValue
1918
from pex.resolve.resolvers import Resolver
2019
from pex.result import Error, try_
@@ -257,8 +256,6 @@ def get_requires_for_build_wheel(
257256
):
258257
# type: (...) -> Tuple[str, ...]
259258

260-
build_system_table = try_(load_build_system_table(project_directory))
261-
requires = OrderedSet(build_system_table.requires)
262259
spawned_job = try_(
263260
_invoke_build_hook(
264261
project_directory,
@@ -269,11 +266,11 @@ def get_requires_for_build_wheel(
269266
)
270267
)
271268
try:
272-
requires.update(spawned_job.await_result())
269+
return tuple(spawned_job.await_result())
273270
except Job.Error as e:
274271
if e.exitcode != _HOOK_UNAVAILABLE_EXIT_CODE:
275272
raise e
276-
return tuple(requires)
273+
return ()
277274

278275

279276
def spawn_prepare_metadata(

pex/build_system/pep_518.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import subprocess
88

99
from pex import toml
10-
from pex.build_system import DEFAULT_BUILD_BACKEND, DEFAULT_BUILD_REQUIRES
10+
from pex.build_system import DEFAULT_BUILD_BACKEND, DEFAULT_BUILD_SYSTEM_TABLE, BuildSystemTable
1111
from pex.common import REPRODUCIBLE_BUILDS_ENV, CopyMode
1212
from pex.dist_metadata import Distribution
1313
from pex.interpreter import PythonInterpreter
@@ -31,13 +31,6 @@
3131
from pex.third_party import attr
3232

3333

34-
@attr.s(frozen=True)
35-
class BuildSystemTable(object):
36-
requires = attr.ib() # type: Tuple[str, ...]
37-
build_backend = attr.ib(default=DEFAULT_BUILD_BACKEND) # type: str
38-
backend_path = attr.ib(default=()) # type: Tuple[str, ...]
39-
40-
4134
def _read_build_system_table(
4235
pyproject_toml, # type: str
4336
):
@@ -175,7 +168,7 @@ def load_build_system_table(project_directory):
175168
maybe_build_system_table_or_error = _maybe_load_build_system_table(project_directory)
176169
if maybe_build_system_table_or_error is not None:
177170
return maybe_build_system_table_or_error
178-
return BuildSystemTable(requires=DEFAULT_BUILD_REQUIRES, build_backend=DEFAULT_BUILD_BACKEND)
171+
return DEFAULT_BUILD_SYSTEM_TABLE
179172

180173

181174
def load_build_system(

pex/cli/commands/lock.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,20 @@ def add_create_lock_options(cls, create_parser):
611611
"extras deps that are never activated, but may trim more in the future."
612612
),
613613
)
614+
create_parser.add_argument(
615+
"--lock-build-systems",
616+
"--no-lock-build-systems",
617+
dest="lock_build_systems",
618+
default=False,
619+
action=HandleBoolAction,
620+
type=bool,
621+
help=(
622+
"When creating a lock that includes sdists, VCS requirements or local project "
623+
"directories that will later need to be built into wheels when using the lock, "
624+
"also lock the build system for each of these source tree artifacts to ensure "
625+
"consistent build environments at future times."
626+
),
627+
)
614628
cls._add_lock_options(create_parser)
615629
cls._add_resolve_options(create_parser)
616630
cls.add_json_options(create_parser, entity="lock", include_switch=False)
@@ -905,6 +919,33 @@ def add_extra_arguments(
905919
) as sync_parser:
906920
cls._add_sync_arguments(sync_parser)
907921

922+
def _get_lock_configuration(self, target_configuration):
923+
# type: (TargetConfiguration) -> Union[LockConfiguration, Error]
924+
if self.options.style is LockStyle.UNIVERSAL:
925+
return LockConfiguration(
926+
style=LockStyle.UNIVERSAL,
927+
requires_python=tuple(
928+
str(interpreter_constraint.requires_python)
929+
for interpreter_constraint in target_configuration.interpreter_constraints
930+
),
931+
target_systems=tuple(self.options.target_systems),
932+
elide_unused_requires_dist=self.options.elide_unused_requires_dist,
933+
lock_build_systems=self.options.lock_build_systems,
934+
)
935+
936+
if self.options.target_systems:
937+
return Error(
938+
"The --target-system option only applies to --style {universal} locks.".format(
939+
universal=LockStyle.UNIVERSAL.value
940+
)
941+
)
942+
943+
return LockConfiguration(
944+
style=self.options.style,
945+
elide_unused_requires_dist=self.options.elide_unused_requires_dist,
946+
lock_build_systems=self.options.lock_build_systems,
947+
)
948+
908949
def _resolve_targets(
909950
self,
910951
action, # type: str
@@ -987,6 +1028,7 @@ def _merge_project_requirements(
9871028

9881029
def _locking_configuration(self, pip_configuration):
9891030
# type: (PipConfiguration) -> Union[LockingConfiguration, Error]
1031+
9901032
requirement_configuration = requirement_options.configure(self.options)
9911033
target_configuration = target_options.configure(
9921034
self.options, pip_configuration=pip_configuration
@@ -998,27 +1040,7 @@ def _locking_configuration(self, pip_configuration):
9981040
)
9991041
requirement_configuration = script_metadata_application.requirement_configuration
10001042
target_configuration = script_metadata_application.target_configuration
1001-
if self.options.style == LockStyle.UNIVERSAL:
1002-
lock_configuration = LockConfiguration(
1003-
style=LockStyle.UNIVERSAL,
1004-
requires_python=tuple(
1005-
str(interpreter_constraint.requires_python)
1006-
for interpreter_constraint in target_configuration.interpreter_constraints
1007-
),
1008-
target_systems=tuple(self.options.target_systems),
1009-
elide_unused_requires_dist=self.options.elide_unused_requires_dist,
1010-
)
1011-
elif self.options.target_systems:
1012-
return Error(
1013-
"The --target-system option only applies to --style {universal} locks.".format(
1014-
universal=LockStyle.UNIVERSAL.value
1015-
)
1016-
)
1017-
else:
1018-
lock_configuration = LockConfiguration(
1019-
style=self.options.style,
1020-
elide_unused_requires_dist=self.options.elide_unused_requires_dist,
1021-
)
1043+
lock_configuration = try_(self._get_lock_configuration(target_configuration))
10221044
return LockingConfiguration(
10231045
requirement_configuration,
10241046
target_configuration,
@@ -1840,8 +1862,8 @@ def process_req_edits(
18401862
lock_file=attr.evolve(
18411863
lock_file,
18421864
pex_version=__version__,
1843-
requirements=SortedTuple(requirements_by_project_name.values(), key=str),
1844-
constraints=SortedTuple(constraints_by_project_name.values(), key=str),
1865+
requirements=SortedTuple(requirements_by_project_name.values()),
1866+
constraints=SortedTuple(constraints_by_project_name.values()),
18451867
locked_resolves=SortedTuple(
18461868
resolve_update.updated_resolve for resolve_update in lock_update.resolves
18471869
),

pex/dist_metadata.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,8 @@ def __str__(self):
730730
)
731731

732732

733-
@attr.s(frozen=True)
733+
@functools.total_ordering
734+
@attr.s(frozen=True, order=False)
734735
class Constraint(object):
735736
@classmethod
736737
def parse(
@@ -849,8 +850,14 @@ def as_requirement(self):
849850
# type: () -> Requirement
850851
return Requirement(name=self.name, specifier=self.specifier, marker=self.marker)
851852

853+
def __lt__(self, other):
854+
# type: (Any) -> bool
855+
if not isinstance(other, Constraint):
856+
return NotImplemented
857+
return self._str < other._str
852858

853-
@attr.s(frozen=True)
859+
860+
@attr.s(frozen=True, order=False)
854861
class Requirement(Constraint):
855862
@classmethod
856863
def parse(
@@ -899,6 +906,12 @@ def as_constraint(self):
899906
# type: () -> Constraint
900907
return Constraint(name=self.name, specifier=self.specifier, marker=self.marker)
901908

909+
def __lt__(self, other):
910+
# type: (Any) -> bool
911+
if not isinstance(other, Requirement):
912+
return NotImplemented
913+
return self._str < other._str
914+
902915

903916
# N.B.: DistributionMetadata can have an expensive hash when a distribution has many requirements;
904917
# so we cache the hash. See: https://github.com/pex-tool/pex/issues/1928

pex/pip/vcs.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import re
88

99
from pex import hashing
10+
from pex.build_system import BuildSystemTable
11+
from pex.build_system.pep_518 import load_build_system_table
1012
from pex.common import is_pyc_dir, is_pyc_file, open_zip, temporary_dir
1113
from pex.hashing import Sha256
1214
from pex.pep_440 import Version
@@ -61,24 +63,24 @@ def fingerprint_downloaded_vcs_archive(
6163
version, # type: str
6264
vcs, # type: VCS.Value
6365
):
64-
# type: (...) -> Tuple[Fingerprint, str]
66+
# type: (...) -> Tuple[Fingerprint, BuildSystemTable, str]
6567

6668
archive_path = try_(
6769
_find_built_source_dist(
6870
build_dir=download_dir, project_name=ProjectName(project_name), version=Version(version)
6971
)
7072
)
7173
digest = Sha256()
72-
digest_vcs_archive(archive_path=archive_path, vcs=vcs, digest=digest)
73-
return Fingerprint.from_digest(digest), archive_path
74+
build_system_table = digest_vcs_archive(archive_path=archive_path, vcs=vcs, digest=digest)
75+
return Fingerprint.from_digest(digest), build_system_table, archive_path
7476

7577

7678
def digest_vcs_archive(
7779
archive_path, # type: str
7880
vcs, # type: VCS.Value
7981
digest, # type: HintedDigest
8082
):
81-
# type: (...) -> None
83+
# type: (...) -> BuildSystemTable
8284

8385
# All VCS requirements are prepared as zip archives as encoded in:
8486
# `pip._internal.req.req_install.InstallRequirement.archive`.
@@ -109,3 +111,5 @@ def digest_vcs_archive(
109111
),
110112
file_filter=lambda f: not is_pyc_file(f),
111113
)
114+
115+
return try_(load_build_system_table(chroot))

0 commit comments

Comments
 (0)