blob: 0a3097111fc644c7bb7163d7b174b9bb6aa4cfcc [file] [log] [blame]
Jan Tattermusch7897ae92017-06-07 22:57:36 +02001# Copyright 2015 gRPC authors.
Masood Malekghassemid65632a2015-07-27 14:30:09 -07002#
Jan Tattermusch7897ae92017-06-07 22:57:36 +02003# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
Masood Malekghassemid65632a2015-07-27 14:30:09 -07006#
Jan Tattermusch7897ae92017-06-07 22:57:36 +02007# http://www.apache.org/licenses/LICENSE-2.0
Masood Malekghassemid65632a2015-07-27 14:30:09 -07008#
Jan Tattermusch7897ae92017-06-07 22:57:36 +02009# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Masood Malekghassemid65632a2015-07-27 14:30:09 -070014"""Provides distutils command classes for the GRPC Python setup process."""
15
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070016import distutils
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080017import glob
Masood Malekghassemid65632a2015-07-27 14:30:09 -070018import os
19import os.path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080020import platform
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070021import re
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080022import shutil
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070023import subprocess
Masood Malekghassemid65632a2015-07-27 14:30:09 -070024import sys
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080025import traceback
Masood Malekghassemid65632a2015-07-27 14:30:09 -070026
27import setuptools
Masood Malekghassemi58a1dc22016-01-21 14:23:55 -080028from setuptools.command import build_ext
Masood Malekghassemi5c147632015-07-31 14:08:19 -070029from setuptools.command import build_py
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080030from setuptools.command import easy_install
31from setuptools.command import install
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070032from setuptools.command import test
Masood Malekghassemi1d177812016-01-12 09:21:57 -080033
Masood Malekghassemi5fec8b32016-01-25 16:16:50 -080034import support
35
Masood Malekghassemi116982e2015-12-11 15:53:38 -080036PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
Ken Payson707c9e22016-04-20 09:42:19 -070037GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
38PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
39PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
Ken Payson77222452016-09-11 22:06:05 -070040CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
Masood Malekghassemi116982e2015-12-11 15:53:38 -080041
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -070042CONF_PY_ADDENDUM = """
Masood Malekghassemid65632a2015-07-27 14:30:09 -070043extensions.append('sphinx.ext.napoleon')
44napoleon_google_docstring = True
45napoleon_numpy_docstring = True
Masood Malekghassemi48d07c62016-07-12 15:47:05 -070046napoleon_include_special_with_doc = True
Masood Malekghassemid65632a2015-07-27 14:30:09 -070047
48html_theme = 'sphinx_rtd_theme'
Masood Malekghassemi25186ae2016-12-27 09:05:13 -080049copyright = "2016, The gRPC Authors"
Masood Malekghassemid65632a2015-07-27 14:30:09 -070050"""
51
Ken Paysonf7f47a62016-07-11 11:09:27 -070052API_GLOSSARY = """
53
54Glossary
55================
56
57.. glossary::
58
59 metadatum
60 A key-value pair included in the HTTP header. It is a
61 2-tuple where the first entry is the key and the
62 second is the value, i.e. (key, value). The metadata key is an ASCII str,
63 and must be a valid HTTP header name. The metadata value can be
64 either a valid HTTP ASCII str, or bytes. If bytes are provided,
65 the key must end with '-bin', i.e.
66 ``('binary-metadata-bin', b'\\x00\\xFF')``
67
68 metadata
69 A sequence of metadatum.
70"""
71
Masood Malekghassemife8dc882015-07-27 15:30:33 -070072
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080073class CommandError(Exception):
Masood Malekghassemicc793702017-01-13 19:20:10 -080074 """Simple exception class for GRPC custom commands."""
Masood Malekghassemi59994bc2016-01-12 08:49:26 -080075
76
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080077# TODO(atash): Remove this once PyPI has better Linux bdist support. See
78# https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080079def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
Masood Malekghassemicc793702017-01-13 19:20:10 -080080 """Returns a string path to a bdist file for Linux to install.
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080081
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080082 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -080083 warning and builds from source.
84 """
Masood Malekghassemicc793702017-01-13 19:20:10 -080085 # TODO(atash): somehow the name that's returned from `wheel` is different
86 # between different versions of 'wheel' (but from a compatibility standpoint,
87 # the names are compatible); we should have some way of determining name
88 # compatibility in the same way `wheel` does to avoid having to rename all of
89 # the custom wheels that we build/upload to GCS.
Masood Malekghassemi334e9e62016-02-10 20:12:59 -080090
Masood Malekghassemicc793702017-01-13 19:20:10 -080091 # Break import style to ensure that setup.py has had a chance to install the
92 # relevant package.
93 from six.moves.urllib import request
94 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
95 try:
96 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
97 bdist_data = request.urlopen(url).read()
98 except IOError as error:
99 raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
100 traceback.format_exc(), decorated_path, error.message))
101 # Our chosen local bdist path.
102 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
103 try:
104 with open(bdist_path, 'w') as bdist_file:
105 bdist_file.write(bdist_data)
106 except IOError as error:
Mehrdad Afshari87cd9942018-01-02 14:40:00 -0800107 raise CommandError('{}\n\nCould not write grpcio bdist: {}'.format(
108 traceback.format_exc(), error.message))
Masood Malekghassemicc793702017-01-13 19:20:10 -0800109 return bdist_path
Masood Malekghassemi154b0ee2016-01-25 16:45:29 -0800110
111
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700112class SphinxDocumentation(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800113 """Command to generate documentation via sphinx."""
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700114
Masood Malekghassemicc793702017-01-13 19:20:10 -0800115 description = 'generate sphinx documentation'
116 user_options = []
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700117
Masood Malekghassemicc793702017-01-13 19:20:10 -0800118 def initialize_options(self):
119 pass
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700120
Masood Malekghassemicc793702017-01-13 19:20:10 -0800121 def finalize_options(self):
122 pass
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700123
Masood Malekghassemicc793702017-01-13 19:20:10 -0800124 def run(self):
125 # We import here to ensure that setup.py has had a chance to install the
126 # relevant package eggs first.
127 import sphinx
128 import sphinx.apidoc
129 metadata = self.distribution.metadata
130 src_dir = os.path.join(PYTHON_STEM, 'grpc')
131 sys.path.append(src_dir)
132 sphinx.apidoc.main([
133 '', '--force', '--full', '-H', metadata.name, '-A', metadata.author,
134 '-V', metadata.version, '-R', metadata.version, '-o',
135 os.path.join('doc', 'src'), src_dir
136 ])
137 conf_filepath = os.path.join('doc', 'src', 'conf.py')
138 with open(conf_filepath, 'a') as conf_file:
139 conf_file.write(CONF_PY_ADDENDUM)
140 glossary_filepath = os.path.join('doc', 'src', 'grpc.rst')
141 with open(glossary_filepath, 'a') as glossary_filepath:
142 glossary_filepath.write(API_GLOSSARY)
143 sphinx.main(
Mehrdad Afshari87cd9942018-01-02 14:40:00 -0800144 ['', os.path.join('doc', 'src'),
145 os.path.join('doc', 'build')])
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700146
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700147
148class BuildProjectMetadata(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800149 """Command to generate project metadata in a module."""
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700150
Masood Malekghassemicc793702017-01-13 19:20:10 -0800151 description = 'build grpcio project metadata files'
152 user_options = []
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700153
Masood Malekghassemicc793702017-01-13 19:20:10 -0800154 def initialize_options(self):
155 pass
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700156
Masood Malekghassemicc793702017-01-13 19:20:10 -0800157 def finalize_options(self):
158 pass
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700159
Masood Malekghassemicc793702017-01-13 19:20:10 -0800160 def run(self):
161 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
162 'w') as module_file:
163 module_file.write('__version__ = """{}"""'.format(
164 self.distribution.get_version()))
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700165
166
167class BuildPy(build_py.build_py):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800168 """Custom project build command."""
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700169
Masood Malekghassemicc793702017-01-13 19:20:10 -0800170 def run(self):
171 self.run_command('build_project_metadata')
172 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700173
174
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700175def _poison_extensions(extensions, message):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800176 """Includes a file that will always fail to compile in all extensions."""
177 poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
178 with open(poison_filename, 'w') as poison:
179 poison.write('#error {}'.format(message))
180 for extension in extensions:
181 extension.sources = [poison_filename]
182
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700183
184def check_and_update_cythonization(extensions):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800185 """Replace .pyx files with their generated counterparts and return whether or
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700186 not cythonization still needs to occur."""
Masood Malekghassemicc793702017-01-13 19:20:10 -0800187 for extension in extensions:
188 generated_pyx_sources = []
189 other_sources = []
190 for source in extension.sources:
191 base, file_ext = os.path.splitext(source)
192 if file_ext == '.pyx':
Mehrdad Afshari87cd9942018-01-02 14:40:00 -0800193 generated_pyx_source = next(
194 (base + gen_ext for gen_ext in (
195 '.c',
196 '.cpp',
197 ) if os.path.isfile(base + gen_ext)), None)
Masood Malekghassemicc793702017-01-13 19:20:10 -0800198 if generated_pyx_source:
199 generated_pyx_sources.append(generated_pyx_source)
200 else:
201 sys.stderr.write('Cython-generated files are missing...\n')
202 return False
203 else:
204 other_sources.append(source)
205 extension.sources = generated_pyx_sources + other_sources
206 sys.stderr.write('Found cython-generated files...\n')
207 return True
208
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700209
210def try_cythonize(extensions, linetracing=False, mandatory=True):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800211 """Attempt to cythonize the extensions.
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700212
213 Args:
214 extensions: A list of `distutils.extension.Extension`.
215 linetracing: A bool indicating whether or not to enable linetracing.
216 mandatory: Whether or not having Cython-generated files is mandatory. If it
217 is, extensions will be poisoned when they can't be fully generated.
218 """
Masood Malekghassemicc793702017-01-13 19:20:10 -0800219 try:
220 # Break import style to ensure we have access to Cython post-setup_requires
221 import Cython.Build
222 except ImportError:
223 if mandatory:
224 sys.stderr.write(
225 "This package needs to generate C files with Cython but it cannot. "
226 "Poisoning extension sources to disallow extension commands...")
227 _poison_extensions(
228 extensions,
229 "Extensions have been poisoned due to missing Cython-generated code."
230 )
231 return extensions
232 cython_compiler_directives = {}
233 if linetracing:
234 additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
235 cython_compiler_directives['linetrace'] = True
236 return Cython.Build.cythonize(
237 extensions,
238 include_path=[
239 include_dir
240 for extension in extensions
241 for include_dir in extension.include_dirs
242 ] + [CYTHON_STEM],
243 compiler_directives=cython_compiler_directives)
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700244
245
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800246class BuildExt(build_ext.build_ext):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800247 """Custom build_ext command to enable compiler-specific flags."""
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800248
Masood Malekghassemicc793702017-01-13 19:20:10 -0800249 C_OPTIONS = {
Ken Payson3b900552017-04-10 15:53:17 -0700250 'unix': ('-pthread',),
Masood Malekghassemicc793702017-01-13 19:20:10 -0800251 'msvc': (),
252 }
253 LINK_OPTIONS = {}
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800254
Masood Malekghassemicc793702017-01-13 19:20:10 -0800255 def build_extensions(self):
Ken Payson571c75a2017-04-13 16:39:37 -0700256 if "darwin" in sys.platform:
Ken Payson63b0d112017-04-17 00:03:40 -0700257 config = os.environ.get('CONFIG', 'opt')
Ken Payson571c75a2017-04-13 16:39:37 -0700258 target_path = os.path.abspath(
Ken Payson0eac8aa2017-04-17 01:58:15 -0700259 os.path.join(
260 os.path.dirname(os.path.realpath(__file__)), '..', '..',
261 '..', 'libs', config))
262 targets = [
263 os.path.join(target_path, 'libboringssl.a'),
264 os.path.join(target_path, 'libares.a'),
265 os.path.join(target_path, 'libgpr.a'),
266 os.path.join(target_path, 'libgrpc.a')
267 ]
Eric Gribkoff0ff641a2018-08-22 12:25:08 -0700268 # Running make separately for Mac means we lose all
269 # Extension.define_macros configured in setup.py. Re-add the macro
270 # for gRPC Core's fork handlers.
271 # TODO(ericgribkoff) Decide what to do about the other missing core
272 # macros, including GRPC_ENABLE_FORK_SUPPORT, which defaults to 1
273 # on Linux but remains unset on Mac.
274 extra_defines = [
275 'EXTRA_DEFINES="GRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1"'
276 ]
Ken Payson0eac8aa2017-04-17 01:58:15 -0700277 make_process = subprocess.Popen(
Eric Gribkoff0ff641a2018-08-22 12:25:08 -0700278 ['make'] + extra_defines + targets,
Ken Payson0eac8aa2017-04-17 01:58:15 -0700279 stdout=subprocess.PIPE,
280 stderr=subprocess.PIPE)
Ken Payson571c75a2017-04-13 16:39:37 -0700281 make_out, make_err = make_process.communicate()
282 if make_out and make_process.returncode != 0:
Ken Paysonb91c5fb2017-05-09 13:28:32 -0700283 sys.stdout.write(str(make_out) + '\n')
Ken Payson571c75a2017-04-13 16:39:37 -0700284 if make_err:
Ken Paysonb91c5fb2017-05-09 13:28:32 -0700285 sys.stderr.write(str(make_err) + '\n')
Ken Payson571c75a2017-04-13 16:39:37 -0700286 if make_process.returncode != 0:
Ken Payson0eac8aa2017-04-17 01:58:15 -0700287 raise Exception("make command failed!")
Ken Payson571c75a2017-04-13 16:39:37 -0700288
Masood Malekghassemicc793702017-01-13 19:20:10 -0800289 compiler = self.compiler.compiler_type
290 if compiler in BuildExt.C_OPTIONS:
291 for extension in self.extensions:
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500292 extension.extra_compile_args += list(
293 BuildExt.C_OPTIONS[compiler])
Masood Malekghassemicc793702017-01-13 19:20:10 -0800294 if compiler in BuildExt.LINK_OPTIONS:
295 for extension in self.extensions:
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500296 extension.extra_link_args += list(
297 BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemicc793702017-01-13 19:20:10 -0800298 if not check_and_update_cythonization(self.extensions):
299 self.extensions = try_cythonize(self.extensions)
300 try:
301 build_ext.build_ext.build_extensions(self)
302 except Exception as error:
303 formatted_exception = traceback.format_exc()
304 support.diagnose_build_ext_error(self, error, formatted_exception)
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500305 raise CommandError(
306 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800307
308
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700309class Gather(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800310 """Command to gather project dependencies."""
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700311
Masood Malekghassemicc793702017-01-13 19:20:10 -0800312 description = 'gather dependencies for grpcio'
Mehrdad Afshari87cd9942018-01-02 14:40:00 -0800313 user_options = [('test', 't',
314 'flag indicating to gather test dependencies'),
315 ('install', 'i',
316 'flag indicating to gather install dependencies')]
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700317
Masood Malekghassemicc793702017-01-13 19:20:10 -0800318 def initialize_options(self):
319 self.test = False
320 self.install = False
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700321
Masood Malekghassemicc793702017-01-13 19:20:10 -0800322 def finalize_options(self):
323 # distutils requires this override.
324 pass
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700325
Masood Malekghassemicc793702017-01-13 19:20:10 -0800326 def run(self):
327 if self.install and self.distribution.install_requires:
328 self.distribution.fetch_build_eggs(
329 self.distribution.install_requires)
330 if self.test and self.distribution.tests_require:
331 self.distribution.fetch_build_eggs(self.distribution.tests_require)