blob: 99ba41b614f0f3e3afc8c721ed3ba0319a18d597 [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 Malekghassemi1d177812016-01-12 09:21:57 -080049
Masood Malekghassemi5fec8b32016-01-25 16:16:50 -080050import support
51
Masood Malekghassemi116982e2015-12-11 15:53:38 -080052PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
53
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070054CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070055extensions.append('sphinx.ext.napoleon')
56napoleon_google_docstring = True
57napoleon_numpy_docstring = True
58
59html_theme = 'sphinx_rtd_theme'
60"""
61
Masood Malekghassemife8dc882015-07-27 15:30:33 -070062
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080063class CommandError(Exception):
64 """Simple exception class for GRPC custom commands."""
65
66
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080067# 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-supported
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080069def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
70 """Returns a string path to a bdist file for Linux to install.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080071
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080072 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080073 warning and builds from source.
74 """
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080075 # 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
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080081 # Break import style to ensure that setup.py has had a chance to install the
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080082 # relevant package.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080083 from six.moves.urllib import request
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080084 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080085 try:
Masood Malekghassemif751b0b2016-02-04 11:34:53 -080086 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080087 bdist_data = request.urlopen(url).read()
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080088 except IOError as error:
89 raise CommandError(
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080090 '{}\n\nCould not find the bdist {}: {}'
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080091 .format(traceback.format_exc(), decorated_path, error.message))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080092 # Our chosen local bdist path.
93 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080094 try:
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080095 with open(bdist_path, 'w') as bdist_file:
96 bdist_file.write(bdist_data)
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080097 except IOError as error:
98 raise CommandError(
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080099 '{}\n\nCould not write grpcio bdist: {}'
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800100 .format(traceback.format_exc(), error.message))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800101 return bdist_path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800102
103
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700104class SphinxDocumentation(setuptools.Command):
105 """Command to generate documentation via sphinx."""
106
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700107 description = 'generate sphinx documentation'
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700108 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
Masood Malekghassemi3ee1f9b2016-02-22 18:37:18 -0800122 src_dir = os.path.join(PYTHON_STEM, 'grpc')
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700123 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:
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700130 conf_file.write(CONF_PY_ADDENDUM)
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700131 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
132
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700133
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700134class 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):
Masood Malekghassemi04672952015-12-21 12:16:50 -0800155 if not self.protoc_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800156 raise CommandError('could not find protoc')
Masood Malekghassemi04672952015-12-21 12:16:50 -0800157 if not self.grpc_python_plugin_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800158 raise CommandError('could not find grpc_python_plugin '
159 '(protoc plugin for GRPC Python)')
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700160 include_regex = re.compile(self.include)
161 exclude_regex = re.compile(self.exclude) if self.exclude else None
162 paths = []
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800163 root_directory = PYTHON_STEM
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700164 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:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800182 raise CommandError('Command:\n{}\nMessage:\n{}\nOutput:\n{}'.format(
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700183 command, e.message, e.output))
184
185
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700186class BuildProjectMetadata(setuptools.Command):
187 """Command to generate project metadata in a module."""
188
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700189 description = 'build grpcio project metadata files'
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700190 user_options = []
191
192 def initialize_options(self):
193 pass
194
195 def finalize_options(self):
196 pass
197
198 def run(self):
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800199 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as module_file:
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700200 module_file.write('__version__ = """{}"""'.format(
201 self.distribution.get_version()))
202
203
204class BuildPy(build_py.build_py):
205 """Custom project build command."""
206
207 def run(self):
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800208 try:
209 self.run_command('build_proto_modules')
210 except CommandError as error:
211 sys.stderr.write('warning: %s\n' % error.message)
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700212 self.run_command('build_project_metadata')
213 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700214
215
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800216class BuildExt(build_ext.build_ext):
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800217 """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])
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800233 try:
234 build_ext.build_ext.build_extensions(self)
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800235 except Exception as error:
Masood Malekghassemi50809092016-01-30 14:26:24 -0800236 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))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800240
241
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700242class 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
266class RunInterop(test.test):
267
268 description = 'run interop test client/server'
269 user_options = [
270 ('args=', 'a', 'pass-thru arguments for the client/server'),
271 ('client', 'c', 'flag indicating to run the client'),
272 ('server', 's', 'flag indicating to run the server')
273 ]
274
275 def initialize_options(self):
276 self.args = ''
277 self.client = False
278 self.server = False
279
280 def finalize_options(self):
281 if self.client and self.server:
282 raise DistutilsOptionError('you may only specify one of client or server')
283
284 def run(self):
285 if self.distribution.install_requires:
286 self.distribution.fetch_build_eggs(self.distribution.install_requires)
287 if self.distribution.tests_require:
288 self.distribution.fetch_build_eggs(self.distribution.tests_require)
289 if self.client:
290 self.run_client()
291 elif self.server:
292 self.run_server()
293
294 def run_server(self):
295 # We import here to ensure that our setuptools parent has had a chance to
296 # edit the Python system path.
297 from tests.interop import server
298 sys.argv[1:] = self.args.split()
299 server.serve()
300
301 def run_client(self):
302 # We import here to ensure that our setuptools parent has had a chance to
303 # edit the Python system path.
304 from tests.interop import client
305 sys.argv[1:] = self.args.split()
306 client.test_interoperability()