blob: e1a3f4bed3cc9ed74644e69d0aed202261b9a422 [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 Malekghassemid65632a2015-07-27 14:30:09 -070033import os
34import os.path
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070035import re
36import subprocess
Masood Malekghassemid65632a2015-07-27 14:30:09 -070037import sys
38
39import setuptools
Masood Malekghassemi5c147632015-07-31 14:08:19 -070040from setuptools.command import build_py
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070041from setuptools.command import test
Masood Malekghassemid65632a2015-07-27 14:30:09 -070042
Masood Malekghassemi1d177812016-01-12 09:21:57 -080043# Because we need to support building without Cython but simultaneously need to
44# subclass its command class when we need to and because distutils requires a
45# special hook to acquire a command class, we attempt to import Cython's
46# build_ext, and if that fails we import setuptools'.
47try:
48 # Due to the strange way Cython's Distutils module re-imports build_ext, we
49 # import the build_ext class directly.
50 from Cython.Distutils.build_ext import build_ext
51except ImportError:
52 from setuptools.command.build_ext import build_ext
53
Masood Malekghassemi116982e2015-12-11 15:53:38 -080054PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
55
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070056CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070057extensions.append('sphinx.ext.napoleon')
58napoleon_google_docstring = True
59napoleon_numpy_docstring = True
60
61html_theme = 'sphinx_rtd_theme'
62"""
63
Masood Malekghassemife8dc882015-07-27 15:30:33 -070064
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080065class CommandError(Exception):
66 """Simple exception class for GRPC custom commands."""
67
68
Masood Malekghassemid65632a2015-07-27 14:30:09 -070069class SphinxDocumentation(setuptools.Command):
70 """Command to generate documentation via sphinx."""
71
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070072 description = 'generate sphinx documentation'
Masood Malekghassemid65632a2015-07-27 14:30:09 -070073 user_options = []
74
75 def initialize_options(self):
76 pass
77
78 def finalize_options(self):
79 pass
80
81 def run(self):
82 # We import here to ensure that setup.py has had a chance to install the
83 # relevant package eggs first.
84 import sphinx
85 import sphinx.apidoc
86 metadata = self.distribution.metadata
87 src_dir = os.path.join(
Masood Malekghassemi116982e2015-12-11 15:53:38 -080088 PYTHON_STEM, self.distribution.package_dir[''], 'grpc')
Masood Malekghassemid65632a2015-07-27 14:30:09 -070089 sys.path.append(src_dir)
90 sphinx.apidoc.main([
91 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
92 '-V', metadata.version, '-R', metadata.version,
93 '-o', os.path.join('doc', 'src'), src_dir])
94 conf_filepath = os.path.join('doc', 'src', 'conf.py')
95 with open(conf_filepath, 'a') as conf_file:
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070096 conf_file.write(CONF_PY_ADDENDUM)
Masood Malekghassemid65632a2015-07-27 14:30:09 -070097 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
98
Masood Malekghassemi5c147632015-07-31 14:08:19 -070099
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700100class BuildProtoModules(setuptools.Command):
101 """Command to generate project *_pb2.py modules from proto files."""
102
103 description = 'build protobuf modules'
104 user_options = [
105 ('include=', None, 'path patterns to include in protobuf generation'),
106 ('exclude=', None, 'path patterns to exclude from protobuf generation')
107 ]
108
109 def initialize_options(self):
110 self.exclude = None
111 self.include = r'.*\.proto$'
112 self.protoc_command = None
113 self.grpc_python_plugin_command = None
114
115 def finalize_options(self):
116 self.protoc_command = distutils.spawn.find_executable('protoc')
117 self.grpc_python_plugin_command = distutils.spawn.find_executable(
118 'grpc_python_plugin')
119
120 def run(self):
Masood Malekghassemi04672952015-12-21 12:16:50 -0800121 if not self.protoc_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800122 raise CommandError('could not find protoc')
Masood Malekghassemi04672952015-12-21 12:16:50 -0800123 if not self.grpc_python_plugin_command:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800124 raise CommandError('could not find grpc_python_plugin '
125 '(protoc plugin for GRPC Python)')
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700126 include_regex = re.compile(self.include)
127 exclude_regex = re.compile(self.exclude) if self.exclude else None
128 paths = []
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800129 root_directory = PYTHON_STEM
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700130 for walk_root, directories, filenames in os.walk(root_directory):
131 for filename in filenames:
132 path = os.path.join(walk_root, filename)
133 if include_regex.match(path) and not (
134 exclude_regex and exclude_regex.match(path)):
135 paths.append(path)
136 command = [
137 self.protoc_command,
138 '--plugin=protoc-gen-python-grpc={}'.format(
139 self.grpc_python_plugin_command),
140 '-I {}'.format(root_directory),
141 '--python_out={}'.format(root_directory),
142 '--python-grpc_out={}'.format(root_directory),
143 ] + paths
144 try:
145 subprocess.check_output(' '.join(command), cwd=root_directory, shell=True,
146 stderr=subprocess.STDOUT)
147 except subprocess.CalledProcessError as e:
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800148 raise CommandError('Command:\n{}\nMessage:\n{}\nOutput:\n{}'.format(
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700149 command, e.message, e.output))
150
151
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700152class BuildProjectMetadata(setuptools.Command):
153 """Command to generate project metadata in a module."""
154
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700155 description = 'build grpcio project metadata files'
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700156 user_options = []
157
158 def initialize_options(self):
159 pass
160
161 def finalize_options(self):
162 pass
163
164 def run(self):
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800165 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as module_file:
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700166 module_file.write('__version__ = """{}"""'.format(
167 self.distribution.get_version()))
168
169
170class BuildPy(build_py.build_py):
171 """Custom project build command."""
172
173 def run(self):
Masood Malekghassemi59994bc2016-01-12 08:49:26 -0800174 try:
175 self.run_command('build_proto_modules')
176 except CommandError as error:
177 sys.stderr.write('warning: %s\n' % error.message)
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700178 self.run_command('build_project_metadata')
179 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700180
181
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800182class BuildExt(build_ext):
183 """Custom build_ext command to enable compiler-specific flags."""
184
185 C_OPTIONS = {
186 'unix': ('-pthread', '-std=gnu99'),
187 'msvc': (),
188 }
189 LINK_OPTIONS = {}
190
191 def build_extensions(self):
192 compiler = self.compiler.compiler_type
193 if compiler in BuildExt.C_OPTIONS:
194 for extension in self.extensions:
195 extension.extra_compile_args += list(BuildExt.C_OPTIONS[compiler])
196 if compiler in BuildExt.LINK_OPTIONS:
197 for extension in self.extensions:
198 extension.extra_link_args += list(BuildExt.LINK_OPTIONS[compiler])
199 build_ext.build_extensions(self)
200
201
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700202class Gather(setuptools.Command):
203 """Command to gather project dependencies."""
204
205 description = 'gather dependencies for grpcio'
206 user_options = [
207 ('test', 't', 'flag indicating to gather test dependencies'),
208 ('install', 'i', 'flag indicating to gather install dependencies')
209 ]
210
211 def initialize_options(self):
212 self.test = False
213 self.install = False
214
215 def finalize_options(self):
216 # distutils requires this override.
217 pass
218
219 def run(self):
220 if self.install and self.distribution.install_requires:
221 self.distribution.fetch_build_eggs(self.distribution.install_requires)
222 if self.test and self.distribution.tests_require:
223 self.distribution.fetch_build_eggs(self.distribution.tests_require)
224
225
226class RunInterop(test.test):
227
228 description = 'run interop test client/server'
229 user_options = [
230 ('args=', 'a', 'pass-thru arguments for the client/server'),
231 ('client', 'c', 'flag indicating to run the client'),
232 ('server', 's', 'flag indicating to run the server')
233 ]
234
235 def initialize_options(self):
236 self.args = ''
237 self.client = False
238 self.server = False
239
240 def finalize_options(self):
241 if self.client and self.server:
242 raise DistutilsOptionError('you may only specify one of client or server')
243
244 def run(self):
245 if self.distribution.install_requires:
246 self.distribution.fetch_build_eggs(self.distribution.install_requires)
247 if self.distribution.tests_require:
248 self.distribution.fetch_build_eggs(self.distribution.tests_require)
249 if self.client:
250 self.run_client()
251 elif self.server:
252 self.run_server()
253
254 def run_server(self):
255 # We import here to ensure that our setuptools parent has had a chance to
256 # edit the Python system path.
257 from tests.interop import server
258 sys.argv[1:] = self.args.split()
259 server.serve()
260
261 def run_client(self):
262 # We import here to ensure that our setuptools parent has had a chance to
263 # edit the Python system path.
264 from tests.interop import client
265 sys.argv[1:] = self.args.split()
266 client.test_interoperability()