commands.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. # Copyright 2015 gRPC authors.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Provides distutils command classes for the GRPC Python setup process."""
  15. import distutils
  16. import glob
  17. import os
  18. import os.path
  19. import platform
  20. import re
  21. import shutil
  22. import subprocess
  23. import sys
  24. import traceback
  25. import setuptools
  26. from setuptools.command import build_ext
  27. from setuptools.command import build_py
  28. from setuptools.command import easy_install
  29. from setuptools.command import install
  30. from setuptools.command import test
  31. import support
  32. PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
  33. GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
  34. PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
  35. PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
  36. CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
  37. class CommandError(Exception):
  38. """Simple exception class for GRPC custom commands."""
  39. # TODO(atash): Remove this once PyPI has better Linux bdist support. See
  40. # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
  41. def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
  42. """Returns a string path to a bdist file for Linux to install.
  43. If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
  44. warning and builds from source.
  45. """
  46. # TODO(atash): somehow the name that's returned from `wheel` is different
  47. # between different versions of 'wheel' (but from a compatibility standpoint,
  48. # the names are compatible); we should have some way of determining name
  49. # compatibility in the same way `wheel` does to avoid having to rename all of
  50. # the custom wheels that we build/upload to GCS.
  51. # Break import style to ensure that setup.py has had a chance to install the
  52. # relevant package.
  53. from six.moves.urllib import request
  54. decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
  55. try:
  56. url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
  57. bdist_data = request.urlopen(url).read()
  58. except IOError as error:
  59. raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
  60. traceback.format_exc(), decorated_path, error.message))
  61. # Our chosen local bdist path.
  62. bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
  63. try:
  64. with open(bdist_path, 'w') as bdist_file:
  65. bdist_file.write(bdist_data)
  66. except IOError as error:
  67. raise CommandError('{}\n\nCould not write grpcio bdist: {}'.format(
  68. traceback.format_exc(), error.message))
  69. return bdist_path
  70. class SphinxDocumentation(setuptools.Command):
  71. """Command to generate documentation via sphinx."""
  72. description = 'generate sphinx documentation'
  73. user_options = []
  74. def initialize_options(self):
  75. pass
  76. def finalize_options(self):
  77. pass
  78. def run(self):
  79. # We import here to ensure that setup.py has had a chance to install the
  80. # relevant package eggs first.
  81. import sphinx.cmd.build
  82. source_dir = os.path.join(GRPC_STEM, 'doc', 'python', 'sphinx')
  83. target_dir = os.path.join(GRPC_STEM, 'doc', 'build')
  84. exit_code = sphinx.cmd.build.build_main(
  85. ['-b', 'html', '-W', '--keep-going', source_dir, target_dir])
  86. if exit_code is not 0:
  87. raise CommandError(
  88. "Documentation generation has warnings or errors")
  89. class BuildProjectMetadata(setuptools.Command):
  90. """Command to generate project metadata in a module."""
  91. description = 'build grpcio project metadata files'
  92. user_options = []
  93. def initialize_options(self):
  94. pass
  95. def finalize_options(self):
  96. pass
  97. def run(self):
  98. with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
  99. 'w') as module_file:
  100. module_file.write('__version__ = """{}"""'.format(
  101. self.distribution.get_version()))
  102. class BuildPy(build_py.build_py):
  103. """Custom project build command."""
  104. def run(self):
  105. self.run_command('build_project_metadata')
  106. build_py.build_py.run(self)
  107. def _poison_extensions(extensions, message):
  108. """Includes a file that will always fail to compile in all extensions."""
  109. poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
  110. with open(poison_filename, 'w') as poison:
  111. poison.write('#error {}'.format(message))
  112. for extension in extensions:
  113. extension.sources = [poison_filename]
  114. def check_and_update_cythonization(extensions):
  115. """Replace .pyx files with their generated counterparts and return whether or
  116. not cythonization still needs to occur."""
  117. for extension in extensions:
  118. generated_pyx_sources = []
  119. other_sources = []
  120. for source in extension.sources:
  121. base, file_ext = os.path.splitext(source)
  122. if file_ext == '.pyx':
  123. generated_pyx_source = next(
  124. (base + gen_ext for gen_ext in (
  125. '.c',
  126. '.cpp',
  127. ) if os.path.isfile(base + gen_ext)), None)
  128. if generated_pyx_source:
  129. generated_pyx_sources.append(generated_pyx_source)
  130. else:
  131. sys.stderr.write('Cython-generated files are missing...\n')
  132. return False
  133. else:
  134. other_sources.append(source)
  135. extension.sources = generated_pyx_sources + other_sources
  136. sys.stderr.write('Found cython-generated files...\n')
  137. return True
  138. def try_cythonize(extensions, linetracing=False, mandatory=True):
  139. """Attempt to cythonize the extensions.
  140. Args:
  141. extensions: A list of `distutils.extension.Extension`.
  142. linetracing: A bool indicating whether or not to enable linetracing.
  143. mandatory: Whether or not having Cython-generated files is mandatory. If it
  144. is, extensions will be poisoned when they can't be fully generated.
  145. """
  146. try:
  147. # Break import style to ensure we have access to Cython post-setup_requires
  148. import Cython.Build
  149. except ImportError:
  150. if mandatory:
  151. sys.stderr.write(
  152. "This package needs to generate C files with Cython but it cannot. "
  153. "Poisoning extension sources to disallow extension commands...")
  154. _poison_extensions(
  155. extensions,
  156. "Extensions have been poisoned due to missing Cython-generated code."
  157. )
  158. return extensions
  159. cython_compiler_directives = {}
  160. if linetracing:
  161. additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
  162. cython_compiler_directives['linetrace'] = True
  163. return Cython.Build.cythonize(
  164. extensions,
  165. include_path=[
  166. include_dir
  167. for extension in extensions
  168. for include_dir in extension.include_dirs
  169. ] + [CYTHON_STEM],
  170. compiler_directives=cython_compiler_directives)
  171. class BuildExt(build_ext.build_ext):
  172. """Custom build_ext command to enable compiler-specific flags."""
  173. C_OPTIONS = {
  174. 'unix': ('-pthread',),
  175. 'msvc': (),
  176. }
  177. LINK_OPTIONS = {}
  178. def build_extensions(self):
  179. # This special conditioning is here due to difference of compiler
  180. # behavior in gcc and clang. The clang doesn't take --stdc++11
  181. # flags but gcc does. Since the setuptools of Python only support
  182. # all C or all C++ compilation, the mix of C and C++ will crash.
  183. # *By default*, the macOS use clang and Linux use gcc, that's why
  184. # the special condition here is checking platform.
  185. if "darwin" in sys.platform:
  186. config = os.environ.get('CONFIG', 'opt')
  187. target_path = os.path.abspath(
  188. os.path.join(
  189. os.path.dirname(os.path.realpath(__file__)), '..', '..',
  190. '..', 'libs', config))
  191. targets = [
  192. os.path.join(target_path, 'libboringssl.a'),
  193. os.path.join(target_path, 'libares.a'),
  194. os.path.join(target_path, 'libgpr.a'),
  195. os.path.join(target_path, 'libgrpc.a')
  196. ]
  197. # Running make separately for Mac means we lose all
  198. # Extension.define_macros configured in setup.py. Re-add the macro
  199. # for gRPC Core's fork handlers.
  200. # TODO(ericgribkoff) Decide what to do about the other missing core
  201. # macros, including GRPC_ENABLE_FORK_SUPPORT, which defaults to 1
  202. # on Linux but remains unset on Mac.
  203. extra_defines = [
  204. 'EXTRA_DEFINES="GRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1"'
  205. ]
  206. # Ensure the BoringSSL are built instead of using system provided
  207. # libraries. It prevents dependency issues while distributing to
  208. # Mac users who use MacPorts to manage their libraries. #17002
  209. mod_env = dict(os.environ)
  210. mod_env['REQUIRE_CUSTOM_LIBRARIES_opt'] = '1'
  211. make_process = subprocess.Popen(
  212. ['make'] + extra_defines + targets,
  213. env=mod_env,
  214. stdout=subprocess.PIPE,
  215. stderr=subprocess.PIPE)
  216. make_out, make_err = make_process.communicate()
  217. if make_out and make_process.returncode != 0:
  218. sys.stdout.write(str(make_out) + '\n')
  219. if make_err:
  220. sys.stderr.write(str(make_err) + '\n')
  221. if make_process.returncode != 0:
  222. raise Exception("make command failed!")
  223. compiler = self.compiler.compiler_type
  224. if compiler in BuildExt.C_OPTIONS:
  225. for extension in self.extensions:
  226. extension.extra_compile_args += list(
  227. BuildExt.C_OPTIONS[compiler])
  228. if compiler in BuildExt.LINK_OPTIONS:
  229. for extension in self.extensions:
  230. extension.extra_link_args += list(
  231. BuildExt.LINK_OPTIONS[compiler])
  232. if not check_and_update_cythonization(self.extensions):
  233. self.extensions = try_cythonize(self.extensions)
  234. try:
  235. build_ext.build_ext.build_extensions(self)
  236. except Exception as error:
  237. formatted_exception = traceback.format_exc()
  238. support.diagnose_build_ext_error(self, error, formatted_exception)
  239. raise CommandError(
  240. "Failed `build_ext` step:\n{}".format(formatted_exception))
  241. class Gather(setuptools.Command):
  242. """Command to gather project dependencies."""
  243. description = 'gather dependencies for grpcio'
  244. user_options = [('test', 't',
  245. 'flag indicating to gather test dependencies'),
  246. ('install', 'i',
  247. 'flag indicating to gather install dependencies')]
  248. def initialize_options(self):
  249. self.test = False
  250. self.install = False
  251. def finalize_options(self):
  252. # distutils requires this override.
  253. pass
  254. def run(self):
  255. if self.install and self.distribution.install_requires:
  256. self.distribution.fetch_build_eggs(
  257. self.distribution.install_requires)
  258. if self.test and self.distribution.tests_require:
  259. self.distribution.fetch_build_eggs(self.distribution.tests_require)