blob: 5de631922ad8999aee49eb3a60ec704862d8c328 [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 Malekghassemi154b0ee2016-01-25 16:45:29 -080044from setuptools.command import bdist_egg
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -080045from setuptools.command import build_ext
Masood Malekghassemi5c147632015-07-31 14:08:19 -070046from setuptools.command import build_py
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080047from setuptools.command import easy_install
48from setuptools.command import install
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070049from setuptools.command import test
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 Malekghassemi7566c9a2015-10-21 20:29:23 -070059CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070060extensions.append('sphinx.ext.napoleon')
61napoleon_google_docstring = True
62napoleon_numpy_docstring = True
63
64html_theme = 'sphinx_rtd_theme'
65"""
66
Masood Malekghassemife8dc882015-07-27 15:30:33 -070067
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080068class CommandError(Exception):
69 """Simple exception class for GRPC custom commands."""
70
71
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080072# TODO(atash): Remove this once PyPI has better Linux bdist support. See
73# https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemiece40b22016-01-27 15:45:35 -080074def _get_grpc_custom_bdist_egg(decorated_basename, target_egg_basename):
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080075 """Returns a string path to a .egg file for Linux to install.
76
77 If we can retrieve a pre-compiled egg from online, uses it. Else, emits a
78 warning and builds from source.
79 """
80 # Break import style to ensure that setup.py has had a chance to install the
81 # relevant package eggs.
82 from six.moves.urllib import request
83 decorated_path = decorated_basename + '.egg'
84 try:
Masood Malekghassemif751b0b2016-02-04 11:34:53 -080085 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080086 egg_data = request.urlopen(url).read()
87 except IOError as error:
88 raise CommandError(
89 '{}\n\nCould not find the bdist egg {}: {}'
90 .format(traceback.format_exc(), decorated_path, error.message))
91 # Our chosen local egg path.
92 egg_path = target_egg_basename + '.egg'
93 try:
94 with open(egg_path, 'w') as egg_file:
95 egg_file.write(egg_data)
96 except IOError as error:
97 raise CommandError(
98 '{}\n\nCould not write grpcio egg: {}'
99 .format(traceback.format_exc(), error.message))
100 return egg_path
101
102
103class EggNameMixin(object):
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800104 """Mixin for setuptools.Command classes to enable acquiring the egg name."""
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800105
106 def egg_name(self, with_custom):
107 """
108 Args:
109 with_custom: Boolean describing whether or not to decorate the egg name
110 with custom gRPC-specific target information.
111 """
112 egg_command = self.get_finalized_command('bdist_egg')
113 base = os.path.splitext(os.path.basename(egg_command.egg_output))[0]
114 if with_custom:
115 flavor = 'ucs2' if sys.maxunicode == 65535 else 'ucs4'
116 return '{base}-{flavor}'.format(base=base, flavor=flavor)
117 else:
118 return base
119
120
121class Install(install.install, EggNameMixin):
122 """Custom Install command for gRPC Python.
123
124 This is for bdist shims and whatever else we might need a custom install
125 command for.
126 """
127
128 user_options = install.install.user_options + [
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800129 # TODO(atash): remove this once PyPI has better Linux bdist support. See
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800130 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800131 ('use-grpc-custom-bdist', None,
132 'Whether to retrieve a binary from the gRPC binary repository instead '
133 'of building from source.'),
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800134 ]
135
136 def initialize_options(self):
137 install.install.initialize_options(self)
Craig Tiller5c047602016-02-05 14:25:56 -0800138 self.use_grpc_custom_bdist = bool(int(os.environ.get('GRPC_PYTHON_USE_CUSTOM_BDIST', '1')))
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800139
140 def finalize_options(self):
141 install.install.finalize_options(self)
142
143 def run(self):
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800144 if self.use_grpc_custom_bdist:
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800145 try:
Masood Malekghassemiece40b22016-01-27 15:45:35 -0800146 egg_path = _get_grpc_custom_bdist_egg(self.egg_name(True),
147 self.egg_name(False))
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800148 except CommandError as error:
149 sys.stderr.write(
150 '\nWARNING: Failed to acquire grpcio prebuilt binary:\n'
151 '{}.\n\n'.format(error.message))
152 raise
153 try:
154 self._run_bdist_retrieval_install(egg_path)
155 except Exception as error:
156 # if anything else happens (and given how there's no way to really know
157 # what's happening in setuptools here, I mean *anything*), warn the user
158 # and fall back to building from source.
159 sys.stderr.write(
160 '{}\nWARNING: Failed to install grpcio prebuilt binary.\n\n'
161 .format(traceback.format_exc()))
162 install.install.run(self)
163 else:
164 install.install.run(self)
165
166 # TODO(atash): Remove this once PyPI has better Linux bdist support. See
167 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
168 def _run_bdist_retrieval_install(self, bdist_egg):
169 easy_install = self.distribution.get_command_class('easy_install')
170 easy_install_command = easy_install(
171 self.distribution, args='x', root=self.root, record=self.record,
172 )
173 easy_install_command.ensure_finalized()
174 easy_install_command.always_copy_from = '.'
175 easy_install_command.package_index.scan(glob.glob('*.egg'))
176 arguments = [bdist_egg]
177 if setuptools.bootstrap_install_from:
178 args.insert(0, setuptools.bootstrap_install_from)
179 easy_install_command.args = arguments
180 easy_install_command.run()
181 setuptools.bootstrap_install_from = None
182
183
184class BdistEggCustomName(bdist_egg.bdist_egg, EggNameMixin):
185 """Thin wrapper around the bdist_egg command to build with our custom name."""
186
187 def run(self):
188 bdist_egg.bdist_egg.run(self)
189 target = os.path.join(self.dist_dir, '{}.egg'.format(self.egg_name(True)))
190 shutil.move(self.get_outputs()[0], target)
191
192
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700193class SphinxDocumentation(setuptools.Command):
194 """Command to generate documentation via sphinx."""
195
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700196 description = 'generate sphinx documentation'
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700197 user_options = []
198
199 def initialize_options(self):
200 pass
201
202 def finalize_options(self):
203 pass
204
205 def run(self):
206 # We import here to ensure that setup.py has had a chance to install the
207 # relevant package eggs first.
208 import sphinx
209 import sphinx.apidoc
210 metadata = self.distribution.metadata
211 src_dir = os.path.join(
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800212 PYTHON_STEM, self.distribution.package_dir[''], 'grpc')
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700213 sys.path.append(src_dir)
214 sphinx.apidoc.main([
215 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
216 '-V', metadata.version, '-R', metadata.version,
217 '-o', os.path.join('doc', 'src'), src_dir])
218 conf_filepath = os.path.join('doc', 'src', 'conf.py')
219 with open(conf_filepath, 'a') as conf_file:
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700220 conf_file.write(CONF_PY_ADDENDUM)
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700221 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
222
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700223
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700224class BuildProtoModules(setuptools.Command):
225 """Command to generate project *_pb2.py modules from proto files."""
226
227 description = 'build protobuf modules'
228 user_options = [
229 ('include=', None, 'path patterns to include in protobuf generation'),
230 ('exclude=', None, 'path patterns to exclude from protobuf generation')
231 ]
232
233 def initialize_options(self):
234 self.exclude = None
235 self.include = r'.*\.proto$'
236 self.protoc_command = None
237 self.grpc_python_plugin_command = None
238
239 def finalize_options(self):
240 self.protoc_command = distutils.spawn.find_executable('protoc')
241 self.grpc_python_plugin_command = distutils.spawn.find_executable(
242 'grpc_python_plugin')
243
244 def run(self):
Masood Malekghassemi04672952015-12-21 12:16:50 -0800245 if not self.protoc_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800246 raise CommandError('could not find protoc')
Masood Malekghassemi04672952015-12-21 12:16:50 -0800247 if not self.grpc_python_plugin_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800248 raise CommandError('could not find grpc_python_plugin '
249 '(protoc plugin for GRPC Python)')
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700250 include_regex = re.compile(self.include)
251 exclude_regex = re.compile(self.exclude) if self.exclude else None
252 paths = []
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800253 root_directory = PYTHON_STEM
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700254 for walk_root, directories, filenames in os.walk(root_directory):
255 for filename in filenames:
256 path = os.path.join(walk_root, filename)
257 if include_regex.match(path) and not (
258 exclude_regex and exclude_regex.match(path)):
259 paths.append(path)
260 command = [
261 self.protoc_command,
262 '--plugin=protoc-gen-python-grpc={}'.format(
263 self.grpc_python_plugin_command),
264 '-I {}'.format(root_directory),
265 '--python_out={}'.format(root_directory),
266 '--python-grpc_out={}'.format(root_directory),
267 ] + paths
268 try:
269 subprocess.check_output(' '.join(command), cwd=root_directory, shell=True,
270 stderr=subprocess.STDOUT)
271 except subprocess.CalledProcessError as e:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800272 raise CommandError('Command:\n{}\nMessage:\n{}\nOutput:\n{}'.format(
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700273 command, e.message, e.output))
274
275
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700276class BuildProjectMetadata(setuptools.Command):
277 """Command to generate project metadata in a module."""
278
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700279 description = 'build grpcio project metadata files'
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700280 user_options = []
281
282 def initialize_options(self):
283 pass
284
285 def finalize_options(self):
286 pass
287
288 def run(self):
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800289 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as module_file:
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700290 module_file.write('__version__ = """{}"""'.format(
291 self.distribution.get_version()))
292
293
294class BuildPy(build_py.build_py):
295 """Custom project build command."""
296
297 def run(self):
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800298 try:
299 self.run_command('build_proto_modules')
300 except CommandError as error:
301 sys.stderr.write('warning: %s\n' % error.message)
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700302 self.run_command('build_project_metadata')
303 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700304
305
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800306class BuildExt(build_ext.build_ext):
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800307 """Custom build_ext command to enable compiler-specific flags."""
308
309 C_OPTIONS = {
310 'unix': ('-pthread', '-std=gnu99'),
311 'msvc': (),
312 }
313 LINK_OPTIONS = {}
314
315 def build_extensions(self):
316 compiler = self.compiler.compiler_type
317 if compiler in BuildExt.C_OPTIONS:
318 for extension in self.extensions:
319 extension.extra_compile_args += list(BuildExt.C_OPTIONS[compiler])
320 if compiler in BuildExt.LINK_OPTIONS:
321 for extension in self.extensions:
322 extension.extra_link_args += list(BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800323 try:
324 build_ext.build_ext.build_extensions(self)
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800325 except Exception as error:
Masood Malekghassemi50809092016-01-30 14:26:24 -0800326 formatted_exception = traceback.format_exc()
327 support.diagnose_build_ext_error(self, error, formatted_exception)
328 raise CommandError(
329 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800330
331
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700332class Gather(setuptools.Command):
333 """Command to gather project dependencies."""
334
335 description = 'gather dependencies for grpcio'
336 user_options = [
337 ('test', 't', 'flag indicating to gather test dependencies'),
338 ('install', 'i', 'flag indicating to gather install dependencies')
339 ]
340
341 def initialize_options(self):
342 self.test = False
343 self.install = False
344
345 def finalize_options(self):
346 # distutils requires this override.
347 pass
348
349 def run(self):
350 if self.install and self.distribution.install_requires:
351 self.distribution.fetch_build_eggs(self.distribution.install_requires)
352 if self.test and self.distribution.tests_require:
353 self.distribution.fetch_build_eggs(self.distribution.tests_require)
354
355
356class RunInterop(test.test):
357
358 description = 'run interop test client/server'
359 user_options = [
360 ('args=', 'a', 'pass-thru arguments for the client/server'),
361 ('client', 'c', 'flag indicating to run the client'),
362 ('server', 's', 'flag indicating to run the server')
363 ]
364
365 def initialize_options(self):
366 self.args = ''
367 self.client = False
368 self.server = False
369
370 def finalize_options(self):
371 if self.client and self.server:
372 raise DistutilsOptionError('you may only specify one of client or server')
373
374 def run(self):
375 if self.distribution.install_requires:
376 self.distribution.fetch_build_eggs(self.distribution.install_requires)
377 if self.distribution.tests_require:
378 self.distribution.fetch_build_eggs(self.distribution.tests_require)
379 if self.client:
380 self.run_client()
381 elif self.server:
382 self.run_server()
383
384 def run_server(self):
385 # We import here to ensure that our setuptools parent has had a chance to
386 # edit the Python system path.
387 from tests.interop import server
388 sys.argv[1:] = self.args.split()
389 server.serve()
390
391 def run_client(self):
392 # We import here to ensure that our setuptools parent has had a chance to
393 # edit the Python system path.
394 from tests.interop import client
395 sys.argv[1:] = self.args.split()
396 client.test_interoperability()