Skip to content

Commit 9a492eb

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 17b896c commit 9a492eb

24 files changed

+660
-196
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: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,20 @@ def add_create_lock_options(cls, create_parser):
526526
)
527527
),
528528
)
529+
create_parser.add_argument(
530+
"--lock-build-systems",
531+
"--no-lock-build-systems",
532+
dest="lock_build_systems",
533+
default=False,
534+
action=HandleBoolAction,
535+
type=bool,
536+
help=(
537+
"When creating a lock that includes sdists, VCS requirements or local project "
538+
"directories that will later need to be built into wheels when using the lock, "
539+
"also lock the build system for each of these source tree artifacts to ensure "
540+
"consistent build environments at future times."
541+
),
542+
)
529543
cls._add_lock_options(create_parser)
530544
cls._add_resolve_options(create_parser)
531545
cls.add_json_options(create_parser, entity="lock", include_switch=False)
@@ -802,6 +816,30 @@ def add_extra_arguments(
802816
) as sync_parser:
803817
cls._add_sync_arguments(sync_parser)
804818

819+
def _get_lock_configuration(self, target_configuration):
820+
# type: (TargetConfiguration) -> Union[LockConfiguration, Error]
821+
if self.options.style is LockStyle.UNIVERSAL:
822+
return LockConfiguration(
823+
style=LockStyle.UNIVERSAL,
824+
requires_python=tuple(
825+
str(interpreter_constraint.requires_python)
826+
for interpreter_constraint in target_configuration.interpreter_constraints
827+
),
828+
target_systems=tuple(self.options.target_systems),
829+
lock_build_systems=self.options.lock_build_systems,
830+
)
831+
832+
if self.options.target_systems:
833+
return Error(
834+
"The --target-system option only applies to --style {universal} locks.".format(
835+
universal=LockStyle.UNIVERSAL.value
836+
)
837+
)
838+
839+
return LockConfiguration(
840+
style=self.options.style, lock_build_systems=self.options.lock_build_systems
841+
)
842+
805843
def _resolve_targets(
806844
self,
807845
action, # type: str
@@ -891,24 +929,7 @@ def _create(self):
891929
target_configuration = target_options.configure(
892930
self.options, pip_configuration=pip_configuration
893931
)
894-
if self.options.style == LockStyle.UNIVERSAL:
895-
lock_configuration = LockConfiguration(
896-
style=LockStyle.UNIVERSAL,
897-
requires_python=tuple(
898-
str(interpreter_constraint.requires_python)
899-
for interpreter_constraint in target_configuration.interpreter_constraints
900-
),
901-
target_systems=tuple(self.options.target_systems),
902-
)
903-
elif self.options.target_systems:
904-
return Error(
905-
"The --target-system option only applies to --style {universal} locks.".format(
906-
universal=LockStyle.UNIVERSAL.value
907-
)
908-
)
909-
else:
910-
lock_configuration = LockConfiguration(style=self.options.style)
911-
932+
lock_configuration = try_(self._get_lock_configuration(target_configuration))
912933
targets = try_(
913934
self._resolve_targets(
914935
action="creating",
@@ -1454,8 +1475,8 @@ def process_req_edits(
14541475
lock_file=attr.evolve(
14551476
lock_file,
14561477
pex_version=__version__,
1457-
requirements=SortedTuple(requirements_by_project_name.values(), key=str),
1458-
constraints=SortedTuple(constraints_by_project_name.values(), key=str),
1478+
requirements=SortedTuple(requirements_by_project_name.values()),
1479+
constraints=SortedTuple(constraints_by_project_name.values()),
14591480
locked_resolves=SortedTuple(
14601481
resolve_update.updated_resolve for resolve_update in lock_update.resolves
14611482
),
@@ -1539,24 +1560,7 @@ def _sync(self):
15391560
target_configuration = target_options.configure(
15401561
self.options, pip_configuration=pip_configuration
15411562
)
1542-
if self.options.style == LockStyle.UNIVERSAL:
1543-
lock_configuration = LockConfiguration(
1544-
style=LockStyle.UNIVERSAL,
1545-
requires_python=tuple(
1546-
str(interpreter_constraint.requires_python)
1547-
for interpreter_constraint in target_configuration.interpreter_constraints
1548-
),
1549-
target_systems=tuple(self.options.target_systems),
1550-
)
1551-
elif self.options.target_systems:
1552-
return Error(
1553-
"The --target-system option only applies to --style {universal} locks.".format(
1554-
universal=LockStyle.UNIVERSAL.value
1555-
)
1556-
)
1557-
else:
1558-
lock_configuration = LockConfiguration(style=self.options.style)
1559-
1563+
lock_configuration = try_(self._get_lock_configuration(target_configuration))
15601564
lock_file_path = self.options.lock
15611565
if os.path.exists(lock_file_path):
15621566
build_configuration = pip_configuration.build_configuration

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)