1 # Copyright 2015 gRPC authors.
3 # 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
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # 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.
14 """Provides distutils command classes for the GRPC Python setup process."""
28 from setuptools.command import build_ext
29 from setuptools.command import build_py
30 from setuptools.command import easy_install
31 from setuptools.command import install
32 from setuptools.command import test
36 PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
37 GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
38 PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
39 PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
40 CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
43 class CommandError(Exception):
44 """Simple exception class for GRPC custom commands."""
47 # TODO(atash): Remove this once PyPI has better Linux bdist support. See
48 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
49 def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
50 """Returns a string path to a bdist file for Linux to install.
52 If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
53 warning and builds from source.
55 # TODO(atash): somehow the name that's returned from `wheel` is different
56 # between different versions of 'wheel' (but from a compatibility standpoint,
57 # the names are compatible); we should have some way of determining name
58 # compatibility in the same way `wheel` does to avoid having to rename all of
59 # the custom wheels that we build/upload to GCS.
61 # Break import style to ensure that setup.py has had a chance to install the
63 from six.moves.urllib import request
64 decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
66 url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
67 bdist_data = request.urlopen(url).read()
68 except IOError as error:
69 raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
70 traceback.format_exc(), decorated_path, error.message))
71 # Our chosen local bdist path.
72 bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
74 with open(bdist_path, 'w') as bdist_file:
75 bdist_file.write(bdist_data)
76 except IOError as error:
77 raise CommandError('{}\n\nCould not write grpcio bdist: {}'.format(
78 traceback.format_exc(), error.message))
82 class SphinxDocumentation(setuptools.Command):
83 """Command to generate documentation via sphinx."""
85 description = 'generate sphinx documentation'
88 def initialize_options(self):
91 def finalize_options(self):
95 # We import here to ensure that setup.py has had a chance to install the
96 # relevant package eggs first.
97 import sphinx.cmd.build
98 source_dir = os.path.join(GRPC_STEM, 'doc', 'python', 'sphinx')
99 target_dir = os.path.join(GRPC_STEM, 'doc', 'build')
100 exit_code = sphinx.cmd.build.build_main(
101 ['-b', 'html', '-W', '--keep-going', source_dir, target_dir])
102 if exit_code is not 0:
104 "Documentation generation has warnings or errors")
107 class BuildProjectMetadata(setuptools.Command):
108 """Command to generate project metadata in a module."""
110 description = 'build grpcio project metadata files'
113 def initialize_options(self):
116 def finalize_options(self):
120 with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
122 module_file.write('__version__ = """{}"""'.format(
123 self.distribution.get_version()))
126 class BuildPy(build_py.build_py):
127 """Custom project build command."""
130 self.run_command('build_project_metadata')
131 build_py.build_py.run(self)
134 def _poison_extensions(extensions, message):
135 """Includes a file that will always fail to compile in all extensions."""
136 poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
137 with open(poison_filename, 'w') as poison:
138 poison.write('#error {}'.format(message))
139 for extension in extensions:
140 extension.sources = [poison_filename]
143 def check_and_update_cythonization(extensions):
144 """Replace .pyx files with their generated counterparts and return whether or
145 not cythonization still needs to occur."""
146 for extension in extensions:
147 generated_pyx_sources = []
149 for source in extension.sources:
150 base, file_ext = os.path.splitext(source)
151 if file_ext == '.pyx':
152 generated_pyx_source = next(
153 (base + gen_ext for gen_ext in (
156 ) if os.path.isfile(base + gen_ext)), None)
157 if generated_pyx_source:
158 generated_pyx_sources.append(generated_pyx_source)
160 sys.stderr.write('Cython-generated files are missing...\n')
163 other_sources.append(source)
164 extension.sources = generated_pyx_sources + other_sources
165 sys.stderr.write('Found cython-generated files...\n')
169 def try_cythonize(extensions, linetracing=False, mandatory=True):
170 """Attempt to cythonize the extensions.
173 extensions: A list of `distutils.extension.Extension`.
174 linetracing: A bool indicating whether or not to enable linetracing.
175 mandatory: Whether or not having Cython-generated files is mandatory. If it
176 is, extensions will be poisoned when they can't be fully generated.
179 # Break import style to ensure we have access to Cython post-setup_requires
184 "This package needs to generate C files with Cython but it cannot. "
185 "Poisoning extension sources to disallow extension commands...")
188 "Extensions have been poisoned due to missing Cython-generated code."
191 cython_compiler_directives = {}
193 additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
194 cython_compiler_directives['linetrace'] = True
195 return Cython.Build.cythonize(
199 for extension in extensions
200 for include_dir in extension.include_dirs
202 compiler_directives=cython_compiler_directives)
205 class BuildExt(build_ext.build_ext):
206 """Custom build_ext command to enable compiler-specific flags."""
209 'unix': ('-pthread',),
214 def build_extensions(self):
216 def compiler_ok_with_extra_std():
217 """Test if default compiler is okay with specifying c++ version
218 when invoked in C mode. GCC is okay with this, while clang is not.
220 cc_test = subprocess.Popen(
221 ['cc', '-x', 'c', '-std=c++11', '-'],
222 stdin=subprocess.PIPE,
223 stdout=subprocess.PIPE,
224 stderr=subprocess.PIPE)
225 _, cc_err = cc_test.communicate(input=b'int main(){return 0;}')
226 return not 'invalid argument' in str(cc_err)
228 # This special conditioning is here due to difference of compiler
229 # behavior in gcc and clang. The clang doesn't take --stdc++11
230 # flags but gcc does. Since the setuptools of Python only support
231 # all C or all C++ compilation, the mix of C and C++ will crash.
232 # *By default*, macOS and FreBSD use clang and Linux use gcc
234 # If we are not using a permissive compiler that's OK with being
235 # passed wrong std flags, swap out compile function by adding a filter
237 if not compiler_ok_with_extra_std():
238 old_compile = self.compiler._compile
240 def new_compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
243 arg for arg in extra_postargs if not '-std=c++' in arg
245 return old_compile(obj, src, ext, cc_args, extra_postargs,
248 self.compiler._compile = new_compile
250 compiler = self.compiler.compiler_type
251 if compiler in BuildExt.C_OPTIONS:
252 for extension in self.extensions:
253 extension.extra_compile_args += list(
254 BuildExt.C_OPTIONS[compiler])
255 if compiler in BuildExt.LINK_OPTIONS:
256 for extension in self.extensions:
257 extension.extra_link_args += list(
258 BuildExt.LINK_OPTIONS[compiler])
259 if not check_and_update_cythonization(self.extensions):
260 self.extensions = try_cythonize(self.extensions)
262 build_ext.build_ext.build_extensions(self)
263 except Exception as error:
264 formatted_exception = traceback.format_exc()
265 support.diagnose_build_ext_error(self, error, formatted_exception)
267 "Failed `build_ext` step:\n{}".format(formatted_exception))
270 class Gather(setuptools.Command):
271 """Command to gather project dependencies."""
273 description = 'gather dependencies for grpcio'
274 user_options = [('test', 't',
275 'flag indicating to gather test dependencies'),
277 'flag indicating to gather install dependencies')]
279 def initialize_options(self):
283 def finalize_options(self):
284 # distutils requires this override.
288 if self.install and self.distribution.install_requires:
289 self.distribution.fetch_build_eggs(
290 self.distribution.install_requires)
291 if self.test and self.distribution.tests_require:
292 self.distribution.fetch_build_eggs(self.distribution.tests_require)