blob: aa29c728f259e6e47e2689e8978fd07b6bea7213 [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
122 src_dir = os.path.join(
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800123 PYTHON_STEM, self.distribution.package_dir[''], 'grpc')
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700124 sys.path.append(src_dir)
125 sphinx.apidoc.main([
126 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
127 '-V', metadata.version, '-R', metadata.version,
128 '-o', os.path.join('doc', 'src'), src_dir])
129 conf_filepath = os.path.join('doc', 'src', 'conf.py')
130 with open(conf_filepath, 'a') as conf_file:
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700131 conf_file.write(CONF_PY_ADDENDUM)
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700132 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
133
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700134
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700135class BuildProtoModules(setuptools.Command):
136 """Command to generate project *_pb2.py modules from proto files."""
137
138 description = 'build protobuf modules'
139 user_options = [
140 ('include=', None, 'path patterns to include in protobuf generation'),
141 ('exclude=', None, 'path patterns to exclude from protobuf generation')
142 ]
143
144 def initialize_options(self):
145 self.exclude = None
146 self.include = r'.*\.proto$'
147 self.protoc_command = None
148 self.grpc_python_plugin_command = None
149
150 def finalize_options(self):
151 self.protoc_command = distutils.spawn.find_executable('protoc')
152 self.grpc_python_plugin_command = distutils.spawn.find_executable(
153 'grpc_python_plugin')
154
155 def run(self):
Masood Malekghassemi04672952015-12-21 12:16:50 -0800156 if not self.protoc_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800157 raise CommandError('could not find protoc')
Masood Malekghassemi04672952015-12-21 12:16:50 -0800158 if not self.grpc_python_plugin_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800159 raise CommandError('could not find grpc_python_plugin '
160 '(protoc plugin for GRPC Python)')
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700161 include_regex = re.compile(self.include)
162 exclude_regex = re.compile(self.exclude) if self.exclude else None
163 paths = []
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800164 root_directory = PYTHON_STEM
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700165 for walk_root, directories, filenames in os.walk(root_directory):
166 for filename in filenames:
167 path = os.path.join(walk_root, filename)
168 if include_regex.match(path) and not (
169 exclude_regex and exclude_regex.match(path)):
170 paths.append(path)
171 command = [
172 self.protoc_command,
173 '--plugin=protoc-gen-python-grpc={}'.format(
174 self.grpc_python_plugin_command),
175 '-I {}'.format(root_directory),
176 '--python_out={}'.format(root_directory),
177 '--python-grpc_out={}'.format(root_directory),
178 ] + paths
179 try:
180 subprocess.check_output(' '.join(command), cwd=root_directory, shell=True,
181 stderr=subprocess.STDOUT)
182 except subprocess.CalledProcessError as e:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800183 raise CommandError('Command:\n{}\nMessage:\n{}\nOutput:\n{}'.format(
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700184 command, e.message, e.output))
185
186
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700187class BuildProjectMetadata(setuptools.Command):
188 """Command to generate project metadata in a module."""
189
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700190 description = 'build grpcio project metadata files'
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700191 user_options = []
192
193 def initialize_options(self):
194 pass
195
196 def finalize_options(self):
197 pass
198
199 def run(self):
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800200 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as module_file:
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700201 module_file.write('__version__ = """{}"""'.format(
202 self.distribution.get_version()))
203
204
205class BuildPy(build_py.build_py):
206 """Custom project build command."""
207
208 def run(self):
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800209 try:
210 self.run_command('build_proto_modules')
211 except CommandError as error:
212 sys.stderr.write('warning: %s\n' % error.message)
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700213 self.run_command('build_project_metadata')
214 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700215
216
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800217class BuildExt(build_ext.build_ext):
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800218 """Custom build_ext command to enable compiler-specific flags."""
219
220 C_OPTIONS = {
221 'unix': ('-pthread', '-std=gnu99'),
222 'msvc': (),
223 }
224 LINK_OPTIONS = {}
225
226 def build_extensions(self):
227 compiler = self.compiler.compiler_type
228 if compiler in BuildExt.C_OPTIONS:
229 for extension in self.extensions:
230 extension.extra_compile_args += list(BuildExt.C_OPTIONS[compiler])
231 if compiler in BuildExt.LINK_OPTIONS:
232 for extension in self.extensions:
233 extension.extra_link_args += list(BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800234 try:
235 build_ext.build_ext.build_extensions(self)
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800236 except Exception as error:
Masood Malekghassemi50809092016-01-30 14:26:24 -0800237 formatted_exception = traceback.format_exc()
238 support.diagnose_build_ext_error(self, error, formatted_exception)
239 raise CommandError(
240 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800241
242
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700243class Gather(setuptools.Command):
244 """Command to gather project dependencies."""
245
246 description = 'gather dependencies for grpcio'
247 user_options = [
248 ('test', 't', 'flag indicating to gather test dependencies'),
249 ('install', 'i', 'flag indicating to gather install dependencies')
250 ]
251
252 def initialize_options(self):
253 self.test = False
254 self.install = False
255
256 def finalize_options(self):
257 # distutils requires this override.
258 pass
259
260 def run(self):
261 if self.install and self.distribution.install_requires:
262 self.distribution.fetch_build_eggs(self.distribution.install_requires)
263 if self.test and self.distribution.tests_require:
264 self.distribution.fetch_build_eggs(self.distribution.tests_require)
265
266
267class RunInterop(test.test):
268
269 description = 'run interop test client/server'
270 user_options = [
271 ('args=', 'a', 'pass-thru arguments for the client/server'),
272 ('client', 'c', 'flag indicating to run the client'),
273 ('server', 's', 'flag indicating to run the server')
274 ]
275
276 def initialize_options(self):
277 self.args = ''
278 self.client = False
279 self.server = False
280
281 def finalize_options(self):
282 if self.client and self.server:
283 raise DistutilsOptionError('you may only specify one of client or server')
284
285 def run(self):
286 if self.distribution.install_requires:
287 self.distribution.fetch_build_eggs(self.distribution.install_requires)
288 if self.distribution.tests_require:
289 self.distribution.fetch_build_eggs(self.distribution.tests_require)
290 if self.client:
291 self.run_client()
292 elif self.server:
293 self.run_server()
294
295 def run_server(self):
296 # We import here to ensure that our setuptools parent has had a chance to
297 # edit the Python system path.
298 from tests.interop import server
299 sys.argv[1:] = self.args.split()
300 server.serve()
301
302 def run_client(self):
303 # We import here to ensure that our setuptools parent has had a chance to
304 # edit the Python system path.
305 from tests.interop import client
306 sys.argv[1:] = self.args.split()
307 client.test_interoperability()