1 """This is invoked in a subprocess to call the build backend hooks.
4 - Command line args: hook_name, control_dir
5 - Environment variables:
6 PEP517_BUILD_BACKEND=entry.point:spec
7 PEP517_BACKEND_PATH=paths (separated with os.pathsep)
8 - control_dir/input.json:
12 - control_dir/output.json
23 from importlib import import_module
24 from os.path import join as pjoin
26 # This file is run as a script, and `import wrappers` is not zip-safe, so we
27 # include write_json() and read_json() from wrappers.py.
30 def write_json(obj, path, **kwargs):
31 with open(path, 'w', encoding='utf-8') as f:
32 json.dump(obj, f, **kwargs)
36 with open(path, encoding='utf-8') as f:
40 class BackendUnavailable(Exception):
41 """Raised if we cannot import the backend"""
42 def __init__(self, traceback):
43 self.traceback = traceback
46 class BackendInvalid(Exception):
47 """Raised if the backend is invalid"""
48 def __init__(self, message):
49 self.message = message
52 class HookMissing(Exception):
53 """Raised if a hook is missing and we are not executing the fallback"""
54 def __init__(self, hook_name=None):
55 super().__init__(hook_name)
56 self.hook_name = hook_name
59 def contained_in(filename, directory):
60 """Test if a file is located within the given directory."""
61 filename = os.path.normcase(os.path.abspath(filename))
62 directory = os.path.normcase(os.path.abspath(directory))
63 return os.path.commonprefix([filename, directory]) == directory
67 """Find and load the build backend"""
68 # Add in-tree backend directories to the front of sys.path.
69 backend_path = os.environ.get('PEP517_BACKEND_PATH')
71 extra_pathitems = backend_path.split(os.pathsep)
72 sys.path[:0] = extra_pathitems
74 ep = os.environ['PEP517_BUILD_BACKEND']
75 mod_path, _, obj_path = ep.partition(':')
77 obj = import_module(mod_path)
79 raise BackendUnavailable(traceback.format_exc())
83 contained_in(obj.__file__, path)
84 for path in extra_pathitems
86 raise BackendInvalid("Backend was not loaded from backend-path")
89 for path_part in obj_path.split('.'):
90 obj = getattr(obj, path_part)
94 def _supported_features():
95 """Return the list of options features supported by the backend.
97 Returns a list of strings.
98 The only possible value is 'build_editable'.
100 backend = _build_backend()
102 if hasattr(backend, "build_editable"):
103 features.append("build_editable")
107 def get_requires_for_build_wheel(config_settings):
108 """Invoke the optional get_requires_for_build_wheel hook
110 Returns [] if the hook is not defined.
112 backend = _build_backend()
114 hook = backend.get_requires_for_build_wheel
115 except AttributeError:
118 return hook(config_settings)
121 def get_requires_for_build_editable(config_settings):
122 """Invoke the optional get_requires_for_build_editable hook
124 Returns [] if the hook is not defined.
126 backend = _build_backend()
128 hook = backend.get_requires_for_build_editable
129 except AttributeError:
132 return hook(config_settings)
135 def prepare_metadata_for_build_wheel(
136 metadata_directory, config_settings, _allow_fallback):
137 """Invoke optional prepare_metadata_for_build_wheel
139 Implements a fallback by building a wheel if the hook isn't defined,
140 unless _allow_fallback is False in which case HookMissing is raised.
142 backend = _build_backend()
144 hook = backend.prepare_metadata_for_build_wheel
145 except AttributeError:
146 if not _allow_fallback:
149 return hook(metadata_directory, config_settings)
150 # fallback to build_wheel outside the try block to avoid exception chaining
151 # which can be confusing to users and is not relevant
152 whl_basename = backend.build_wheel(metadata_directory, config_settings)
153 return _get_wheel_metadata_from_wheel(whl_basename, metadata_directory,
157 def prepare_metadata_for_build_editable(
158 metadata_directory, config_settings, _allow_fallback):
159 """Invoke optional prepare_metadata_for_build_editable
161 Implements a fallback by building an editable wheel if the hook isn't
162 defined, unless _allow_fallback is False in which case HookMissing is
165 backend = _build_backend()
167 hook = backend.prepare_metadata_for_build_editable
168 except AttributeError:
169 if not _allow_fallback:
172 build_hook = backend.build_editable
173 except AttributeError:
174 raise HookMissing(hook_name='build_editable')
176 whl_basename = build_hook(metadata_directory, config_settings)
177 return _get_wheel_metadata_from_wheel(whl_basename,
181 return hook(metadata_directory, config_settings)
184 WHEEL_BUILT_MARKER = 'PEP517_ALREADY_BUILT_WHEEL'
187 def _dist_info_files(whl_zip):
188 """Identify the .dist-info folder inside a wheel ZipFile."""
190 for path in whl_zip.namelist():
191 m = re.match(r'[^/\\]+-[^/\\]+\.dist-info/', path)
196 raise Exception("No .dist-info folder found in wheel")
199 def _get_wheel_metadata_from_wheel(
200 whl_basename, metadata_directory, config_settings):
201 """Extract the metadata from a wheel.
203 Fallback for when the build backend does not
204 define the 'get_wheel_metadata' hook.
206 from zipfile import ZipFile
207 with open(os.path.join(metadata_directory, WHEEL_BUILT_MARKER), 'wb'):
208 pass # Touch marker file
210 whl_file = os.path.join(metadata_directory, whl_basename)
211 with ZipFile(whl_file) as zipf:
212 dist_info = _dist_info_files(zipf)
213 zipf.extractall(path=metadata_directory, members=dist_info)
214 return dist_info[0].split('/')[0]
217 def _find_already_built_wheel(metadata_directory):
218 """Check for a wheel already built during the get_wheel_metadata hook.
220 if not metadata_directory:
222 metadata_parent = os.path.dirname(metadata_directory)
223 if not os.path.isfile(pjoin(metadata_parent, WHEEL_BUILT_MARKER)):
226 whl_files = glob(os.path.join(metadata_parent, '*.whl'))
228 print('Found wheel built marker, but no .whl files')
230 if len(whl_files) > 1:
231 print('Found multiple .whl files; unspecified behaviour. '
232 'Will call build_wheel.')
235 # Exactly one .whl file
239 def build_wheel(wheel_directory, config_settings, metadata_directory=None):
240 """Invoke the mandatory build_wheel hook.
242 If a wheel was already built in the
243 prepare_metadata_for_build_wheel fallback, this
244 will copy it rather than rebuilding the wheel.
246 prebuilt_whl = _find_already_built_wheel(metadata_directory)
248 shutil.copy2(prebuilt_whl, wheel_directory)
249 return os.path.basename(prebuilt_whl)
251 return _build_backend().build_wheel(wheel_directory, config_settings,
255 def build_editable(wheel_directory, config_settings, metadata_directory=None):
256 """Invoke the optional build_editable hook.
258 If a wheel was already built in the
259 prepare_metadata_for_build_editable fallback, this
260 will copy it rather than rebuilding the wheel.
262 backend = _build_backend()
264 hook = backend.build_editable
265 except AttributeError:
268 prebuilt_whl = _find_already_built_wheel(metadata_directory)
270 shutil.copy2(prebuilt_whl, wheel_directory)
271 return os.path.basename(prebuilt_whl)
273 return hook(wheel_directory, config_settings, metadata_directory)
276 def get_requires_for_build_sdist(config_settings):
277 """Invoke the optional get_requires_for_build_wheel hook
279 Returns [] if the hook is not defined.
281 backend = _build_backend()
283 hook = backend.get_requires_for_build_sdist
284 except AttributeError:
287 return hook(config_settings)
290 class _DummyException(Exception):
291 """Nothing should ever raise this exception"""
294 class GotUnsupportedOperation(Exception):
295 """For internal use when backend raises UnsupportedOperation"""
296 def __init__(self, traceback):
297 self.traceback = traceback
300 def build_sdist(sdist_directory, config_settings):
301 """Invoke the mandatory build_sdist hook."""
302 backend = _build_backend()
304 return backend.build_sdist(sdist_directory, config_settings)
305 except getattr(backend, 'UnsupportedOperation', _DummyException):
306 raise GotUnsupportedOperation(traceback.format_exc())
310 'get_requires_for_build_wheel',
311 'prepare_metadata_for_build_wheel',
313 'get_requires_for_build_editable',
314 'prepare_metadata_for_build_editable',
316 'get_requires_for_build_sdist',
318 '_supported_features',
323 if len(sys.argv) < 3:
324 sys.exit("Needs args: hook_name, control_dir")
325 hook_name = sys.argv[1]
326 control_dir = sys.argv[2]
327 if hook_name not in HOOK_NAMES:
328 sys.exit("Unknown hook: %s" % hook_name)
329 hook = globals()[hook_name]
331 hook_input = read_json(pjoin(control_dir, 'input.json'))
333 json_out = {'unsupported': False, 'return_val': None}
335 json_out['return_val'] = hook(**hook_input['kwargs'])
336 except BackendUnavailable as e:
337 json_out['no_backend'] = True
338 json_out['traceback'] = e.traceback
339 except BackendInvalid as e:
340 json_out['backend_invalid'] = True
341 json_out['backend_error'] = e.message
342 except GotUnsupportedOperation as e:
343 json_out['unsupported'] = True
344 json_out['traceback'] = e.traceback
345 except HookMissing as e:
346 json_out['hook_missing'] = True
347 json_out['missing_hook_name'] = e.hook_name or hook_name
349 write_json(json_out, pjoin(control_dir, 'output.json'), indent=2)
352 if __name__ == '__main__':