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.

150 lines
5.7 KiB

4 years ago
  1. """Build wheels/sdists by installing build deps to a temporary environment.
  2. """
  3. import os
  4. import logging
  5. import pytoml
  6. import shutil
  7. from subprocess import check_call
  8. import sys
  9. from sysconfig import get_paths
  10. from tempfile import mkdtemp
  11. from .wrappers import Pep517HookCaller
  12. log = logging.getLogger(__name__)
  13. def _load_pyproject(source_dir):
  14. with open(os.path.join(source_dir, 'pyproject.toml')) as f:
  15. pyproject_data = pytoml.load(f)
  16. buildsys = pyproject_data['build-system']
  17. return buildsys['requires'], buildsys['build-backend']
  18. class BuildEnvironment(object):
  19. """Context manager to install build deps in a simple temporary environment
  20. Based on code I wrote for pip, which is MIT licensed.
  21. """
  22. # Copyright (c) 2008-2016 The pip developers (see AUTHORS.txt file)
  23. #
  24. # Permission is hereby granted, free of charge, to any person obtaining
  25. # a copy of this software and associated documentation files (the
  26. # "Software"), to deal in the Software without restriction, including
  27. # without limitation the rights to use, copy, modify, merge, publish,
  28. # distribute, sublicense, and/or sell copies of the Software, and to
  29. # permit persons to whom the Software is furnished to do so, subject to
  30. # the following conditions:
  31. #
  32. # The above copyright notice and this permission notice shall be
  33. # included in all copies or substantial portions of the Software.
  34. #
  35. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  36. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  37. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  38. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  39. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  40. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  41. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  42. path = None
  43. def __init__(self, cleanup=True):
  44. self._cleanup = cleanup
  45. def __enter__(self):
  46. self.path = mkdtemp(prefix='pep517-build-env-')
  47. log.info('Temporary build environment: %s', self.path)
  48. self.save_path = os.environ.get('PATH', None)
  49. self.save_pythonpath = os.environ.get('PYTHONPATH', None)
  50. install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
  51. install_dirs = get_paths(install_scheme, vars={
  52. 'base': self.path,
  53. 'platbase': self.path,
  54. })
  55. scripts = install_dirs['scripts']
  56. if self.save_path:
  57. os.environ['PATH'] = scripts + os.pathsep + self.save_path
  58. else:
  59. os.environ['PATH'] = scripts + os.pathsep + os.defpath
  60. if install_dirs['purelib'] == install_dirs['platlib']:
  61. lib_dirs = install_dirs['purelib']
  62. else:
  63. lib_dirs = install_dirs['purelib'] + os.pathsep + \
  64. install_dirs['platlib']
  65. if self.save_pythonpath:
  66. os.environ['PYTHONPATH'] = lib_dirs + os.pathsep + \
  67. self.save_pythonpath
  68. else:
  69. os.environ['PYTHONPATH'] = lib_dirs
  70. return self
  71. def pip_install(self, reqs):
  72. """Install dependencies into this env by calling pip in a subprocess"""
  73. if not reqs:
  74. return
  75. log.info('Calling pip to install %s', reqs)
  76. check_call([sys.executable, '-m', 'pip', 'install', '--ignore-installed',
  77. '--prefix', self.path] + list(reqs))
  78. def __exit__(self, exc_type, exc_val, exc_tb):
  79. if self._cleanup and (self.path is not None) and os.path.isdir(self.path):
  80. shutil.rmtree(self.path)
  81. if self.save_path is None:
  82. os.environ.pop('PATH', None)
  83. else:
  84. os.environ['PATH'] = self.save_path
  85. if self.save_pythonpath is None:
  86. os.environ.pop('PYTHONPATH', None)
  87. else:
  88. os.environ['PYTHONPATH'] = self.save_pythonpath
  89. def build_wheel(source_dir, wheel_dir, config_settings=None):
  90. """Build a wheel from a source directory using PEP 517 hooks.
  91. :param str source_dir: Source directory containing pyproject.toml
  92. :param str wheel_dir: Target directory to create wheel in
  93. :param dict config_settings: Options to pass to build backend
  94. This is a blocking function which will run pip in a subprocess to install
  95. build requirements.
  96. """
  97. if config_settings is None:
  98. config_settings = {}
  99. requires, backend = _load_pyproject(source_dir)
  100. hooks = Pep517HookCaller(source_dir, backend)
  101. with BuildEnvironment() as env:
  102. env.pip_install(requires)
  103. reqs = hooks.get_requires_for_build_wheel(config_settings)
  104. env.pip_install(reqs)
  105. return hooks.build_wheel(wheel_dir, config_settings)
  106. def build_sdist(source_dir, sdist_dir, config_settings=None):
  107. """Build an sdist from a source directory using PEP 517 hooks.
  108. :param str source_dir: Source directory containing pyproject.toml
  109. :param str sdist_dir: Target directory to place sdist in
  110. :param dict config_settings: Options to pass to build backend
  111. This is a blocking function which will run pip in a subprocess to install
  112. build requirements.
  113. """
  114. if config_settings is None:
  115. config_settings = {}
  116. requires, backend = _load_pyproject(source_dir)
  117. hooks = Pep517HookCaller(source_dir, backend)
  118. with BuildEnvironment() as env:
  119. env.pip_install(requires)
  120. reqs = hooks.get_requires_for_build_sdist(config_settings)
  121. env.pip_install(reqs)
  122. return hooks.build_sdist(sdist_dir, config_settings)