blob: e50ccbe23e74a6633536067cee87eca35f9a229b [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.
Masood Malekghassemid65632a2015-07-27 14:30:09 -070029"""Provides distutils command classes for the GRPC Python setup process."""
30
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070031import distutils
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080032import glob
Masood Malekghassemid65632a2015-07-27 14:30:09 -070033import os
34import os.path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080035import platform
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070036import re
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080037import shutil
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070038import subprocess
Masood Malekghassemid65632a2015-07-27 14:30:09 -070039import sys
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080040import traceback
Masood Malekghassemid65632a2015-07-27 14:30:09 -070041
42import setuptools
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -080043from setuptools.command import build_ext
Masood Malekghassemi5c147632015-07-31 14:08:19 -070044from setuptools.command import build_py
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080045from setuptools.command import easy_install
46from setuptools.command import install
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070047from setuptools.command import test
Masood Malekghassemi1d177812016-01-12 09:21:57 -080048
Masood Malekghassemi5fec8b32016-01-25 16:16:50 -080049import support
50
Masood Malekghassemi116982e2015-12-11 15:53:38 -080051PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
Ken Payson707c9e22016-04-20 09:42:19 -070052GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
53PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
54PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
Ken Payson77222452016-09-11 22:06:05 -070055CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
Masood Malekghassemi116982e2015-12-11 15:53:38 -080056
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070057CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070058extensions.append('sphinx.ext.napoleon')
59napoleon_google_docstring = True
60napoleon_numpy_docstring = True
Masood Malekghassemi48d07c62016-07-12 15:47:05 -070061napoleon_include_special_with_doc = True
Masood Malekghassemid65632a2015-07-27 14:30:09 -070062
63html_theme = 'sphinx_rtd_theme'
Masood Malekghassemi25186ae2016-12-27 09:05:13 -080064copyright = "2016, The gRPC Authors"
Masood Malekghassemid65632a2015-07-27 14:30:09 -070065"""
66
Ken Paysonf7f47a62016-07-11 11:09:27 -070067API_GLOSSARY = """
68
69Glossary
70================
71
72.. glossary::
73
74 metadatum
75 A key-value pair included in the HTTP header. It is a
76 2-tuple where the first entry is the key and the
77 second is the value, i.e. (key, value). The metadata key is an ASCII str,
78 and must be a valid HTTP header name. The metadata value can be
79 either a valid HTTP ASCII str, or bytes. If bytes are provided,
80 the key must end with '-bin', i.e.
81 ``('binary-metadata-bin', b'\\x00\\xFF')``
82
83 metadata
84 A sequence of metadatum.
85"""
86
Masood Malekghassemife8dc882015-07-27 15:30:33 -070087
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080088class CommandError(Exception):
Masood Malekghassemicc793702017-01-13 19:20:10 -080089 """Simple exception class for GRPC custom commands."""
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080090
91
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080092# TODO(atash): Remove this once PyPI has better Linux bdist support. See
93# https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080094def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
Masood Malekghassemicc793702017-01-13 19:20:10 -080095 """Returns a string path to a bdist file for Linux to install.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080096
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080097 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080098 warning and builds from source.
99 """
Masood Malekghassemicc793702017-01-13 19:20:10 -0800100 # TODO(atash): somehow the name that's returned from `wheel` is different
101 # between different versions of 'wheel' (but from a compatibility standpoint,
102 # the names are compatible); we should have some way of determining name
103 # compatibility in the same way `wheel` does to avoid having to rename all of
104 # the custom wheels that we build/upload to GCS.
Masood Malekghassemi334e9e62016-02-10 20:12:59 -0800105
Masood Malekghassemicc793702017-01-13 19:20:10 -0800106 # Break import style to ensure that setup.py has had a chance to install the
107 # relevant package.
108 from six.moves.urllib import request
109 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
110 try:
111 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
112 bdist_data = request.urlopen(url).read()
113 except IOError as error:
114 raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
115 traceback.format_exc(), decorated_path, error.message))
116 # Our chosen local bdist path.
117 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
118 try:
119 with open(bdist_path, 'w') as bdist_file:
120 bdist_file.write(bdist_data)
121 except IOError as error:
122 raise CommandError('{}\n\nCould not write grpcio bdist: {}'
123 .format(traceback.format_exc(), error.message))
124 return bdist_path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800125
126
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700127class SphinxDocumentation(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800128 """Command to generate documentation via sphinx."""
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700129
Masood Malekghassemicc793702017-01-13 19:20:10 -0800130 description = 'generate sphinx documentation'
131 user_options = []
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700132
Masood Malekghassemicc793702017-01-13 19:20:10 -0800133 def initialize_options(self):
134 pass
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700135
Masood Malekghassemicc793702017-01-13 19:20:10 -0800136 def finalize_options(self):
137 pass
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700138
Masood Malekghassemicc793702017-01-13 19:20:10 -0800139 def run(self):
140 # We import here to ensure that setup.py has had a chance to install the
141 # relevant package eggs first.
142 import sphinx
143 import sphinx.apidoc
144 metadata = self.distribution.metadata
145 src_dir = os.path.join(PYTHON_STEM, 'grpc')
146 sys.path.append(src_dir)
147 sphinx.apidoc.main([
148 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
149 '-V', metadata.version, '-R', metadata.version, '-o',
150 os.path.join('doc', 'src'), src_dir
151 ])
152 conf_filepath = os.path.join('doc', 'src', 'conf.py')
153 with open(conf_filepath, 'a') as conf_file:
154 conf_file.write(CONF_PY_ADDENDUM)
155 glossary_filepath = os.path.join('doc', 'src', 'grpc.rst')
156 with open(glossary_filepath, 'a') as glossary_filepath:
157 glossary_filepath.write(API_GLOSSARY)
158 sphinx.main(
159 ['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700160
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700161
162class BuildProjectMetadata(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800163 """Command to generate project metadata in a module."""
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700164
Masood Malekghassemicc793702017-01-13 19:20:10 -0800165 description = 'build grpcio project metadata files'
166 user_options = []
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700167
Masood Malekghassemicc793702017-01-13 19:20:10 -0800168 def initialize_options(self):
169 pass
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700170
Masood Malekghassemicc793702017-01-13 19:20:10 -0800171 def finalize_options(self):
172 pass
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700173
Masood Malekghassemicc793702017-01-13 19:20:10 -0800174 def run(self):
175 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
176 'w') as module_file:
177 module_file.write('__version__ = """{}"""'.format(
178 self.distribution.get_version()))
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700179
180
181class BuildPy(build_py.build_py):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800182 """Custom project build command."""
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700183
Masood Malekghassemicc793702017-01-13 19:20:10 -0800184 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):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800190 """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
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700197
198def check_and_update_cythonization(extensions):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800199 """Replace .pyx files with their generated counterparts and return whether or
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700200 not cythonization still needs to occur."""
Masood Malekghassemicc793702017-01-13 19:20:10 -0800201 for extension in extensions:
202 generated_pyx_sources = []
203 other_sources = []
204 for source in extension.sources:
205 base, file_ext = os.path.splitext(source)
206 if file_ext == '.pyx':
207 generated_pyx_source = next((base + gen_ext
Ken Payson2fa5f2f2017-02-06 10:27:09 -0800208 for gen_ext in ('.c', '.cpp',)
Masood Malekghassemicc793702017-01-13 19:20:10 -0800209 if os.path.isfile(base + gen_ext)),
210 None)
211 if generated_pyx_source:
212 generated_pyx_sources.append(generated_pyx_source)
213 else:
214 sys.stderr.write('Cython-generated files are missing...\n')
215 return False
216 else:
217 other_sources.append(source)
218 extension.sources = generated_pyx_sources + other_sources
219 sys.stderr.write('Found cython-generated files...\n')
220 return True
221
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700222
223def try_cythonize(extensions, linetracing=False, mandatory=True):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800224 """Attempt to cythonize the extensions.
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700225
226 Args:
227 extensions: A list of `distutils.extension.Extension`.
228 linetracing: A bool indicating whether or not to enable linetracing.
229 mandatory: Whether or not having Cython-generated files is mandatory. If it
230 is, extensions will be poisoned when they can't be fully generated.
231 """
Masood Malekghassemicc793702017-01-13 19:20:10 -0800232 try:
233 # Break import style to ensure we have access to Cython post-setup_requires
234 import Cython.Build
235 except ImportError:
236 if mandatory:
237 sys.stderr.write(
238 "This package needs to generate C files with Cython but it cannot. "
239 "Poisoning extension sources to disallow extension commands...")
240 _poison_extensions(
241 extensions,
242 "Extensions have been poisoned due to missing Cython-generated code."
243 )
244 return extensions
245 cython_compiler_directives = {}
246 if linetracing:
247 additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
248 cython_compiler_directives['linetrace'] = True
249 return Cython.Build.cythonize(
250 extensions,
251 include_path=[
252 include_dir
253 for extension in extensions
254 for include_dir in extension.include_dirs
255 ] + [CYTHON_STEM],
256 compiler_directives=cython_compiler_directives)
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700257
258
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800259class BuildExt(build_ext.build_ext):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800260 """Custom build_ext command to enable compiler-specific flags."""
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800261
Masood Malekghassemicc793702017-01-13 19:20:10 -0800262 C_OPTIONS = {
263 'unix': ('-pthread', '-std=gnu99'),
264 'msvc': (),
265 }
266 LINK_OPTIONS = {}
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800267
Masood Malekghassemicc793702017-01-13 19:20:10 -0800268 def build_extensions(self):
269 compiler = self.compiler.compiler_type
270 if compiler in BuildExt.C_OPTIONS:
271 for extension in self.extensions:
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500272 extension.extra_compile_args += list(
273 BuildExt.C_OPTIONS[compiler])
Masood Malekghassemicc793702017-01-13 19:20:10 -0800274 if compiler in BuildExt.LINK_OPTIONS:
275 for extension in self.extensions:
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500276 extension.extra_link_args += list(
277 BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemicc793702017-01-13 19:20:10 -0800278 if not check_and_update_cythonization(self.extensions):
279 self.extensions = try_cythonize(self.extensions)
280 try:
281 build_ext.build_ext.build_extensions(self)
282 except Exception as error:
283 formatted_exception = traceback.format_exc()
284 support.diagnose_build_ext_error(self, error, formatted_exception)
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500285 raise CommandError(
286 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800287
288
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700289class Gather(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800290 """Command to gather project dependencies."""
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700291
Masood Malekghassemicc793702017-01-13 19:20:10 -0800292 description = 'gather dependencies for grpcio'
293 user_options = [
294 ('test', 't', 'flag indicating to gather test dependencies'),
295 ('install', 'i', 'flag indicating to gather install dependencies')
296 ]
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700297
Masood Malekghassemicc793702017-01-13 19:20:10 -0800298 def initialize_options(self):
299 self.test = False
300 self.install = False
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700301
Masood Malekghassemicc793702017-01-13 19:20:10 -0800302 def finalize_options(self):
303 # distutils requires this override.
304 pass
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700305
Masood Malekghassemicc793702017-01-13 19:20:10 -0800306 def run(self):
307 if self.install and self.distribution.install_requires:
308 self.distribution.fetch_build_eggs(
309 self.distribution.install_requires)
310 if self.test and self.distribution.tests_require:
311 self.distribution.fetch_build_eggs(self.distribution.tests_require)