blob: 701c6af0179300b52344a981f0a770202a7fcba2 [file] [log] [blame]
Craig Tiller6169d5f2016-03-31 07:46:18 -07001# Copyright 2015, 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__))
Ken Payson707c9e22016-04-20 09:42:19 -070053GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
54PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
55PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
Ken Payson77222452016-09-11 22:06:05 -070056CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
Masood Malekghassemi116982e2015-12-11 15:53:38 -080057
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070058CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070059extensions.append('sphinx.ext.napoleon')
60napoleon_google_docstring = True
61napoleon_numpy_docstring = True
Masood Malekghassemi48d07c62016-07-12 15:47:05 -070062napoleon_include_special_with_doc = True
Masood Malekghassemid65632a2015-07-27 14:30:09 -070063
64html_theme = 'sphinx_rtd_theme'
Masood Malekghassemi25186ae2016-12-27 09:05:13 -080065copyright = "2016, The gRPC Authors"
Masood Malekghassemid65632a2015-07-27 14:30:09 -070066"""
67
Ken Paysonf7f47a62016-07-11 11:09:27 -070068API_GLOSSARY = """
69
70Glossary
71================
72
73.. glossary::
74
75 metadatum
76 A key-value pair included in the HTTP header. It is a
77 2-tuple where the first entry is the key and the
78 second is the value, i.e. (key, value). The metadata key is an ASCII str,
79 and must be a valid HTTP header name. The metadata value can be
80 either a valid HTTP ASCII str, or bytes. If bytes are provided,
81 the key must end with '-bin', i.e.
82 ``('binary-metadata-bin', b'\\x00\\xFF')``
83
84 metadata
85 A sequence of metadatum.
86"""
87
Masood Malekghassemife8dc882015-07-27 15:30:33 -070088
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080089class CommandError(Exception):
90 """Simple exception class for GRPC custom commands."""
91
92
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080093# TODO(atash): Remove this once PyPI has better Linux bdist support. See
94# https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080095def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
96 """Returns a string path to a bdist file for Linux to install.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080097
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080098 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080099 warning and builds from source.
100 """
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800101 # TODO(atash): somehow the name that's returned from `wheel` is different
102 # between different versions of 'wheel' (but from a compatibility standpoint,
103 # the names are compatible); we should have some way of determining name
104 # compatibility in the same way `wheel` does to avoid having to rename all of
105 # the custom wheels that we build/upload to GCS.
106
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800107 # Break import style to ensure that setup.py has had a chance to install the
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800108 # relevant package.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800109 from six.moves.urllib import request
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800110 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800111 try:
Masood Malekghassemif751b0b2016-02-04 11:34:53 -0800112 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800113 bdist_data = request.urlopen(url).read()
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800114 except IOError as error:
115 raise CommandError(
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800116 '{}\n\nCould not find the bdist {}: {}'
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800117 .format(traceback.format_exc(), decorated_path, error.message))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800118 # Our chosen local bdist path.
119 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800120 try:
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800121 with open(bdist_path, 'w') as bdist_file:
122 bdist_file.write(bdist_data)
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800123 except IOError as error:
124 raise CommandError(
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800125 '{}\n\nCould not write grpcio bdist: {}'
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800126 .format(traceback.format_exc(), error.message))
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800127 return bdist_path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800128
129
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700130class SphinxDocumentation(setuptools.Command):
131 """Command to generate documentation via sphinx."""
132
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700133 description = 'generate sphinx documentation'
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700134 user_options = []
135
136 def initialize_options(self):
137 pass
138
139 def finalize_options(self):
140 pass
141
142 def run(self):
143 # We import here to ensure that setup.py has had a chance to install the
144 # relevant package eggs first.
145 import sphinx
146 import sphinx.apidoc
147 metadata = self.distribution.metadata
Masood Malekghassemi3ee1f9b2016-02-22 18:37:18 -0800148 src_dir = os.path.join(PYTHON_STEM, 'grpc')
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700149 sys.path.append(src_dir)
150 sphinx.apidoc.main([
151 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
152 '-V', metadata.version, '-R', metadata.version,
153 '-o', os.path.join('doc', 'src'), src_dir])
154 conf_filepath = os.path.join('doc', 'src', 'conf.py')
155 with open(conf_filepath, 'a') as conf_file:
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700156 conf_file.write(CONF_PY_ADDENDUM)
Ken Paysonf7f47a62016-07-11 11:09:27 -0700157 glossary_filepath = os.path.join('doc', 'src', 'grpc.rst')
158 with open(glossary_filepath, 'a') as glossary_filepath:
159 glossary_filepath.write(API_GLOSSARY)
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700160 sphinx.main(['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
161
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700162
163class BuildProjectMetadata(setuptools.Command):
164 """Command to generate project metadata in a module."""
165
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700166 description = 'build grpcio project metadata files'
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700167 user_options = []
168
169 def initialize_options(self):
170 pass
171
172 def finalize_options(self):
173 pass
174
175 def run(self):
Masood Malekghassemi116982e2015-12-11 15:53:38 -0800176 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'), 'w') as module_file:
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700177 module_file.write('__version__ = """{}"""'.format(
178 self.distribution.get_version()))
179
180
181class BuildPy(build_py.build_py):
182 """Custom project build command."""
183
184 def run(self):
185 self.run_command('build_project_metadata')
186 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700187
188
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700189def _poison_extensions(extensions, message):
190 """Includes a file that will always fail to compile in all extensions."""
191 poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
192 with open(poison_filename, 'w') as poison:
193 poison.write('#error {}'.format(message))
194 for extension in extensions:
195 extension.sources = [poison_filename]
196
197def check_and_update_cythonization(extensions):
198 """Replace .pyx files with their generated counterparts and return whether or
199 not cythonization still needs to occur."""
200 for extension in extensions:
201 generated_pyx_sources = []
202 other_sources = []
203 for source in extension.sources:
204 base, file_ext = os.path.splitext(source)
205 if file_ext == '.pyx':
206 generated_pyx_source = next(
207 (base + gen_ext for gen_ext in ('.c', '.cpp',)
208 if os.path.isfile(base + gen_ext)), None)
209 if generated_pyx_source:
210 generated_pyx_sources.append(generated_pyx_source)
211 else:
212 sys.stderr.write('Cython-generated files are missing...\n')
213 return False
214 else:
215 other_sources.append(source)
216 extension.sources = generated_pyx_sources + other_sources
217 sys.stderr.write('Found cython-generated files...\n')
218 return True
219
220def try_cythonize(extensions, linetracing=False, mandatory=True):
221 """Attempt to cythonize the extensions.
222
223 Args:
224 extensions: A list of `distutils.extension.Extension`.
225 linetracing: A bool indicating whether or not to enable linetracing.
226 mandatory: Whether or not having Cython-generated files is mandatory. If it
227 is, extensions will be poisoned when they can't be fully generated.
228 """
229 try:
230 # Break import style to ensure we have access to Cython post-setup_requires
231 import Cython.Build
232 except ImportError:
233 if mandatory:
234 sys.stderr.write(
235 "This package needs to generate C files with Cython but it cannot. "
236 "Poisoning extension sources to disallow extension commands...")
237 _poison_extensions(
238 extensions,
239 "Extensions have been poisoned due to missing Cython-generated code.")
240 return extensions
241 cython_compiler_directives = {}
242 if linetracing:
243 additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
244 cython_compiler_directives['linetrace'] = True
245 return Cython.Build.cythonize(
246 extensions,
247 include_path=[
248 include_dir for extension in extensions for include_dir in extension.include_dirs
Ken Payson77222452016-09-11 22:06:05 -0700249 ] + [CYTHON_STEM],
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700250 compiler_directives=cython_compiler_directives
251 )
252
253
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800254class BuildExt(build_ext.build_ext):
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800255 """Custom build_ext command to enable compiler-specific flags."""
256
257 C_OPTIONS = {
258 'unix': ('-pthread', '-std=gnu99'),
259 'msvc': (),
260 }
261 LINK_OPTIONS = {}
262
263 def build_extensions(self):
264 compiler = self.compiler.compiler_type
265 if compiler in BuildExt.C_OPTIONS:
266 for extension in self.extensions:
267 extension.extra_compile_args += list(BuildExt.C_OPTIONS[compiler])
268 if compiler in BuildExt.LINK_OPTIONS:
269 for extension in self.extensions:
270 extension.extra_link_args += list(BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700271 if not check_and_update_cythonization(self.extensions):
272 self.extensions = try_cythonize(self.extensions)
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800273 try:
274 build_ext.build_ext.build_extensions(self)
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -0800275 except Exception as error:
Masood Malekghassemi50809092016-01-30 14:26:24 -0800276 formatted_exception = traceback.format_exc()
277 support.diagnose_build_ext_error(self, error, formatted_exception)
278 raise CommandError(
279 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800280
281
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700282class Gather(setuptools.Command):
283 """Command to gather project dependencies."""
284
285 description = 'gather dependencies for grpcio'
286 user_options = [
287 ('test', 't', 'flag indicating to gather test dependencies'),
288 ('install', 'i', 'flag indicating to gather install dependencies')
289 ]
290
291 def initialize_options(self):
292 self.test = False
293 self.install = False
294
295 def finalize_options(self):
296 # distutils requires this override.
297 pass
298
299 def run(self):
300 if self.install and self.distribution.install_requires:
301 self.distribution.fetch_build_eggs(self.distribution.install_requires)
302 if self.test and self.distribution.tests_require:
303 self.distribution.fetch_build_eggs(self.distribution.tests_require)