_in_process.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """This is invoked in a subprocess to call the build backend hooks.
  2. It expects:
  3. - Command line args: hook_name, control_dir
  4. - Environment variables:
  5. PEP517_BUILD_BACKEND=entry.point:spec
  6. PEP517_BACKEND_PATH=paths (separated with os.pathsep)
  7. - control_dir/input.json:
  8. - {"kwargs": {...}}
  9. Results:
  10. - control_dir/output.json
  11. - {"return_val": ...}
  12. """
  13. from glob import glob
  14. from importlib import import_module
  15. import json
  16. import os
  17. import os.path
  18. from os.path import join as pjoin
  19. import re
  20. import shutil
  21. import sys
  22. import traceback
  23. # This file is run as a script, and `import compat` is not zip-safe, so we
  24. # include write_json() and read_json() from compat.py.
  25. #
  26. # Handle reading and writing JSON in UTF-8, on Python 3 and 2.
  27. if sys.version_info[0] >= 3:
  28. # Python 3
  29. def write_json(obj, path, **kwargs):
  30. with open(path, 'w', encoding='utf-8') as f:
  31. json.dump(obj, f, **kwargs)
  32. def read_json(path):
  33. with open(path, 'r', encoding='utf-8') as f:
  34. return json.load(f)
  35. else:
  36. # Python 2
  37. def write_json(obj, path, **kwargs):
  38. with open(path, 'wb') as f:
  39. json.dump(obj, f, encoding='utf-8', **kwargs)
  40. def read_json(path):
  41. with open(path, 'rb') as f:
  42. return json.load(f)
  43. class BackendUnavailable(Exception):
  44. """Raised if we cannot import the backend"""
  45. def __init__(self, traceback):
  46. self.traceback = traceback
  47. class BackendInvalid(Exception):
  48. """Raised if the backend is invalid"""
  49. def __init__(self, message):
  50. self.message = message
  51. class HookMissing(Exception):
  52. """Raised if a hook is missing and we are not executing the fallback"""
  53. def contained_in(filename, directory):
  54. """Test if a file is located within the given directory."""
  55. filename = os.path.normcase(os.path.abspath(filename))
  56. directory = os.path.normcase(os.path.abspath(directory))
  57. return os.path.commonprefix([filename, directory]) == directory
  58. def _build_backend():
  59. """Find and load the build backend"""
  60. # Add in-tree backend directories to the front of sys.path.
  61. backend_path = os.environ.get('PEP517_BACKEND_PATH')
  62. if backend_path:
  63. extra_pathitems = backend_path.split(os.pathsep)
  64. sys.path[:0] = extra_pathitems
  65. ep = os.environ['PEP517_BUILD_BACKEND']
  66. mod_path, _, obj_path = ep.partition(':')
  67. try:
  68. obj = import_module(mod_path)
  69. except ImportError:
  70. raise BackendUnavailable(traceback.format_exc())
  71. if backend_path:
  72. if not any(
  73. contained_in(obj.__file__, path)
  74. for path in extra_pathitems
  75. ):
  76. raise BackendInvalid("Backend was not loaded from backend-path")
  77. if obj_path:
  78. for path_part in obj_path.split('.'):
  79. obj = getattr(obj, path_part)
  80. return obj
  81. def get_requires_for_build_wheel(config_settings):
  82. """Invoke the optional get_requires_for_build_wheel hook
  83. Returns [] if the hook is not defined.
  84. """
  85. backend = _build_backend()
  86. try:
  87. hook = backend.get_requires_for_build_wheel
  88. except AttributeError:
  89. return []
  90. else:
  91. return hook(config_settings)
  92. def prepare_metadata_for_build_wheel(
  93. metadata_directory, config_settings, _allow_fallback):
  94. """Invoke optional prepare_metadata_for_build_wheel
  95. Implements a fallback by building a wheel if the hook isn't defined,
  96. unless _allow_fallback is False in which case HookMissing is raised.
  97. """
  98. backend = _build_backend()
  99. try:
  100. hook = backend.prepare_metadata_for_build_wheel
  101. except AttributeError:
  102. if not _allow_fallback:
  103. raise HookMissing()
  104. return _get_wheel_metadata_from_wheel(backend, metadata_directory,
  105. config_settings)
  106. else:
  107. return hook(metadata_directory, config_settings)
  108. WHEEL_BUILT_MARKER = 'PEP517_ALREADY_BUILT_WHEEL'
  109. def _dist_info_files(whl_zip):
  110. """Identify the .dist-info folder inside a wheel ZipFile."""
  111. res = []
  112. for path in whl_zip.namelist():
  113. m = re.match(r'[^/\\]+-[^/\\]+\.dist-info/', path)
  114. if m:
  115. res.append(path)
  116. if res:
  117. return res
  118. raise Exception("No .dist-info folder found in wheel")
  119. def _get_wheel_metadata_from_wheel(
  120. backend, metadata_directory, config_settings):
  121. """Build a wheel and extract the metadata from it.
  122. Fallback for when the build backend does not
  123. define the 'get_wheel_metadata' hook.
  124. """
  125. from zipfile import ZipFile
  126. whl_basename = backend.build_wheel(metadata_directory, config_settings)
  127. with open(os.path.join(metadata_directory, WHEEL_BUILT_MARKER), 'wb'):
  128. pass # Touch marker file
  129. whl_file = os.path.join(metadata_directory, whl_basename)
  130. with ZipFile(whl_file) as zipf:
  131. dist_info = _dist_info_files(zipf)
  132. zipf.extractall(path=metadata_directory, members=dist_info)
  133. return dist_info[0].split('/')[0]
  134. def _find_already_built_wheel(metadata_directory):
  135. """Check for a wheel already built during the get_wheel_metadata hook.
  136. """
  137. if not metadata_directory:
  138. return None
  139. metadata_parent = os.path.dirname(metadata_directory)
  140. if not os.path.isfile(pjoin(metadata_parent, WHEEL_BUILT_MARKER)):
  141. return None
  142. whl_files = glob(os.path.join(metadata_parent, '*.whl'))
  143. if not whl_files:
  144. print('Found wheel built marker, but no .whl files')
  145. return None
  146. if len(whl_files) > 1:
  147. print('Found multiple .whl files; unspecified behaviour. '
  148. 'Will call build_wheel.')
  149. return None
  150. # Exactly one .whl file
  151. return whl_files[0]
  152. def build_wheel(wheel_directory, config_settings, metadata_directory=None):
  153. """Invoke the mandatory build_wheel hook.
  154. If a wheel was already built in the
  155. prepare_metadata_for_build_wheel fallback, this
  156. will copy it rather than rebuilding the wheel.
  157. """
  158. prebuilt_whl = _find_already_built_wheel(metadata_directory)
  159. if prebuilt_whl:
  160. shutil.copy2(prebuilt_whl, wheel_directory)
  161. return os.path.basename(prebuilt_whl)
  162. return _build_backend().build_wheel(wheel_directory, config_settings,
  163. metadata_directory)
  164. def get_requires_for_build_sdist(config_settings):
  165. """Invoke the optional get_requires_for_build_wheel hook
  166. Returns [] if the hook is not defined.
  167. """
  168. backend = _build_backend()
  169. try:
  170. hook = backend.get_requires_for_build_sdist
  171. except AttributeError:
  172. return []
  173. else:
  174. return hook(config_settings)
  175. class _DummyException(Exception):
  176. """Nothing should ever raise this exception"""
  177. class GotUnsupportedOperation(Exception):
  178. """For internal use when backend raises UnsupportedOperation"""
  179. def __init__(self, traceback):
  180. self.traceback = traceback
  181. def build_sdist(sdist_directory, config_settings):
  182. """Invoke the mandatory build_sdist hook."""
  183. backend = _build_backend()
  184. try:
  185. return backend.build_sdist(sdist_directory, config_settings)
  186. except getattr(backend, 'UnsupportedOperation', _DummyException):
  187. raise GotUnsupportedOperation(traceback.format_exc())
  188. HOOK_NAMES = {
  189. 'get_requires_for_build_wheel',
  190. 'prepare_metadata_for_build_wheel',
  191. 'build_wheel',
  192. 'get_requires_for_build_sdist',
  193. 'build_sdist',
  194. }
  195. def main():
  196. if len(sys.argv) < 3:
  197. sys.exit("Needs args: hook_name, control_dir")
  198. hook_name = sys.argv[1]
  199. control_dir = sys.argv[2]
  200. if hook_name not in HOOK_NAMES:
  201. sys.exit("Unknown hook: %s" % hook_name)
  202. hook = globals()[hook_name]
  203. hook_input = read_json(pjoin(control_dir, 'input.json'))
  204. json_out = {'unsupported': False, 'return_val': None}
  205. try:
  206. json_out['return_val'] = hook(**hook_input['kwargs'])
  207. except BackendUnavailable as e:
  208. json_out['no_backend'] = True
  209. json_out['traceback'] = e.traceback
  210. except BackendInvalid as e:
  211. json_out['backend_invalid'] = True
  212. json_out['backend_error'] = e.message
  213. except GotUnsupportedOperation as e:
  214. json_out['unsupported'] = True
  215. json_out['traceback'] = e.traceback
  216. except HookMissing:
  217. json_out['hook_missing'] = True
  218. write_json(json_out, pjoin(control_dir, 'output.json'), indent=2)
  219. if __name__ == '__main__':
  220. main()