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