commands.py
Go to the documentation of this file.
1 # Copyright 2015 gRPC authors.
2 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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."""
15 
16 # NOTE(https://github.com/grpc/grpc/issues/24028): allow setuptools to monkey
17 # patch distutils
18 import setuptools # isort:skip
19 
20 import glob
21 import os
22 import os.path
23 import shutil
24 import subprocess
25 import sys
26 import sysconfig
27 import traceback
28 
29 from setuptools.command import build_ext
30 from setuptools.command import build_py
31 import support
32 
33 PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
34 GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
35 PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
36 PROTO_GEN_STEM = os.path.join(GRPC_STEM, 'src', 'python', 'gens')
37 CYTHON_STEM = os.path.join(PYTHON_STEM, 'grpc', '_cython')
38 
39 
40 class CommandError(Exception):
41  """Simple exception class for GRPC custom commands."""
42 
43 
44 # TODO(atash): Remove this once PyPI has better Linux bdist support. See
45 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
46 def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
47  """Returns a string path to a bdist file for Linux to install.
48 
49  If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
50  warning and builds from source.
51  """
52  # TODO(atash): somehow the name that's returned from `wheel` is different
53  # between different versions of 'wheel' (but from a compatibility standpoint,
54  # the names are compatible); we should have some way of determining name
55  # compatibility in the same way `wheel` does to avoid having to rename all of
56  # the custom wheels that we build/upload to GCS.
57 
58  # Break import style to ensure that setup.py has had a chance to install the
59  # relevant package.
60  from six.moves.urllib import request
61  decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
62  try:
63  url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
64  bdist_data = request.urlopen(url).read()
65  except IOError as error:
66  raise CommandError('{}\n\nCould not find the bdist {}: {}'.format(
67  traceback.format_exc(), decorated_path, error.message))
68  # Our chosen local bdist path.
69  bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
70  try:
71  with open(bdist_path, 'w') as bdist_file:
72  bdist_file.write(bdist_data)
73  except IOError as error:
74  raise CommandError('{}\n\nCould not write grpcio bdist: {}'.format(
75  traceback.format_exc(), error.message))
76  return bdist_path
77 
78 
79 class SphinxDocumentation(setuptools.Command):
80  """Command to generate documentation via sphinx."""
81 
82  description = 'generate sphinx documentation'
83  user_options = []
84 
85  def initialize_options(self):
86  pass
87 
88  def finalize_options(self):
89  pass
90 
91  def run(self):
92  # We import here to ensure that setup.py has had a chance to install the
93  # relevant package eggs first.
94  import sphinx.cmd.build
95  source_dir = os.path.join(GRPC_STEM, 'doc', 'python', 'sphinx')
96  target_dir = os.path.join(GRPC_STEM, 'doc', 'build')
97  exit_code = sphinx.cmd.build.build_main(
98  ['-b', 'html', '-W', '--keep-going', source_dir, target_dir])
99  if exit_code != 0:
100  raise CommandError(
101  "Documentation generation has warnings or errors")
102 
103 
104 class BuildProjectMetadata(setuptools.Command):
105  """Command to generate project metadata in a module."""
106 
107  description = 'build grpcio project metadata files'
108  user_options = []
109 
111  pass
112 
113  def finalize_options(self):
114  pass
115 
116  def run(self):
117  with open(os.path.join(PYTHON_STEM, 'grpc/_grpcio_metadata.py'),
118  'w') as module_file:
119  module_file.write('__version__ = """{}"""'.format(
120  self.distribution.get_version()))
121 
122 
123 class BuildPy(build_py.build_py):
124  """Custom project build command."""
125 
126  def run(self):
127  self.run_command('build_project_metadata')
128  build_py.build_py.run(self)
129 
130 
131 def _poison_extensions(extensions, message):
132  """Includes a file that will always fail to compile in all extensions."""
133  poison_filename = os.path.join(PYTHON_STEM, 'poison.c')
134  with open(poison_filename, 'w') as poison:
135  poison.write('#error {}'.format(message))
136  for extension in extensions:
137  extension.sources = [poison_filename]
138 
139 
141  """Replace .pyx files with their generated counterparts and return whether or
142  not cythonization still needs to occur."""
143  for extension in extensions:
144  generated_pyx_sources = []
145  other_sources = []
146  for source in extension.sources:
147  base, file_ext = os.path.splitext(source)
148  if file_ext == '.pyx':
149  generated_pyx_source = next((base + gen_ext for gen_ext in (
150  '.c',
151  '.cpp',
152  ) if os.path.isfile(base + gen_ext)), None)
153  if generated_pyx_source:
154  generated_pyx_sources.append(generated_pyx_source)
155  else:
156  sys.stderr.write('Cython-generated files are missing...\n')
157  return False
158  else:
159  other_sources.append(source)
160  extension.sources = generated_pyx_sources + other_sources
161  sys.stderr.write('Found cython-generated files...\n')
162  return True
163 
164 
165 def try_cythonize(extensions, linetracing=False, mandatory=True):
166  """Attempt to cythonize the extensions.
167 
168  Args:
169  extensions: A list of `distutils.extension.Extension`.
170  linetracing: A bool indicating whether or not to enable linetracing.
171  mandatory: Whether or not having Cython-generated files is mandatory. If it
172  is, extensions will be poisoned when they can't be fully generated.
173  """
174  try:
175  # Break import style to ensure we have access to Cython post-setup_requires
176  import Cython.Build
177  except ImportError:
178  if mandatory:
179  sys.stderr.write(
180  "This package needs to generate C files with Cython but it cannot. "
181  "Poisoning extension sources to disallow extension commands...")
183  extensions,
184  "Extensions have been poisoned due to missing Cython-generated code."
185  )
186  return extensions
187  cython_compiler_directives = {}
188  if linetracing:
189  additional_define_macros = [('CYTHON_TRACE_NOGIL', '1')]
190  cython_compiler_directives['linetrace'] = True
191  return Cython.Build.cythonize(
192  extensions,
193  include_path=[
194  include_dir for extension in extensions
195  for include_dir in extension.include_dirs
196  ] + [CYTHON_STEM],
197  compiler_directives=cython_compiler_directives)
198 
199 
200 class BuildExt(build_ext.build_ext):
201  """Custom build_ext command to enable compiler-specific flags."""
202 
203  C_OPTIONS = {
204  'unix': ('-pthread',),
205  'msvc': (),
206  }
207  LINK_OPTIONS = {}
208 
209  def get_ext_filename(self, ext_name):
210  # since python3.5, python extensions' shared libraries use a suffix that corresponds to the value
211  # of sysconfig.get_config_var('EXT_SUFFIX') and contains info about the architecture the library targets.
212  # E.g. on x64 linux the suffix is ".cpython-XYZ-x86_64-linux-gnu.so"
213  # When crosscompiling python wheels, we need to be able to override this suffix
214  # so that the resulting file name matches the target architecture and we end up with a well-formed
215  # wheel.
216  filename = build_ext.build_ext.get_ext_filename(self, ext_name)
217  orig_ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
218  new_ext_suffix = os.getenv('GRPC_PYTHON_OVERRIDE_EXT_SUFFIX')
219  if new_ext_suffix and filename.endswith(orig_ext_suffix):
220  filename = filename[:-len(orig_ext_suffix)] + new_ext_suffix
221  return filename
222 
223  def build_extensions(self):
224 
225  def compiler_ok_with_extra_std():
226  """Test if default compiler is okay with specifying c++ version
227  when invoked in C mode. GCC is okay with this, while clang is not.
228  """
229  try:
230  # TODO(lidiz) Remove the generated a.out for success tests.
231  cc_test = subprocess.Popen(['cc', '-x', 'c', '-std=c++14', '-'],
232  stdin=subprocess.PIPE,
233  stdout=subprocess.PIPE,
234  stderr=subprocess.PIPE)
235  _, cc_err = cc_test.communicate(input=b'int main(){return 0;}')
236  return not 'invalid argument' in str(cc_err)
237  except:
238  sys.stderr.write('Non-fatal exception:' +
239  traceback.format_exc() + '\n')
240  return False
241 
242  # This special conditioning is here due to difference of compiler
243  # behavior in gcc and clang. The clang doesn't take --stdc++11
244  # flags but gcc does. Since the setuptools of Python only support
245  # all C or all C++ compilation, the mix of C and C++ will crash.
246  # *By default*, macOS and FreBSD use clang and Linux use gcc
247  #
248  # If we are not using a permissive compiler that's OK with being
249  # passed wrong std flags, swap out compile function by adding a filter
250  # for it.
251  if not compiler_ok_with_extra_std():
252  old_compile = self.compiler._compile
253 
254  def new_compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
255  if src.endswith('.c'):
256  extra_postargs = [
257  arg for arg in extra_postargs if not '-std=c++' in arg
258  ]
259  elif src.endswith('.cc') or src.endswith('.cpp'):
260  extra_postargs = [
261  arg for arg in extra_postargs if not '-std=gnu99' in arg
262  ]
263  return old_compile(obj, src, ext, cc_args, extra_postargs,
264  pp_opts)
265 
266  self.compiler._compile = new_compile
267 
268  compiler = self.compiler.compiler_type
269  if compiler in BuildExt.C_OPTIONS:
270  for extension in self.extensions:
271  extension.extra_compile_args += list(
272  BuildExt.C_OPTIONS[compiler])
273  if compiler in BuildExt.LINK_OPTIONS:
274  for extension in self.extensions:
275  extension.extra_link_args += list(
276  BuildExt.LINK_OPTIONS[compiler])
279  try:
280  build_ext.build_ext.build_extensions(self)
281  except Exception as error:
282  formatted_exception = traceback.format_exc()
283  support.diagnose_build_ext_error(self, error, formatted_exception)
284  raise CommandError(
285  "Failed `build_ext` step:\n{}".format(formatted_exception))
286 
287 
288 class Gather(setuptools.Command):
289  """Command to gather project dependencies."""
290 
291  description = 'gather dependencies for grpcio'
292  user_options = [
293  ('test', 't', 'flag indicating to gather test dependencies'),
294  ('install', 'i', 'flag indicating to gather install dependencies')
295  ]
296 
298  self.test = False
299  self.install = False
300 
301  def finalize_options(self):
302  # distutils requires this override.
303  pass
304 
305  def run(self):
306  if self.install and self.distribution.install_requires:
307  self.distribution.fetch_build_eggs(
308  self.distribution.install_requires)
309  if self.test and self.distribution.tests_require:
310  self.distribution.fetch_build_eggs(self.distribution.tests_require)
311 
312 
313 class Clean(setuptools.Command):
314  """Command to clean build artifacts."""
315 
316  description = 'Clean build artifacts.'
317  user_options = [
318  ('all', 'a', 'a phony flag to allow our script to continue'),
319  ]
320 
321  _FILE_PATTERNS = (
322  'python_build',
323  'src/python/grpcio/__pycache__/',
324  'src/python/grpcio/grpc/_cython/cygrpc.cpp',
325  'src/python/grpcio/grpc/_cython/*.so',
326  'src/python/grpcio/grpcio.egg-info/',
327  )
328  _CURRENT_DIRECTORY = os.path.normpath(
329  os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../.."))
330 
332  self.all = False
333 
334  def finalize_options(self):
335  pass
336 
337  def run(self):
338  for path_spec in self._FILE_PATTERNS:
339  this_glob = os.path.normpath(
340  os.path.join(Clean._CURRENT_DIRECTORY, path_spec))
341  abs_paths = glob.glob(this_glob)
342  for path in abs_paths:
343  if not str(path).startswith(Clean._CURRENT_DIRECTORY):
344  raise ValueError(
345  "Cowardly refusing to delete {}.".format(path))
346  print("Removing {}".format(os.path.relpath(path)))
347  if os.path.isfile(path):
348  os.remove(str(path))
349  else:
350  shutil.rmtree(str(path))
xds_interop_client.str
str
Definition: xds_interop_client.py:487
commands.try_cythonize
def try_cythonize(extensions, linetracing=False, mandatory=True)
Definition: commands.py:165
http2_test_server.format
format
Definition: http2_test_server.py:118
commands.SphinxDocumentation.run
def run(self)
Definition: commands.py:91
support.diagnose_build_ext_error
def diagnose_build_ext_error(build_ext, error, formatted)
Definition: support.py:107
commands.SphinxDocumentation
Definition: commands.py:79
commands.BuildPy
Definition: commands.py:123
commands.BuildExt
Definition: commands.py:200
commands.CommandError
Definition: commands.py:40
commands.Clean.initialize_options
def initialize_options(self)
Definition: commands.py:331
commands.BuildProjectMetadata.run
def run(self)
Definition: commands.py:116
commands.Clean.all
all
Definition: commands.py:332
commands.check_and_update_cythonization
def check_and_update_cythonization(extensions)
Definition: commands.py:140
commands.BuildExt.get_ext_filename
def get_ext_filename(self, ext_name)
Definition: commands.py:209
commands.BuildProjectMetadata.initialize_options
def initialize_options(self)
Definition: commands.py:110
commands.SphinxDocumentation.initialize_options
def initialize_options(self)
Definition: commands.py:85
commands.Clean.run
def run(self)
Definition: commands.py:337
commands.Gather.run
def run(self)
Definition: commands.py:305
commands.Clean.finalize_options
def finalize_options(self)
Definition: commands.py:334
commands.BuildExt.build_extensions
def build_extensions(self)
Definition: commands.py:223
commands.Gather
Definition: commands.py:288
commands._poison_extensions
def _poison_extensions(extensions, message)
Definition: commands.py:131
commands.BuildProjectMetadata.finalize_options
def finalize_options(self)
Definition: commands.py:113
read
int read(izstream &zs, T *x, Items items)
Definition: bloaty/third_party/zlib/contrib/iostream2/zstream.h:115
commands.Gather.initialize_options
def initialize_options(self)
Definition: commands.py:297
commands.BuildExt.extensions
extensions
Definition: commands.py:278
next
AllocList * next[kMaxLevel]
Definition: abseil-cpp/absl/base/internal/low_level_alloc.cc:100
commands.BuildProjectMetadata
Definition: commands.py:104
open
#define open
Definition: test-fs.c:46
commands.Clean
Definition: commands.py:313
commands._get_grpc_custom_bdist
def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename)
Definition: commands.py:46
len
int len
Definition: abseil-cpp/absl/base/internal/low_level_alloc_test.cc:46
commands.Gather.test
test
Definition: commands.py:298
commands.BuildPy.run
def run(self)
Definition: commands.py:126
commands.Gather.install
install
Definition: commands.py:299
commands.SphinxDocumentation.finalize_options
def finalize_options(self)
Definition: commands.py:88
commands.Gather.finalize_options
def finalize_options(self)
Definition: commands.py:301


grpc
Author(s):
autogenerated on Thu Mar 13 2025 02:58:51