33
44from __future__ import absolute_import
55
6+ import json
7+ import os
8+ import subprocess
9+ from textwrap import dedent
10+
11+ from pex .common import REPRODUCIBLE_BUILDS_ENV , CopyMode , safe_mkdtemp
12+ from pex .dist_metadata import Distribution
13+ from pex .interpreter import PythonInterpreter
14+ from pex .jobs import Job , SpawnedJob
15+ from pex .pex import PEX
16+ from pex .pex_bootstrapper import VenvPex , ensure_venv
17+ from pex .pex_builder import PEXBuilder
18+ from pex .result import Error
619from pex .typing import TYPE_CHECKING
20+ from pex .variables import ENV
21+ from pex .venv .bin_path import BinPath
22+ from pex .venv .virtualenv import Virtualenv
723
824if TYPE_CHECKING :
9- from typing import Tuple
25+ from typing import Any , Iterable , Mapping , Optional , Tuple , Union
1026
1127 import attr # vendor:skip
1228else :
@@ -33,3 +49,161 @@ class BuildSystemTable(object):
3349DEFAULT_BUILD_SYSTEM_TABLE = BuildSystemTable (
3450 requires = DEFAULT_BUILD_REQUIRES , build_backend = DEFAULT_BUILD_BACKEND
3551)
52+
53+
54+ # Exit code 75 is EX_TEMPFAIL defined in /usr/include/sysexits.h
55+ # this seems an appropriate signal of DNE vs execute and fail.
56+ _HOOK_UNAVAILABLE_EXIT_CODE = 75
57+
58+
59+ @attr .s (frozen = True )
60+ class BuildSystem (object ):
61+ @classmethod
62+ def create (
63+ cls ,
64+ interpreter , # type: PythonInterpreter
65+ requires , # type: Iterable[str]
66+ resolved , # type: Iterable[Distribution]
67+ build_backend , # type: str
68+ backend_path , # type: Tuple[str, ...]
69+ extra_requirements = None , # type: Optional[Iterable[str]]
70+ use_system_time = False , # type: bool
71+ ** extra_env # type: str
72+ ):
73+ # type: (...) -> Union[BuildSystem, Error]
74+ pex_builder = PEXBuilder (copy_mode = CopyMode .SYMLINK )
75+ pex_builder .info .venv = True
76+ pex_builder .info .venv_site_packages_copies = True
77+ pex_builder .info .venv_bin_path = BinPath .PREPEND
78+ # Allow REPRODUCIBLE_BUILDS_ENV PYTHONHASHSEED env var to take effect.
79+ pex_builder .info .venv_hermetic_scripts = False
80+ for req in requires :
81+ pex_builder .add_requirement (req )
82+ for dist in resolved :
83+ pex_builder .add_distribution (dist )
84+ pex_builder .freeze (bytecode_compile = False )
85+ venv_pex = ensure_venv (PEX (pex_builder .path (), interpreter = interpreter ))
86+ if extra_requirements :
87+ # N.B.: We install extra requirements separately instead of having them resolved and
88+ # handed in with the `resolved` above because there are cases in the wild where the
89+ # build system requires (PEP-518) and the results of PEP-517 `get_requires_for_*` can
90+ # return overlapping requirements. Pip will error for overlaps complaining of duplicate
91+ # requirements if we attempt to resolve all the requirements at once; so we instead
92+ # resolve and install in two phases. This obviously has problems! That said, it is, in
93+ # fact, how Pip's internal PEP-517 build frontend works; so we emulate that.
94+ virtualenv = Virtualenv (venv_pex .venv_dir )
95+ # Python 3.5 comes with Pip 9.0.1 which is pretty broken: it doesn't work with our test
96+ # cases; so we upgrade.
97+ # For Python 2.7 we use virtualenv (there is no -m venv built into Python) and that
98+ # comes with Pip 22.0.2, Python 3.6 comes with Pip 18.1 and Python 3.7 comes with
99+ # Pip 22.04 and the default Pips only get newer with newer version of Pythons. These all
100+ # work well enough for our test cases and, in general, they should work well enough with
101+ # the Python they come paired with.
102+ upgrade_pip = virtualenv .interpreter .version [:2 ] == (3 , 5 )
103+ virtualenv .ensure_pip (upgrade = upgrade_pip )
104+ with open (os .devnull , "wb" ) as dev_null :
105+ _ , process = virtualenv .interpreter .open_process (
106+ args = [
107+ "-m" ,
108+ "pip" ,
109+ "install" ,
110+ "--ignore-installed" ,
111+ "--no-user" ,
112+ "--no-warn-script-location" ,
113+ ]
114+ + list (extra_requirements ),
115+ stdout = dev_null ,
116+ stderr = subprocess .PIPE ,
117+ )
118+ _ , stderr = process .communicate ()
119+ if process .returncode != 0 :
120+ return Error (
121+ "Failed to install extra requirement in venv at {venv_dir}: "
122+ "{extra_requirements}\n STDERR:\n {stderr}" .format (
123+ venv_dir = venv_pex .venv_dir ,
124+ extra_requirements = ", " .join (extra_requirements ),
125+ stderr = stderr .decode ("utf-8" ),
126+ )
127+ )
128+
129+ # Ensure all PEX* env vars are stripped except for PEX_ROOT and PEX_VERBOSE. We want folks
130+ # to be able to steer the location of the cache and the logging verbosity, but nothing else.
131+ # We control the entry-point, etc. of the PEP-518 build backend venv for internal use.
132+ with ENV .strip ().patch (PEX_ROOT = ENV .PEX_ROOT , PEX_VERBOSE = str (ENV .PEX_VERBOSE )) as env :
133+ if extra_env :
134+ env .update (extra_env )
135+ if backend_path :
136+ env .update (PEX_EXTRA_SYS_PATH = os .pathsep .join (backend_path ))
137+ if not use_system_time :
138+ env .update (REPRODUCIBLE_BUILDS_ENV )
139+ return cls (
140+ venv_pex = venv_pex , build_backend = build_backend , requires = tuple (requires ), env = env
141+ )
142+
143+ venv_pex = attr .ib () # type: VenvPex
144+ build_backend = attr .ib () # type: str
145+ requires = attr .ib () # type: Tuple[str, ...]
146+ env = attr .ib () # type: Mapping[str, str]
147+
148+ def invoke_build_hook (
149+ self ,
150+ project_directory , # type: str
151+ hook_method , # type: str
152+ hook_args = (), # type: Iterable[Any]
153+ hook_kwargs = None , # type: Optional[Mapping[str, Any]]
154+ ):
155+ # type: (...) -> Union[SpawnedJob[Any], Error]
156+
157+ # The interfaces are spec'd here: https://peps.python.org/pep-0517
158+ build_backend_module , _ , _ = self .build_backend .partition (":" )
159+ build_backend_object = self .build_backend .replace (":" , "." )
160+ build_hook_result = os .path .join (
161+ safe_mkdtemp (prefix = "pex-pep-517." ), "build_hook_result.json"
162+ )
163+ args = self .venv_pex .execute_args (
164+ additional_args = (
165+ "-c" ,
166+ dedent (
167+ """\
168+ import json
169+ import sys
170+
171+ import {build_backend_module}
172+
173+
174+ if not hasattr({build_backend_object}, {hook_method!r}):
175+ sys.exit({hook_unavailable_exit_code})
176+
177+ result = {build_backend_object}.{hook_method}(*{hook_args!r}, **{hook_kwargs!r})
178+ with open({result_file!r}, "w") as fp:
179+ json.dump(result, fp)
180+ """
181+ ).format (
182+ build_backend_module = build_backend_module ,
183+ build_backend_object = build_backend_object ,
184+ hook_method = hook_method ,
185+ hook_args = tuple (hook_args ),
186+ hook_kwargs = dict (hook_kwargs ) if hook_kwargs else {},
187+ hook_unavailable_exit_code = _HOOK_UNAVAILABLE_EXIT_CODE ,
188+ result_file = build_hook_result ,
189+ ),
190+ )
191+ )
192+ process = subprocess .Popen (
193+ args = args ,
194+ env = self .env ,
195+ cwd = project_directory ,
196+ stdout = subprocess .PIPE ,
197+ stderr = subprocess .PIPE ,
198+ )
199+ return SpawnedJob .file (
200+ Job (
201+ command = args ,
202+ process = process ,
203+ context = "PEP-517:{hook_method} at {project_directory}" .format (
204+ hook_method = hook_method , project_directory = project_directory
205+ ),
206+ ),
207+ output_file = build_hook_result ,
208+ result_func = lambda file_content : json .loads (file_content .decode ("utf-8" )),
209+ )
0 commit comments