commands.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. from __future__ import print_function
  16. import distutils
  17. import glob
  18. import os
  19. import os.path
  20. import platform
  21. import re
  22. import shutil
  23. import subprocess
  24. import sys
  25. import traceback
  26. import setuptools
  27. from setuptools.command import build_ext
  28. from setuptools.command import build_py
  29. from setuptools.command import easy_install
  30. from setuptools.command import install
  31. from setuptools.command import test
  32. import support
  33. PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
  34. GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
  35. PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
  36. PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
  37. CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
  38. class CommandError(Exception):
  39. """Simple exception class for GRPC custom commands."""
  40. # TODO(atash): Remove this once PyPI has better Linux bdist support. See
  41. # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
  42. def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
  43. """Returns a string path to a bdist file for Linux to install.
  44. If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
  45. warning and builds from source.
  46. """
  47. # TODO(atash): somehow the name that's returned from `wheel` is different
  48. # between different versions of 'wheel' (but from a compatibility standpoint,
  49. # the names are compatible); we should have some way of determining name
  50. # compatibility in the same way `wheel` does to avoid having to rename all of
  51. # the custom wheels that we build/upload to GCS.
  52. # Break import style to ensure that setup.py has had a chance to install the
  53. # relevant package.
  54. from six.moves.urllib import request
  55. decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
  56. try:
  57. url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
  58. bdist_data = request.urlopen(url).read()
  59. except IOError as error:
  60. raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
  61. traceback.format_exc(), decorated_path, error.message))
  62. # Our chosen local bdist path.
  63. bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
  64. try:
  65. with open(bdist_path, 'w') as bdist_file:
  66. bdist_file.write(bdist_data)
  67. except IOError as error:
  68. raise CommandError('{}\n\nCould not write grpcio bdist: {}'.format(
  69. traceback.format_exc(), error.message))
  70. return bdist_path
  71. class SphinxDocumentation(setuptools.Command):
  72. """Command to generate documentation via sphinx."""
  73. description = 'generate sphinx documentation'
  74. user_options = []
  75. def initialize_options(self):
  76. pass
  77. def finalize_options(self):
  78. pass
  79. def run(self):
  80. # We import here to ensure that setup.py has had a chance to install the
  81. # relevant package eggs first.
  82. import sphinx.cmd.build
  83. source_dir = os.path.join(GRPC_STEM, 'doc', 'python', 'sphinx')
  84. target_dir = os.path.join(GRPC_STEM, 'doc', 'build')
  85. exit_code = sphinx.cmd.build.build_main(
  86. ['-b', 'html', '-W', '--keep-going', source_dir, target_dir])
  87. if exit_code != 0:
  88. raise CommandError(
  89. "Documentation generation has warnings or errors")
  90. class BuildProjectMetadata(setuptools.Command):
  91. """Command to generate project metadata in a module."""
  92. description = 'build grpcio project metadata files'
  93. user_options = []
  94. def initialize_options(self):
  95. pass
  96. def finalize_options(self):
  97. pass
  98. def run(self):
  99. with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
  100. 'w') as module_file:
  101. module_file.write('__version__ = """{}"""'.format(
  102. self.distribution.get_version()))
  103. class BuildPy(build_py.build_py):
  104. """Custom project build command."""
  105. def run(self):
  106. self.run_command('build_project_metadata')
  107. build_py.build_py.run(self)
  108. def _poison_extensions(extensions, message):
  109. """Includes a file that will always fail to compile in all extensions."""
  110. poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
  111. with open(poison_filename, 'w') as poison:
  112. poison.write('#error {}'.format(message))
  113. for extension in extensions:
  114. extension.sources = [poison_filename]
  115. def check_and_update_cythonization(extensions):
  116. """Replace .pyx files with their generated counterparts and return whether or
  117. not cythonization still needs to occur."""
  118. for extension in extensions:
  119. generated_pyx_sources = []
  120. other_sources = []
  121. for source in extension.sources:
  122. base, file_ext = os.path.splitext(source)
  123. if file_ext == '.pyx':
  124. generated_pyx_source = next((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 for extension in extensions
  167. for include_dir in extension.include_dirs
  168. ] + [CYTHON_STEM],
  169. compiler_directives=cython_compiler_directives)
  170. class BuildExt(build_ext.build_ext):
  171. """Custom build_ext command to enable compiler-specific flags."""
  172. C_OPTIONS = {
  173. 'unix': ('-pthread',),
  174. 'msvc': (),
  175. }
  176. LINK_OPTIONS = {}
  177. def build_extensions(self):
  178. def compiler_ok_with_extra_std():
  179. """Test if default compiler is okay with specifying c++ version
  180. when invoked in C mode. GCC is okay with this, while clang is not.
  181. """
  182. try:
  183. # TODO(lidiz) Remove the generated a.out for success tests.
  184. cc_test = subprocess.Popen(['cc', '-x', 'c', '-std=c++11', '-'],
  185. stdin=subprocess.PIPE,
  186. stdout=subprocess.PIPE,
  187. stderr=subprocess.PIPE)
  188. _, cc_err = cc_test.communicate(input=b'int main(){return 0;}')
  189. return not 'invalid argument' in str(cc_err)
  190. except:
  191. sys.stderr.write('Non-fatal exception:' +
  192. traceback.format_exc() + '\n')
  193. return False
  194. # This special conditioning is here due to difference of compiler
  195. # behavior in gcc and clang. The clang doesn't take --stdc++11
  196. # flags but gcc does. Since the setuptools of Python only support
  197. # all C or all C++ compilation, the mix of C and C++ will crash.
  198. # *By default*, macOS and FreBSD use clang and Linux use gcc
  199. #
  200. # If we are not using a permissive compiler that's OK with being
  201. # passed wrong std flags, swap out compile function by adding a filter
  202. # for it.
  203. if not compiler_ok_with_extra_std():
  204. old_compile = self.compiler._compile
  205. def new_compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
  206. if src[-2:] == '.c':
  207. extra_postargs = [
  208. arg for arg in extra_postargs if not '-std=c++' in arg
  209. ]
  210. return old_compile(obj, src, ext, cc_args, extra_postargs,
  211. pp_opts)
  212. self.compiler._compile = new_compile
  213. compiler = self.compiler.compiler_type
  214. if compiler in BuildExt.C_OPTIONS:
  215. for extension in self.extensions:
  216. extension.extra_compile_args += list(
  217. BuildExt.C_OPTIONS[compiler])
  218. if compiler in BuildExt.LINK_OPTIONS:
  219. for extension in self.extensions:
  220. extension.extra_link_args += list(
  221. BuildExt.LINK_OPTIONS[compiler])
  222. if not check_and_update_cythonization(self.extensions):
  223. self.extensions = try_cythonize(self.extensions)
  224. try:
  225. build_ext.build_ext.build_extensions(self)
  226. except Exception as error:
  227. formatted_exception = traceback.format_exc()
  228. support.diagnose_build_ext_error(self, error, formatted_exception)
  229. raise CommandError(
  230. "Failed `build_ext` step:\n{}".format(formatted_exception))
  231. class Gather(setuptools.Command):
  232. """Command to gather project dependencies."""
  233. description = 'gather dependencies for grpcio'
  234. user_options = [
  235. ('test', 't', 'flag indicating to gather test dependencies'),
  236. ('install', 'i', 'flag indicating to gather install dependencies')
  237. ]
  238. def initialize_options(self):
  239. self.test = False
  240. self.install = False
  241. def finalize_options(self):
  242. # distutils requires this override.
  243. pass
  244. def run(self):
  245. if self.install and self.distribution.install_requires:
  246. self.distribution.fetch_build_eggs(
  247. self.distribution.install_requires)
  248. if self.test and self.distribution.tests_require:
  249. self.distribution.fetch_build_eggs(self.distribution.tests_require)
  250. class Clean(setuptools.Command):
  251. """Command to clean build artifacts."""
  252. description = 'Clean build artifacts.'
  253. user_options = [
  254. ('all', 'a', 'a phony flag to allow our script to continue'),
  255. ]
  256. _FILE_PATTERNS = (
  257. 'python_build',
  258. 'src/python/grpcio/__pycache__/',
  259. 'src/python/grpcio/grpc/_cython/cygrpc.cpp',
  260. 'src/python/grpcio/grpc/_cython/*.so',
  261. 'src/python/grpcio/grpcio.egg-info/',
  262. )
  263. _CURRENT_DIRECTORY = os.path.normpath(
  264. os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../.."))
  265. def initialize_options(self):
  266. self.all = False
  267. def finalize_options(self):
  268. pass
  269. def run(self):
  270. for path_spec in self._FILE_PATTERNS:
  271. this_glob = os.path.normpath(
  272. os.path.join(Clean._CURRENT_DIRECTORY, path_spec))
  273. abs_paths = glob.glob(this_glob)
  274. for path in abs_paths:
  275. if not str(path).startswith(Clean._CURRENT_DIRECTORY):
  276. raise ValueError(
  277. "Cowardly refusing to delete {}.".format(path))
  278. print("Removing {}".format(os.path.relpath(path)))
  279. if os.path.isfile(path):
  280. os.remove(str(path))
  281. else:
  282. shutil.rmtree(str(path))