You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

157 lines
5.9 KiB

4 years ago
  1. from contextlib import contextmanager
  2. import os
  3. from os.path import dirname, abspath, join as pjoin
  4. import shutil
  5. from subprocess import check_call
  6. import sys
  7. from tempfile import mkdtemp
  8. from . import compat
  9. _in_proc_script = pjoin(dirname(abspath(__file__)), '_in_process.py')
  10. @contextmanager
  11. def tempdir():
  12. td = mkdtemp()
  13. try:
  14. yield td
  15. finally:
  16. shutil.rmtree(td)
  17. class BackendUnavailable(Exception):
  18. """Will be raised if the backend cannot be imported in the hook process."""
  19. class UnsupportedOperation(Exception):
  20. """May be raised by build_sdist if the backend indicates that it can't."""
  21. def default_subprocess_runner(cmd, cwd=None, extra_environ=None):
  22. """The default method of calling the wrapper subprocess."""
  23. env = os.environ.copy()
  24. if extra_environ:
  25. env.update(extra_environ)
  26. check_call(cmd, cwd=cwd, env=env)
  27. class Pep517HookCaller(object):
  28. """A wrapper around a source directory to be built with a PEP 517 backend.
  29. source_dir : The path to the source directory, containing pyproject.toml.
  30. backend : The build backend spec, as per PEP 517, from pyproject.toml.
  31. """
  32. def __init__(self, source_dir, build_backend):
  33. self.source_dir = abspath(source_dir)
  34. self.build_backend = build_backend
  35. self._subprocess_runner = default_subprocess_runner
  36. # TODO: Is this over-engineered? Maybe frontends only need to
  37. # set this when creating the wrapper, not on every call.
  38. @contextmanager
  39. def subprocess_runner(self, runner):
  40. prev = self._subprocess_runner
  41. self._subprocess_runner = runner
  42. yield
  43. self._subprocess_runner = prev
  44. def get_requires_for_build_wheel(self, config_settings=None):
  45. """Identify packages required for building a wheel
  46. Returns a list of dependency specifications, e.g.:
  47. ["wheel >= 0.25", "setuptools"]
  48. This does not include requirements specified in pyproject.toml.
  49. It returns the result of calling the equivalently named hook in a
  50. subprocess.
  51. """
  52. return self._call_hook('get_requires_for_build_wheel', {
  53. 'config_settings': config_settings
  54. })
  55. def prepare_metadata_for_build_wheel(self, metadata_directory, config_settings=None):
  56. """Prepare a *.dist-info folder with metadata for this project.
  57. Returns the name of the newly created folder.
  58. If the build backend defines a hook with this name, it will be called
  59. in a subprocess. If not, the backend will be asked to build a wheel,
  60. and the dist-info extracted from that.
  61. """
  62. return self._call_hook('prepare_metadata_for_build_wheel', {
  63. 'metadata_directory': abspath(metadata_directory),
  64. 'config_settings': config_settings,
  65. })
  66. def build_wheel(self, wheel_directory, config_settings=None, metadata_directory=None):
  67. """Build a wheel from this project.
  68. Returns the name of the newly created file.
  69. In general, this will call the 'build_wheel' hook in the backend.
  70. However, if that was previously called by
  71. 'prepare_metadata_for_build_wheel', and the same metadata_directory is
  72. used, the previously built wheel will be copied to wheel_directory.
  73. """
  74. if metadata_directory is not None:
  75. metadata_directory = abspath(metadata_directory)
  76. return self._call_hook('build_wheel', {
  77. 'wheel_directory': abspath(wheel_directory),
  78. 'config_settings': config_settings,
  79. 'metadata_directory': metadata_directory,
  80. })
  81. def get_requires_for_build_sdist(self, config_settings=None):
  82. """Identify packages required for building a wheel
  83. Returns a list of dependency specifications, e.g.:
  84. ["setuptools >= 26"]
  85. This does not include requirements specified in pyproject.toml.
  86. It returns the result of calling the equivalently named hook in a
  87. subprocess.
  88. """
  89. return self._call_hook('get_requires_for_build_sdist', {
  90. 'config_settings': config_settings
  91. })
  92. def build_sdist(self, sdist_directory, config_settings=None):
  93. """Build an sdist from this project.
  94. Returns the name of the newly created file.
  95. This calls the 'build_sdist' backend hook in a subprocess.
  96. """
  97. return self._call_hook('build_sdist', {
  98. 'sdist_directory': abspath(sdist_directory),
  99. 'config_settings': config_settings,
  100. })
  101. def _call_hook(self, hook_name, kwargs):
  102. # On Python 2, pytoml returns Unicode values (which is correct) but the
  103. # environment passed to check_call needs to contain string values. We
  104. # convert here by encoding using ASCII (the backend can only contain
  105. # letters, digits and _, . and : characters, and will be used as a
  106. # Python identifier, so non-ASCII content is wrong on Python 2 in
  107. # any case).
  108. if sys.version_info[0] == 2:
  109. build_backend = self.build_backend.encode('ASCII')
  110. else:
  111. build_backend = self.build_backend
  112. with tempdir() as td:
  113. compat.write_json({'kwargs': kwargs}, pjoin(td, 'input.json'),
  114. indent=2)
  115. # Run the hook in a subprocess
  116. self._subprocess_runner(
  117. [sys.executable, _in_proc_script, hook_name, td],
  118. cwd=self.source_dir,
  119. extra_environ={'PEP517_BUILD_BACKEND': build_backend}
  120. )
  121. data = compat.read_json(pjoin(td, 'output.json'))
  122. if data.get('unsupported'):
  123. raise UnsupportedOperation
  124. if data.get('no_backend'):
  125. raise BackendUnavailable
  126. return data['return_val']