OLD | NEW |
(Empty) | |
| 1 # Copyright 2015-2016, Google Inc. |
| 2 # All rights reserved. |
| 3 # |
| 4 # Redistribution and use in source and binary forms, with or without |
| 5 # modification, are permitted provided that the following conditions are |
| 6 # met: |
| 7 # |
| 8 # * Redistributions of source code must retain the above copyright |
| 9 # notice, this list of conditions and the following disclaimer. |
| 10 # * Redistributions in binary form must reproduce the above |
| 11 # copyright notice, this list of conditions and the following disclaimer |
| 12 # in the documentation and/or other materials provided with the |
| 13 # distribution. |
| 14 # * Neither the name of Google Inc. nor the names of its |
| 15 # contributors may be used to endorse or promote products derived from |
| 16 # this software without specific prior written permission. |
| 17 # |
| 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 |
| 30 """Provides distutils command classes for the GRPC Python setup process.""" |
| 31 |
| 32 import distutils |
| 33 import glob |
| 34 import os |
| 35 import os.path |
| 36 import platform |
| 37 import re |
| 38 import shutil |
| 39 import subprocess |
| 40 import sys |
| 41 import traceback |
| 42 |
| 43 import setuptools |
| 44 from setuptools.command import build_ext |
| 45 from setuptools.command import build_py |
| 46 from setuptools.command import easy_install |
| 47 from setuptools.command import install |
| 48 from setuptools.command import test |
| 49 |
| 50 import support |
| 51 |
| 52 PYTHON_STEM = os.path.dirname(os.path.abspath(__file__)) |
| 53 |
| 54 CONF_PY_ADDENDUM = """ |
| 55 extensions.append('sphinx.ext.napoleon') |
| 56 napoleon_google_docstring = True |
| 57 napoleon_numpy_docstring = True |
| 58 |
| 59 html_theme = 'sphinx_rtd_theme' |
| 60 """ |
| 61 |
| 62 |
| 63 class CommandError(Exception): |
| 64 """Simple exception class for GRPC custom commands.""" |
| 65 |
| 66 |
| 67 # TODO(atash): Remove this once PyPI has better Linux bdist support. See |
| 68 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-sup
ported |
| 69 def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename): |
| 70 """Returns a string path to a bdist file for Linux to install. |
| 71 |
| 72 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a |
| 73 warning and builds from source. |
| 74 """ |
| 75 # TODO(atash): somehow the name that's returned from `wheel` is different |
| 76 # between different versions of 'wheel' (but from a compatibility standpoint, |
| 77 # the names are compatible); we should have some way of determining name |
| 78 # compatibility in the same way `wheel` does to avoid having to rename all of |
| 79 # the custom wheels that we build/upload to GCS. |
| 80 |
| 81 # Break import style to ensure that setup.py has had a chance to install the |
| 82 # relevant package. |
| 83 from six.moves.urllib import request |
| 84 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT |
| 85 try: |
| 86 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path) |
| 87 bdist_data = request.urlopen(url).read() |
| 88 except IOError as error: |
| 89 raise CommandError( |
| 90 '{}\n\nCould not find the bdist {}: {}' |
| 91 .format(traceback.format_exc(), decorated_path, error.message)) |
| 92 # Our chosen local bdist path. |
| 93 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT |
| 94 try: |
| 95 with open(bdist_path, 'w') as bdist_file: |
| 96 bdist_file.write(bdist_data) |
| 97 except IOError as error: |
| 98 raise CommandError( |
| 99 '{}\n\nCould not write grpcio bdist: {}' |
| 100 .format(traceback.format_exc(), error.message)) |
| 101 return bdist_path |
| 102 |
| 103 |
| 104 class SphinxDocumentation(setuptools.Command): |
| 105 """Command to generate documentation via sphinx.""" |
| 106 |
| 107 description = 'generate sphinx documentation' |
| 108 user_options = [] |
| 109 |
| 110 def initialize_options(self): |
| 111 pass |
| 112 |
| 113 def finalize_options(self): |
| 114 pass |
| 115 |
| 116 def run(self): |
| 117 # We import here to ensure that setup.py has had a chance to install the |
| 118 # relevant package eggs first. |
| 119 import sphinx |
| 120 import sphinx.apidoc |
| 121 metadata = self.distribution.metadata |
| 122 src_dir = os.path.join(PYTHON_STEM, 'grpc') |
| 123 sys.path.append(src_dir) |
| 124 sphinx.apidoc.main([ |
| 125 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author, |
| 126 '-V', metadata.version, '-R', metadata.version, |
| 127 '-o', os.path.join('doc', 'src'), src_dir]) |
| 128 conf_filepath = os.path.join('doc', 'src', 'conf.py') |
| 129 with open(conf_filepath, 'a') as conf_file: |
| 130 conf_file.write(CONF_PY_ADDENDUM) |
| 131 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')]) |
| 132 |
| 133 |
| 134 class BuildProtoModules(setuptools.Command): |
| 135 """Command to generate project *_pb2.py modules from proto files.""" |
| 136 |
| 137 description = 'build protobuf modules' |
| 138 user_options = [ |
| 139 ('include=', None, 'path patterns to include in protobuf generation'), |
| 140 ('exclude=', None, 'path patterns to exclude from protobuf generation') |
| 141 ] |
| 142 |
| 143 def initialize_options(self): |
| 144 self.exclude = None |
| 145 self.include = r'.*\.proto$' |
| 146 self.protoc_command = None |
| 147 self.grpc_python_plugin_command = None |
| 148 |
| 149 def finalize_options(self): |
| 150 self.protoc_command = distutils.spawn.find_executable('protoc') |
| 151 self.grpc_python_plugin_command = distutils.spawn.find_executable( |
| 152 'grpc_python_plugin') |
| 153 |
| 154 def run(self): |
| 155 if not self.protoc_command: |
| 156 raise CommandError('could not find protoc') |
| 157 if not self.grpc_python_plugin_command: |
| 158 raise CommandError('could not find grpc_python_plugin ' |
| 159 '(protoc plugin for GRPC Python)') |
| 160 include_regex = re.compile(self.include) |
| 161 exclude_regex = re.compile(self.exclude) if self.exclude else None |
| 162 paths = [] |
| 163 root_directory = PYTHON_STEM |
| 164 for walk_root, directories, filenames in os.walk(root_directory): |
| 165 for filename in filenames: |
| 166 path = os.path.join(walk_root, filename) |
| 167 if include_regex.match(path) and not ( |
| 168 exclude_regex and exclude_regex.match(path)): |
| 169 paths.append(path) |
| 170 command = [ |
| 171 self.protoc_command, |
| 172 '--plugin=protoc-gen-python-grpc={}'.format( |
| 173 self.grpc_python_plugin_command), |
| 174 '-I {}'.format(root_directory), |
| 175 '--python_out={}'.format(root_directory), |
| 176 '--python-grpc_out={}'.format(root_directory), |
| 177 ] + paths |
| 178 try: |
| 179 subprocess.check_output(' '.join(command), cwd=root_directory, shell=True, |
| 180 stderr=subprocess.STDOUT) |
| 181 except subprocess.CalledProcessError as e: |
| 182 raise CommandError('Command:\n{}\nMessage:\n{}\nOutput:\n{}'.format( |
| 183 command, e.message, e.output)) |
| 184 |
| 185 |
| 186 class BuildProjectMetadata(setuptools.Command): |
| 187 """Command to generate project metadata in a module.""" |
| 188 |
| 189 description = 'build grpcio project metadata files' |
| 190 user_options = [] |
| 191 |
| 192 def initialize_options(self): |
| 193 pass |
| 194 |
| 195 def finalize_options(self): |
| 196 pass |
| 197 |
| 198 def run(self): |
| 199 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as mod
ule_file: |
| 200 module_file.write('__version__ = """{}"""'.format( |
| 201 self.distribution.get_version())) |
| 202 |
| 203 |
| 204 class BuildPy(build_py.build_py): |
| 205 """Custom project build command.""" |
| 206 |
| 207 def run(self): |
| 208 try: |
| 209 self.run_command('build_proto_modules') |
| 210 except CommandError as error: |
| 211 sys.stderr.write('warning: %s\n' % error.message) |
| 212 self.run_command('build_project_metadata') |
| 213 build_py.build_py.run(self) |
| 214 |
| 215 |
| 216 class BuildExt(build_ext.build_ext): |
| 217 """Custom build_ext command to enable compiler-specific flags.""" |
| 218 |
| 219 C_OPTIONS = { |
| 220 'unix': ('-pthread', '-std=gnu99'), |
| 221 'msvc': (), |
| 222 } |
| 223 LINK_OPTIONS = {} |
| 224 |
| 225 def build_extensions(self): |
| 226 compiler = self.compiler.compiler_type |
| 227 if compiler in BuildExt.C_OPTIONS: |
| 228 for extension in self.extensions: |
| 229 extension.extra_compile_args += list(BuildExt.C_OPTIONS[compiler]) |
| 230 if compiler in BuildExt.LINK_OPTIONS: |
| 231 for extension in self.extensions: |
| 232 extension.extra_link_args += list(BuildExt.LINK_OPTIONS[compiler]) |
| 233 try: |
| 234 build_ext.build_ext.build_extensions(self) |
| 235 except Exception as error: |
| 236 formatted_exception = traceback.format_exc() |
| 237 support.diagnose_build_ext_error(self, error, formatted_exception) |
| 238 raise CommandError( |
| 239 "Failed `build_ext` step:\n{}".format(formatted_exception)) |
| 240 |
| 241 |
| 242 class Gather(setuptools.Command): |
| 243 """Command to gather project dependencies.""" |
| 244 |
| 245 description = 'gather dependencies for grpcio' |
| 246 user_options = [ |
| 247 ('test', 't', 'flag indicating to gather test dependencies'), |
| 248 ('install', 'i', 'flag indicating to gather install dependencies') |
| 249 ] |
| 250 |
| 251 def initialize_options(self): |
| 252 self.test = False |
| 253 self.install = False |
| 254 |
| 255 def finalize_options(self): |
| 256 # distutils requires this override. |
| 257 pass |
| 258 |
| 259 def run(self): |
| 260 if self.install and self.distribution.install_requires: |
| 261 self.distribution.fetch_build_eggs(self.distribution.install_requires) |
| 262 if self.test and self.distribution.tests_require: |
| 263 self.distribution.fetch_build_eggs(self.distribution.tests_require) |
| 264 |
| 265 |
| 266 class TestLite(setuptools.Command): |
| 267 """Command to run tests without fetching or building anything.""" |
| 268 |
| 269 description = 'run tests without fetching or building anything.' |
| 270 user_options = [] |
| 271 |
| 272 def initialize_options(self): |
| 273 pass |
| 274 |
| 275 def finalize_options(self): |
| 276 # distutils requires this override. |
| 277 pass |
| 278 |
| 279 def run(self): |
| 280 self._add_eggs_to_path() |
| 281 |
| 282 import tests |
| 283 loader = tests.Loader() |
| 284 loader.loadTestsFromNames(['tests']) |
| 285 runner = tests.Runner() |
| 286 result = runner.run(loader.suite) |
| 287 if not result.wasSuccessful(): |
| 288 sys.exit('Test failure') |
| 289 |
| 290 def _add_eggs_to_path(self): |
| 291 """Adds all egg files under .eggs to sys.path""" |
| 292 # TODO(jtattemusch): there has to be a cleaner way to do this |
| 293 import pkg_resources |
| 294 eggs_dir = os.path.join(PYTHON_STEM, '../../../.eggs') |
| 295 eggs = [os.path.join(eggs_dir, filename) |
| 296 for filename in os.listdir(eggs_dir) |
| 297 if filename.endswith('.egg')] |
| 298 for egg in eggs: |
| 299 sys.path.insert(0, pkg_resources.normalize_path(egg)) |
| 300 |
| 301 |
| 302 class RunInterop(test.test): |
| 303 |
| 304 description = 'run interop test client/server' |
| 305 user_options = [ |
| 306 ('args=', 'a', 'pass-thru arguments for the client/server'), |
| 307 ('client', 'c', 'flag indicating to run the client'), |
| 308 ('server', 's', 'flag indicating to run the server') |
| 309 ] |
| 310 |
| 311 def initialize_options(self): |
| 312 self.args = '' |
| 313 self.client = False |
| 314 self.server = False |
| 315 |
| 316 def finalize_options(self): |
| 317 if self.client and self.server: |
| 318 raise DistutilsOptionError('you may only specify one of client or server') |
| 319 |
| 320 def run(self): |
| 321 if self.distribution.install_requires: |
| 322 self.distribution.fetch_build_eggs(self.distribution.install_requires) |
| 323 if self.distribution.tests_require: |
| 324 self.distribution.fetch_build_eggs(self.distribution.tests_require) |
| 325 if self.client: |
| 326 self.run_client() |
| 327 elif self.server: |
| 328 self.run_server() |
| 329 |
| 330 def run_server(self): |
| 331 # We import here to ensure that our setuptools parent has had a chance to |
| 332 # edit the Python system path. |
| 333 from tests.interop import server |
| 334 sys.argv[1:] = self.args.split() |
| 335 server.serve() |
| 336 |
| 337 def run_client(self): |
| 338 # We import here to ensure that our setuptools parent has had a chance to |
| 339 # edit the Python system path. |
| 340 from tests.interop import client |
| 341 sys.argv[1:] = self.args.split() |
| 342 client.test_interoperability() |
OLD | NEW |