blob: 58af6bebf56f754fce72ca93e659a02eb5f127fc [file] [log] [blame]
murgatroid993466c4b2016-01-12 10:26:04 -08001# Copyright 2015-2016, Google Inc.
Masood Malekghassemid65632a2015-07-27 14:30:09 -07002# 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
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070032import distutils
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080033import glob
Masood Malekghassemid65632a2015-07-27 14:30:09 -070034import os
35import os.path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080036import platform
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070037import re
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080038import shutil
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070039import subprocess
Masood Malekghassemid65632a2015-07-27 14:30:09 -070040import sys
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080041import traceback
Masood Malekghassemid65632a2015-07-27 14:30:09 -070042
43import setuptools
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -080044from setuptools.command import build_ext
Masood Malekghassemi5c147632015-07-31 14:08:19 -070045from setuptools.command import build_py
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080046from setuptools.command import easy_install
47from setuptools.command import install
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070048from setuptools.command import test
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080049from wheel import bdist_wheel
Masood Malekghassemi1d177812016-01-12 09:21:57 -080050
Masood Malekghassemi5fec8b32016-01-25 16:16:50 -080051import support
52
Masood Malekghassemi116982e2015-12-11 15:53:38 -080053PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
54
Masood Malekghassemif751b0b2016-02-04 11:34:53 -080055BINARIES_REPOSITORY = os.environ.get(
56 'GRPC_PYTHON_BINARIES_REPOSITORY',
Masood Malekghassemi064d37d2016-02-08 12:19:20 -080057 'https://storage.googleapis.com/grpc-precompiled-binaries/python')
Masood Malekghassemif751b0b2016-02-04 11:34:53 -080058
Masood Malekghassemi6598ce12016-02-08 13:31:21 -080059USE_GRPC_CUSTOM_BDIST = bool(int(os.environ.get(
60 'GRPC_PYTHON_USE_CUSTOM_BDIST', '1')))
61
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080062GRPC_CUSTOM_BDIST_EXT = '.whl'
63
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070064CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070065extensions.append('sphinx.ext.napoleon')
66napoleon_google_docstring = True
67napoleon_numpy_docstring = True
68
69html_theme = 'sphinx_rtd_theme'
70"""
71
Masood Malekghassemife8dc882015-07-27 15:30:33 -070072
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080073class CommandError(Exception):
74 """Simple exception class for GRPC custom commands."""
75
76
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080077# TODO(atash): Remove this once PyPI has better Linux bdist support. See
78# https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080079def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
80 """Returns a string path to a bdist file for Linux to install.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080081
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080082 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080083 warning and builds from source.
84 """
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080085 # TODO(atash): somehow the name that's returned from `wheel` is different
86 # between different versions of 'wheel' (but from a compatibility standpoint,
87 # the names are compatible); we should have some way of determining name
88 # compatibility in the same way `wheel` does to avoid having to rename all of
89 # the custom wheels that we build/upload to GCS.
90
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080091 # Break import style to ensure that setup.py has had a chance to install the
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080092 # relevant package.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080093 from six.moves.urllib import request
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080094 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080095 try:
Masood Malekghassemif751b0b2016-02-04 11:34:53 -080096 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080097 bdist_data = request.urlopen(url).read()
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080098 except IOError as error:
99 raise CommandError(
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800100 '{}\n\nCould not find the bdist {}: {}'
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800101 .format(traceback.format_exc(), decorated_path, error.message))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800102 # Our chosen local bdist path.
103 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800104 try:
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800105 with open(bdist_path, 'w') as bdist_file:
106 bdist_file.write(bdist_data)
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800107 except IOError as error:
108 raise CommandError(
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800109 '{}\n\nCould not write grpcio bdist: {}'
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800110 .format(traceback.format_exc(), error.message))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800111 return bdist_path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800112
113
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800114class WheelNameMixin(object):
115 """Mixin for setuptools.Command classes to enable acquiring the bdist name."""
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800116
Masood Malekghassemidd6ee7a2016-02-11 20:08:49 -0800117 def wheel_custom_name(self):
118 base = self.wheel_name()
119 # Drop troublesome parts of the target tuple
120 base_split = base.split('-')
121 base = '-'.join(base_split[0:3] + base_split[4:])
122 flavor = 'ucs2' if sys.maxunicode == 65535 else 'ucs4'
123 return '{base}-{flavor}'.format(base=base, flavor=flavor)
124
125 def wheel_name(self):
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800126 wheel_command = self.get_finalized_command('bdist_wheel')
Masood Malekghassemidd6ee7a2016-02-11 20:08:49 -0800127 return wheel_command.get_archive_basename()
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800128
129
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800130class Install(install.install, WheelNameMixin):
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800131 """Custom Install command for gRPC Python.
132
133 This is for bdist shims and whatever else we might need a custom install
134 command for.
135 """
136
137 user_options = install.install.user_options + [
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800138 # TODO(atash): remove this once PyPI has better Linux bdist support. See
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800139 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800140 ('use-grpc-custom-bdist', None,
141 'Whether to retrieve a binary from the gRPC binary repository instead '
142 'of building from source.'),
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800143 ]
144
145 def initialize_options(self):
146 install.install.initialize_options(self)
Masood Malekghassemi6598ce12016-02-08 13:31:21 -0800147 self.use_grpc_custom_bdist = USE_GRPC_CUSTOM_BDIST
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800148
149 def finalize_options(self):
150 install.install.finalize_options(self)
151
152 def run(self):
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800153 if self.use_grpc_custom_bdist:
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800154 try:
Masood Malekghassemi35afe4a2016-02-08 14:50:18 -0800155 try:
Masood Malekghassemidd6ee7a2016-02-11 20:08:49 -0800156 bdist_path = _get_grpc_custom_bdist(self.wheel_custom_name(),
157 self.wheel_name())
Masood Malekghassemi35afe4a2016-02-08 14:50:18 -0800158 except CommandError as error:
159 sys.stderr.write(
160 '\nWARNING: Failed to acquire grpcio prebuilt binary:\n'
161 '{}.\n\n'.format(error.message))
162 raise
163 try:
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800164 self._run_bdist_retrieval_install(bdist_path)
Masood Malekghassemi35afe4a2016-02-08 14:50:18 -0800165 except Exception as error:
166 # if anything else happens (and given how there's no way to really know
167 # what's happening in setuptools here, I mean *anything*), warn the user
168 # and fall back to building from source.
169 sys.stderr.write(
170 '{}\nWARNING: Failed to install grpcio prebuilt binary.\n\n'
171 .format(traceback.format_exc()))
172 raise
173 except Exception:
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800174 install.install.run(self)
175 else:
176 install.install.run(self)
177
178 # TODO(atash): Remove this once PyPI has better Linux bdist support. See
179 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800180 def _run_bdist_retrieval_install(self, bdist_path):
181 import pip
182 pip.main(['install', bdist_path])
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800183
184
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800185class BdistWheelCustomName(bdist_wheel.bdist_wheel, WheelNameMixin):
186 """Thin wrapper around the bdist command to build with our custom name."""
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800187
Masood Malekghassemib6d3a822016-02-11 13:08:14 -0800188 description = ("Create a gRPC custom-named wheel distribution. "
189 "Cannot be run with any other distribution-related command.")
190
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800191 def run(self):
Masood Malekghassemib6d3a822016-02-11 13:08:14 -0800192 # TODO(atash): if the hack we use to support Linux binaries becomes
193 # 'supported' (i.e.
194 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
195 # is not solved and we see users beginning to use this command, ill-advised
196 # as that may be) consider making the following capable of running with
197 # other distribution-related commands. Currently it depends on the (AFAIK
198 # undocumented, private) ordering of the distribution files.
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800199 bdist_wheel.bdist_wheel.run(self)
200 output = self.distribution.dist_files[-1][2]
Masood Malekghassemidd6ee7a2016-02-11 20:08:49 -0800201 target = os.path.join(
202 self.dist_dir, '{}.whl'.format(self.wheel_custom_name()))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800203 shutil.move(output, target)
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800204
205
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700206class SphinxDocumentation(setuptools.Command):
207 """Command to generate documentation via sphinx."""
208
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700209 description = 'generate sphinx documentation'
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700210 user_options = []
211
212 def initialize_options(self):
213 pass
214
215 def finalize_options(self):
216 pass
217
218 def run(self):
219 # We import here to ensure that setup.py has had a chance to install the
220 # relevant package eggs first.
221 import sphinx
222 import sphinx.apidoc
223 metadata = self.distribution.metadata
224 src_dir = os.path.join(
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800225 PYTHON_STEM, self.distribution.package_dir[''], 'grpc')
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700226 sys.path.append(src_dir)
227 sphinx.apidoc.main([
228 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
229 '-V', metadata.version, '-R', metadata.version,
230 '-o', os.path.join('doc', 'src'), src_dir])
231 conf_filepath = os.path.join('doc', 'src', 'conf.py')
232 with open(conf_filepath, 'a') as conf_file:
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700233 conf_file.write(CONF_PY_ADDENDUM)
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700234 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
235
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700236
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700237class BuildProtoModules(setuptools.Command):
238 """Command to generate project *_pb2.py modules from proto files."""
239
240 description = 'build protobuf modules'
241 user_options = [
242 ('include=', None, 'path patterns to include in protobuf generation'),
243 ('exclude=', None, 'path patterns to exclude from protobuf generation')
244 ]
245
246 def initialize_options(self):
247 self.exclude = None
248 self.include = r'.*\.proto$'
249 self.protoc_command = None
250 self.grpc_python_plugin_command = None
251
252 def finalize_options(self):
253 self.protoc_command = distutils.spawn.find_executable('protoc')
254 self.grpc_python_plugin_command = distutils.spawn.find_executable(
255 'grpc_python_plugin')
256
257 def run(self):
Masood Malekghassemi04672952015-12-21 12:16:50 -0800258 if not self.protoc_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800259 raise CommandError('could not find protoc')
Masood Malekghassemi04672952015-12-21 12:16:50 -0800260 if not self.grpc_python_plugin_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800261 raise CommandError('could not find grpc_python_plugin '
262 '(protoc plugin for GRPC Python)')
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700263 include_regex = re.compile(self.include)
264 exclude_regex = re.compile(self.exclude) if self.exclude else None
265 paths = []
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800266 root_directory = PYTHON_STEM
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700267 for walk_root, directories, filenames in os.walk(root_directory):
268 for filename in filenames:
269 path = os.path.join(walk_root, filename)
270 if include_regex.match(path) and not (
271 exclude_regex and exclude_regex.match(path)):
272 paths.append(path)
273 command = [
274 self.protoc_command,
275 '--plugin=protoc-gen-python-grpc={}'.format(
276 self.grpc_python_plugin_command),
277 '-I {}'.format(root_directory),
278 '--python_out={}'.format(root_directory),
279 '--python-grpc_out={}'.format(root_directory),
280 ] + paths
281 try:
282 subprocess.check_output(' '.join(command), cwd=root_directory, shell=True,
283 stderr=subprocess.STDOUT)
284 except subprocess.CalledProcessError as e:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800285 raise CommandError('Command:\n{}\nMessage:\n{}\nOutput:\n{}'.format(
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700286 command, e.message, e.output))
287
288
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700289class BuildProjectMetadata(setuptools.Command):
290 """Command to generate project metadata in a module."""
291
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700292 description = 'build grpcio project metadata files'
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700293 user_options = []
294
295 def initialize_options(self):
296 pass
297
298 def finalize_options(self):
299 pass
300
301 def run(self):
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800302 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as module_file:
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700303 module_file.write('__version__ = """{}"""'.format(
304 self.distribution.get_version()))
305
306
307class BuildPy(build_py.build_py):
308 """Custom project build command."""
309
310 def run(self):
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800311 try:
312 self.run_command('build_proto_modules')
313 except CommandError as error:
314 sys.stderr.write('warning: %s\n' % error.message)
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700315 self.run_command('build_project_metadata')
316 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700317
318
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800319class BuildExt(build_ext.build_ext):
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800320 """Custom build_ext command to enable compiler-specific flags."""
321
322 C_OPTIONS = {
323 'unix': ('-pthread', '-std=gnu99'),
324 'msvc': (),
325 }
326 LINK_OPTIONS = {}
327
328 def build_extensions(self):
329 compiler = self.compiler.compiler_type
330 if compiler in BuildExt.C_OPTIONS:
331 for extension in self.extensions:
332 extension.extra_compile_args += list(BuildExt.C_OPTIONS[compiler])
333 if compiler in BuildExt.LINK_OPTIONS:
334 for extension in self.extensions:
335 extension.extra_link_args += list(BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800336 try:
337 build_ext.build_ext.build_extensions(self)
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800338 except Exception as error:
Masood Malekghassemi50809092016-01-30 14:26:24 -0800339 formatted_exception = traceback.format_exc()
340 support.diagnose_build_ext_error(self, error, formatted_exception)
341 raise CommandError(
342 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800343
344
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700345class Gather(setuptools.Command):
346 """Command to gather project dependencies."""
347
348 description = 'gather dependencies for grpcio'
349 user_options = [
350 ('test', 't', 'flag indicating to gather test dependencies'),
351 ('install', 'i', 'flag indicating to gather install dependencies')
352 ]
353
354 def initialize_options(self):
355 self.test = False
356 self.install = False
357
358 def finalize_options(self):
359 # distutils requires this override.
360 pass
361
362 def run(self):
363 if self.install and self.distribution.install_requires:
364 self.distribution.fetch_build_eggs(self.distribution.install_requires)
365 if self.test and self.distribution.tests_require:
366 self.distribution.fetch_build_eggs(self.distribution.tests_require)
367
368
369class RunInterop(test.test):
370
371 description = 'run interop test client/server'
372 user_options = [
373 ('args=', 'a', 'pass-thru arguments for the client/server'),
374 ('client', 'c', 'flag indicating to run the client'),
375 ('server', 's', 'flag indicating to run the server')
376 ]
377
378 def initialize_options(self):
379 self.args = ''
380 self.client = False
381 self.server = False
382
383 def finalize_options(self):
384 if self.client and self.server:
385 raise DistutilsOptionError('you may only specify one of client or server')
386
387 def run(self):
388 if self.distribution.install_requires:
389 self.distribution.fetch_build_eggs(self.distribution.install_requires)
390 if self.distribution.tests_require:
391 self.distribution.fetch_build_eggs(self.distribution.tests_require)
392 if self.client:
393 self.run_client()
394 elif self.server:
395 self.run_server()
396
397 def run_server(self):
398 # We import here to ensure that our setuptools parent has had a chance to
399 # edit the Python system path.
400 from tests.interop import server
401 sys.argv[1:] = self.args.split()
402 server.serve()
403
404 def run_client(self):
405 # We import here to ensure that our setuptools parent has had a chance to
406 # edit the Python system path.
407 from tests.interop import client
408 sys.argv[1:] = self.args.split()
409 client.test_interoperability()