blob: 5f28e9101f651d770b3636e1b270bbc181911be0 [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:
107 raise CommandError('{}\n\nCould not write grpcio bdist: {}'
108 .format(traceback.format_exc(), error.message))
109 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(
144 ['', os.path.join('doc', 'src'), os.path.join('doc', 'build')])
Masood Malekghassemid65632a2015-07-27 14:30:09 -0700145
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700146
147class BuildProjectMetadata(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800148 """Command to generate project metadata in a module."""
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700149
Masood Malekghassemicc793702017-01-13 19:20:10 -0800150 description = 'build grpcio project metadata files'
151 user_options = []
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700152
Masood Malekghassemicc793702017-01-13 19:20:10 -0800153 def initialize_options(self):
154 pass
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700155
Masood Malekghassemicc793702017-01-13 19:20:10 -0800156 def finalize_options(self):
157 pass
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700158
Masood Malekghassemicc793702017-01-13 19:20:10 -0800159 def run(self):
160 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
161 'w') as module_file:
162 module_file.write('__version__ = """{}"""'.format(
163 self.distribution.get_version()))
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700164
165
166class BuildPy(build_py.build_py):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800167 """Custom project build command."""
Masood Malekghassemi5c147632015-07-31 14:08:19 -0700168
Masood Malekghassemicc793702017-01-13 19:20:10 -0800169 def run(self):
170 self.run_command('build_project_metadata')
171 build_py.build_py.run(self)
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700172
173
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700174def _poison_extensions(extensions, message):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800175 """Includes a file that will always fail to compile in all extensions."""
176 poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
177 with open(poison_filename, 'w') as poison:
178 poison.write('#error {}'.format(message))
179 for extension in extensions:
180 extension.sources = [poison_filename]
181
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700182
183def check_and_update_cythonization(extensions):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800184 """Replace .pyx files with their generated counterparts and return whether or
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700185 not cythonization still needs to occur."""
Masood Malekghassemicc793702017-01-13 19:20:10 -0800186 for extension in extensions:
187 generated_pyx_sources = []
188 other_sources = []
189 for source in extension.sources:
190 base, file_ext = os.path.splitext(source)
191 if file_ext == '.pyx':
192 generated_pyx_source = next((base + gen_ext
Ken Payson2fa5f2f2017-02-06 10:27:09 -0800193 for gen_ext in ('.c', '.cpp',)
Masood Malekghassemicc793702017-01-13 19:20:10 -0800194 if os.path.isfile(base + gen_ext)),
195 None)
196 if generated_pyx_source:
197 generated_pyx_sources.append(generated_pyx_source)
198 else:
199 sys.stderr.write('Cython-generated files are missing...\n')
200 return False
201 else:
202 other_sources.append(source)
203 extension.sources = generated_pyx_sources + other_sources
204 sys.stderr.write('Found cython-generated files...\n')
205 return True
206
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700207
208def try_cythonize(extensions, linetracing=False, mandatory=True):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800209 """Attempt to cythonize the extensions.
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700210
211 Args:
212 extensions: A list of `distutils.extension.Extension`.
213 linetracing: A bool indicating whether or not to enable linetracing.
214 mandatory: Whether or not having Cython-generated files is mandatory. If it
215 is, extensions will be poisoned when they can't be fully generated.
216 """
Masood Malekghassemicc793702017-01-13 19:20:10 -0800217 try:
218 # Break import style to ensure we have access to Cython post-setup_requires
219 import Cython.Build
220 except ImportError:
221 if mandatory:
222 sys.stderr.write(
223 "This package needs to generate C files with Cython but it cannot. "
224 "Poisoning extension sources to disallow extension commands...")
225 _poison_extensions(
226 extensions,
227 "Extensions have been poisoned due to missing Cython-generated code."
228 )
229 return extensions
230 cython_compiler_directives = {}
231 if linetracing:
232 additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
233 cython_compiler_directives['linetrace'] = True
234 return Cython.Build.cythonize(
235 extensions,
236 include_path=[
237 include_dir
238 for extension in extensions
239 for include_dir in extension.include_dirs
240 ] + [CYTHON_STEM],
241 compiler_directives=cython_compiler_directives)
Masood Malekghassemifd9cc102016-07-21 14:20:43 -0700242
243
Masood Malekghassemi14a0a932016-01-21 20:13:22 -0800244class BuildExt(build_ext.build_ext):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800245 """Custom build_ext command to enable compiler-specific flags."""
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800246
Masood Malekghassemicc793702017-01-13 19:20:10 -0800247 C_OPTIONS = {
Ken Payson3b900552017-04-10 15:53:17 -0700248 'unix': ('-pthread',),
Masood Malekghassemicc793702017-01-13 19:20:10 -0800249 'msvc': (),
250 }
251 LINK_OPTIONS = {}
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800252
Masood Malekghassemicc793702017-01-13 19:20:10 -0800253 def build_extensions(self):
Ken Payson571c75a2017-04-13 16:39:37 -0700254 if "darwin" in sys.platform:
Ken Payson63b0d112017-04-17 00:03:40 -0700255 config = os.environ.get('CONFIG', 'opt')
Ken Payson571c75a2017-04-13 16:39:37 -0700256 target_path = os.path.abspath(
Ken Payson0eac8aa2017-04-17 01:58:15 -0700257 os.path.join(
258 os.path.dirname(os.path.realpath(__file__)), '..', '..',
259 '..', 'libs', config))
260 targets = [
261 os.path.join(target_path, 'libboringssl.a'),
262 os.path.join(target_path, 'libares.a'),
263 os.path.join(target_path, 'libgpr.a'),
264 os.path.join(target_path, 'libgrpc.a')
265 ]
266 make_process = subprocess.Popen(
267 ['make'] + targets,
268 stdout=subprocess.PIPE,
269 stderr=subprocess.PIPE)
Ken Payson571c75a2017-04-13 16:39:37 -0700270 make_out, make_err = make_process.communicate()
271 if make_out and make_process.returncode != 0:
Ken Paysonb91c5fb2017-05-09 13:28:32 -0700272 sys.stdout.write(str(make_out) + '\n')
Ken Payson571c75a2017-04-13 16:39:37 -0700273 if make_err:
Ken Paysonb91c5fb2017-05-09 13:28:32 -0700274 sys.stderr.write(str(make_err) + '\n')
Ken Payson571c75a2017-04-13 16:39:37 -0700275 if make_process.returncode != 0:
Ken Payson0eac8aa2017-04-17 01:58:15 -0700276 raise Exception("make command failed!")
Ken Payson571c75a2017-04-13 16:39:37 -0700277
Masood Malekghassemicc793702017-01-13 19:20:10 -0800278 compiler = self.compiler.compiler_type
279 if compiler in BuildExt.C_OPTIONS:
280 for extension in self.extensions:
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500281 extension.extra_compile_args += list(
282 BuildExt.C_OPTIONS[compiler])
Masood Malekghassemicc793702017-01-13 19:20:10 -0800283 if compiler in BuildExt.LINK_OPTIONS:
284 for extension in self.extensions:
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500285 extension.extra_link_args += list(
286 BuildExt.LINK_OPTIONS[compiler])
Masood Malekghassemicc793702017-01-13 19:20:10 -0800287 if not check_and_update_cythonization(self.extensions):
288 self.extensions = try_cythonize(self.extensions)
289 try:
290 build_ext.build_ext.build_extensions(self)
291 except Exception as error:
292 formatted_exception = traceback.format_exc()
293 support.diagnose_build_ext_error(self, error, formatted_exception)
Masood Malekghassemi6b890d12017-01-23 12:02:08 -0500294 raise CommandError(
295 "Failed `build_ext` step:\n{}".format(formatted_exception))
Masood Malekghassemi1d177812016-01-12 09:21:57 -0800296
297
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700298class Gather(setuptools.Command):
Masood Malekghassemicc793702017-01-13 19:20:10 -0800299 """Command to gather project dependencies."""
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700300
Masood Malekghassemicc793702017-01-13 19:20:10 -0800301 description = 'gather dependencies for grpcio'
302 user_options = [
303 ('test', 't', 'flag indicating to gather test dependencies'),
304 ('install', 'i', 'flag indicating to gather install dependencies')
305 ]
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700306
Masood Malekghassemicc793702017-01-13 19:20:10 -0800307 def initialize_options(self):
308 self.test = False
309 self.install = False
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700310
Masood Malekghassemicc793702017-01-13 19:20:10 -0800311 def finalize_options(self):
312 # distutils requires this override.
313 pass
Masood Malekghassemi7566c9a2015-10-21 20:29:23 -0700314
Masood Malekghassemicc793702017-01-13 19:20:10 -0800315 def run(self):
316 if self.install and self.distribution.install_requires:
317 self.distribution.fetch_build_eggs(
318 self.distribution.install_requires)
319 if self.test and self.distribution.tests_require:
320 self.distribution.fetch_build_eggs(self.distribution.tests_require)