commands.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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. CONF_PY_ADDENDUM = """
  38. extensions.append('sphinx.ext.napoleon')
  39. napoleon_google_docstring = True
  40. napoleon_numpy_docstring = True
  41. napoleon_include_special_with_doc = True
  42. html_theme = 'sphinx_rtd_theme'
  43. copyright = "2016, The gRPC Authors"
  44. """
  45. API_GLOSSARY = """
  46. Glossary
  47. ================
  48. .. glossary::
  49. metadatum
  50. A key-value pair included in the HTTP header. It is a
  51. 2-tuple where the first entry is the key and the
  52. second is the value, i.e. (key, value). The metadata key is an ASCII str,
  53. and must be a valid HTTP header name. The metadata value can be
  54. either a valid HTTP ASCII str, or bytes. If bytes are provided,
  55. the key must end with '-bin', i.e.
  56. ``('binary-metadata-bin', b'\\x00\\xFF')``
  57. metadata
  58. A sequence of metadatum.
  59. """
  60. class CommandError(Exception):
  61. """Simple exception class for GRPC custom commands."""
  62. # TODO(atash): Remove this once PyPI has better Linux bdist support. See
  63. # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
  64. def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
  65. """Returns a string path to a bdist file for Linux to install.
  66. If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
  67. warning and builds from source.
  68. """
  69. # TODO(atash): somehow the name that's returned from `wheel` is different
  70. # between different versions of 'wheel' (but from a compatibility standpoint,
  71. # the names are compatible); we should have some way of determining name
  72. # compatibility in the same way `wheel` does to avoid having to rename all of
  73. # the custom wheels that we build/upload to GCS.
  74. # Break import style to ensure that setup.py has had a chance to install the
  75. # relevant package.
  76. from six.moves.urllib import request
  77. decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
  78. try:
  79. url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
  80. bdist_data = request.urlopen(url).read()
  81. except IOError as error:
  82. raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
  83. traceback.format_exc(), decorated_path, error.message))
  84. # Our chosen local bdist path.
  85. bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
  86. try:
  87. with open(bdist_path, 'w') as bdist_file:
  88. bdist_file.write(bdist_data)
  89. except IOError as error:
  90. raise CommandError('{}\n\nCould not write grpcio bdist: {}'.format(
  91. traceback.format_exc(), error.message))
  92. return bdist_path
  93. class SphinxDocumentation(setuptools.Command):
  94. """Command to generate documentation via sphinx."""
  95. description = 'generate sphinx documentation'
  96. user_options = []
  97. def initialize_options(self):
  98. pass
  99. def finalize_options(self):
  100. pass
  101. def run(self):
  102. # We import here to ensure that setup.py has had a chance to install the
  103. # relevant package eggs first.
  104. import sphinx
  105. import sphinx.apidoc
  106. metadata = self.distribution.metadata
  107. src_dir = os.path.join(PYTHON_STEM, 'grpc')
  108. sys.path.append(src_dir)
  109. sphinx.apidoc.main([
  110. '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
  111. '-V', metadata.version, '-R', metadata.version, '-o',
  112. os.path.join('doc', 'src'), src_dir
  113. ])
  114. conf_filepath = os.path.join('doc', 'src', 'conf.py')
  115. with open(conf_filepath, 'a') as conf_file:
  116. conf_file.write(CONF_PY_ADDENDUM)
  117. glossary_filepath = os.path.join('doc', 'src', 'grpc.rst')
  118. with open(glossary_filepath, 'a') as glossary_filepath:
  119. glossary_filepath.write(API_GLOSSARY)
  120. sphinx.main(
  121. ['', os.path.join('doc', 'src'),
  122. os.path.join('doc', 'build')])
  123. class BuildProjectMetadata(setuptools.Command):
  124. """Command to generate project metadata in a module."""
  125. description = 'build grpcio project metadata files'
  126. user_options = []
  127. def initialize_options(self):
  128. pass
  129. def finalize_options(self):
  130. pass
  131. def run(self):
  132. with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
  133. 'w') as module_file:
  134. module_file.write('__version__ = """{}"""'.format(
  135. self.distribution.get_version()))
  136. class BuildPy(build_py.build_py):
  137. """Custom project build command."""
  138. def run(self):
  139. self.run_command('build_project_metadata')
  140. build_py.build_py.run(self)
  141. def _poison_extensions(extensions, message):
  142. """Includes a file that will always fail to compile in all extensions."""
  143. poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
  144. with open(poison_filename, 'w') as poison:
  145. poison.write('#error {}'.format(message))
  146. for extension in extensions:
  147. extension.sources = [poison_filename]
  148. def check_and_update_cythonization(extensions):
  149. """Replace .pyx files with their generated counterparts and return whether or
  150. not cythonization still needs to occur."""
  151. for extension in extensions:
  152. generated_pyx_sources = []
  153. other_sources = []
  154. for source in extension.sources:
  155. base, file_ext = os.path.splitext(source)
  156. if file_ext == '.pyx':
  157. generated_pyx_source = next(
  158. (base + gen_ext for gen_ext in (
  159. '.c',
  160. '.cpp',
  161. ) if os.path.isfile(base + gen_ext)), None)
  162. if generated_pyx_source:
  163. generated_pyx_sources.append(generated_pyx_source)
  164. else:
  165. sys.stderr.write('Cython-generated files are missing...\n')
  166. return False
  167. else:
  168. other_sources.append(source)
  169. extension.sources = generated_pyx_sources + other_sources
  170. sys.stderr.write('Found cython-generated files...\n')
  171. return True
  172. def try_cythonize(extensions, linetracing=False, mandatory=True):
  173. """Attempt to cythonize the extensions.
  174. Args:
  175. extensions: A list of `distutils.extension.Extension`.
  176. linetracing: A bool indicating whether or not to enable linetracing.
  177. mandatory: Whether or not having Cython-generated files is mandatory. If it
  178. is, extensions will be poisoned when they can't be fully generated.
  179. """
  180. try:
  181. # Break import style to ensure we have access to Cython post-setup_requires
  182. import Cython.Build
  183. except ImportError:
  184. if mandatory:
  185. sys.stderr.write(
  186. "This package needs to generate C files with Cython but it cannot. "
  187. "Poisoning extension sources to disallow extension commands...")
  188. _poison_extensions(
  189. extensions,
  190. "Extensions have been poisoned due to missing Cython-generated code."
  191. )
  192. return extensions
  193. cython_compiler_directives = {}
  194. if linetracing:
  195. additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
  196. cython_compiler_directives['linetrace'] = True
  197. return Cython.Build.cythonize(
  198. extensions,
  199. include_path=[
  200. include_dir
  201. for extension in extensions
  202. for include_dir in extension.include_dirs
  203. ] + [CYTHON_STEM],
  204. compiler_directives=cython_compiler_directives)
  205. class BuildExt(build_ext.build_ext):
  206. """Custom build_ext command to enable compiler-specific flags."""
  207. C_OPTIONS = {
  208. 'unix': ('-pthread',),
  209. 'msvc': (),
  210. }
  211. LINK_OPTIONS = {}
  212. def build_extensions(self):
  213. if "darwin" in sys.platform:
  214. config = os.environ.get('CONFIG', 'opt')
  215. target_path = os.path.abspath(
  216. os.path.join(
  217. os.path.dirname(os.path.realpath(__file__)), '..', '..',
  218. '..', 'libs', config))
  219. targets = [
  220. os.path.join(target_path, 'libboringssl.a'),
  221. os.path.join(target_path, 'libares.a'),
  222. os.path.join(target_path, 'libgpr.a'),
  223. os.path.join(target_path, 'libgrpc.a')
  224. ]
  225. make_process = subprocess.Popen(
  226. ['make'] + targets,
  227. stdout=subprocess.PIPE,
  228. stderr=subprocess.PIPE)
  229. make_out, make_err = make_process.communicate()
  230. if make_out and make_process.returncode != 0:
  231. sys.stdout.write(str(make_out) + '\n')
  232. if make_err:
  233. sys.stderr.write(str(make_err) + '\n')
  234. if make_process.returncode != 0:
  235. raise Exception("make command failed!")
  236. compiler = self.compiler.compiler_type
  237. if compiler in BuildExt.C_OPTIONS:
  238. for extension in self.extensions:
  239. extension.extra_compile_args += list(
  240. BuildExt.C_OPTIONS[compiler])
  241. if compiler in BuildExt.LINK_OPTIONS:
  242. for extension in self.extensions:
  243. extension.extra_link_args += list(
  244. BuildExt.LINK_OPTIONS[compiler])
  245. if not check_and_update_cythonization(self.extensions):
  246. self.extensions = try_cythonize(self.extensions)
  247. try:
  248. build_ext.build_ext.build_extensions(self)
  249. except Exception as error:
  250. formatted_exception = traceback.format_exc()
  251. support.diagnose_build_ext_error(self, error, formatted_exception)
  252. raise CommandError(
  253. "Failed `build_ext` step:\n{}".format(formatted_exception))
  254. class Gather(setuptools.Command):
  255. """Command to gather project dependencies."""
  256. description = 'gather dependencies for grpcio'
  257. user_options = [('test', 't',
  258. 'flag indicating to gather test dependencies'),
  259. ('install', 'i',
  260. 'flag indicating to gather install dependencies')]
  261. def initialize_options(self):
  262. self.test = False
  263. self.install = False
  264. def finalize_options(self):
  265. # distutils requires this override.
  266. pass
  267. def run(self):
  268. if self.install and self.distribution.install_requires:
  269. self.distribution.fetch_build_eggs(
  270. self.distribution.install_requires)
  271. if self.test and self.distribution.tests_require:
  272. self.distribution.fetch_build_eggs(self.distribution.tests_require)