1# Copyright 2016 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 15import errno 16import os 17import os.path 18import platform 19import re 20import shlex 21import shutil 22import subprocess 23from subprocess import PIPE 24import sys 25import sysconfig 26 27import setuptools 28from setuptools import Extension 29from setuptools.command import build_ext 30 31# TODO(atash) add flag to disable Cython use 32 33_PACKAGE_PATH = os.path.realpath(os.path.dirname(__file__)) 34_README_PATH = os.path.join(_PACKAGE_PATH, "README.rst") 35 36os.chdir(os.path.dirname(os.path.abspath(__file__))) 37sys.path.insert(0, os.path.abspath(".")) 38 39import _parallel_compile_patch 40import protoc_lib_deps 41 42import grpc_version 43 44_EXT_INIT_SYMBOL = None 45if sys.version_info[0] == 2: 46 _EXT_INIT_SYMBOL = "init_protoc_compiler" 47else: 48 _EXT_INIT_SYMBOL = "PyInit__protoc_compiler" 49 50_parallel_compile_patch.monkeypatch_compile_maybe() 51 52CLASSIFIERS = [ 53 "Development Status :: 5 - Production/Stable", 54 "Programming Language :: Python", 55 "Programming Language :: Python :: 3", 56 "License :: OSI Approved :: Apache Software License", 57] 58 59PY3 = sys.version_info.major == 3 60 61 62def _env_bool_value(env_name, default): 63 """Parses a bool option from an environment variable""" 64 return os.environ.get(env_name, default).upper() not in ["FALSE", "0", ""] 65 66 67# Environment variable to determine whether or not the Cython extension should 68# *use* Cython or use the generated C files. Note that this requires the C files 69# to have been generated by building first *with* Cython support. 70BUILD_WITH_CYTHON = _env_bool_value("GRPC_PYTHON_BUILD_WITH_CYTHON", "False") 71 72# Export this variable to force building the python extension with a statically linked libstdc++. 73# At least on linux, this is normally not needed as we can build manylinux-compatible wheels on linux just fine 74# without statically linking libstdc++ (which leads to a slight increase in the wheel size). 75# This option is useful when crosscompiling wheels for aarch64 where 76# it's difficult to ensure that the crosscompilation toolchain has a high-enough version 77# of GCC (we require >=5.1) but still uses old-enough libstdc++ symbols. 78# TODO(jtattermusch): remove this workaround once issues with crosscompiler version are resolved. 79BUILD_WITH_STATIC_LIBSTDCXX = _env_bool_value( 80 "GRPC_PYTHON_BUILD_WITH_STATIC_LIBSTDCXX", "False" 81) 82 83 84def check_linker_need_libatomic(): 85 """Test if linker on system needs libatomic.""" 86 code_test = ( 87 b"#include <atomic>\n" 88 + b"int main() { return std::atomic<int64_t>{}; }" 89 ) 90 cxx = os.environ.get("CXX", "c++") 91 cpp_test = subprocess.Popen( 92 [cxx, "-x", "c++", "-std=c++14", "-"], 93 stdin=PIPE, 94 stdout=PIPE, 95 stderr=PIPE, 96 ) 97 cpp_test.communicate(input=code_test) 98 if cpp_test.returncode == 0: 99 return False 100 # Double-check to see if -latomic actually can solve the problem. 101 # https://github.com/grpc/grpc/issues/22491 102 cpp_test = subprocess.Popen( 103 [cxx, "-x", "c++", "-std=c++14", "-", "-latomic"], 104 stdin=PIPE, 105 stdout=PIPE, 106 stderr=PIPE, 107 ) 108 cpp_test.communicate(input=code_test) 109 return cpp_test.returncode == 0 110 111 112class BuildExt(build_ext.build_ext): 113 """Custom build_ext command.""" 114 115 def get_ext_filename(self, ext_name): 116 # since python3.5, python extensions' shared libraries use a suffix that corresponds to the value 117 # of sysconfig.get_config_var('EXT_SUFFIX') and contains info about the architecture the library targets. 118 # E.g. on x64 linux the suffix is ".cpython-XYZ-x86_64-linux-gnu.so" 119 # When crosscompiling python wheels, we need to be able to override this suffix 120 # so that the resulting file name matches the target architecture and we end up with a well-formed 121 # wheel. 122 filename = build_ext.build_ext.get_ext_filename(self, ext_name) 123 orig_ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") 124 new_ext_suffix = os.getenv("GRPC_PYTHON_OVERRIDE_EXT_SUFFIX") 125 if new_ext_suffix and filename.endswith(orig_ext_suffix): 126 filename = filename[: -len(orig_ext_suffix)] + new_ext_suffix 127 return filename 128 129 def build_extensions(self): 130 # This special conditioning is here due to difference of compiler 131 # behavior in gcc and clang. The clang doesn't take --stdc++11 132 # flags but gcc does. Since the setuptools of Python only support 133 # all C or all C++ compilation, the mix of C and C++ will crash. 134 # *By default*, macOS and FreBSD use clang and Linux use gcc 135 # 136 # If we are not using a permissive compiler that's OK with being 137 # passed wrong std flags, swap out compile function by adding a filter 138 # for it. 139 old_compile = self.compiler._compile 140 141 def new_compile(obj, src, ext, cc_args, extra_postargs, pp_opts): 142 if src.endswith(".c"): 143 extra_postargs = [ 144 arg for arg in extra_postargs if "-std=c++" not in arg 145 ] 146 elif src.endswith(".cc") or src.endswith(".cpp"): 147 extra_postargs = [ 148 arg for arg in extra_postargs if "-std=c11" not in arg 149 ] 150 return old_compile(obj, src, ext, cc_args, extra_postargs, pp_opts) 151 152 self.compiler._compile = new_compile 153 154 build_ext.build_ext.build_extensions(self) 155 156 157# There are some situations (like on Windows) where CC, CFLAGS, and LDFLAGS are 158# entirely ignored/dropped/forgotten by distutils and its Cygwin/MinGW support. 159# We use these environment variables to thus get around that without locking 160# ourselves in w.r.t. the multitude of operating systems this ought to build on. 161# We can also use these variables as a way to inject environment-specific 162# compiler/linker flags. We assume GCC-like compilers and/or MinGW as a 163# reasonable default. 164EXTRA_ENV_COMPILE_ARGS = os.environ.get("GRPC_PYTHON_CFLAGS", None) 165EXTRA_ENV_LINK_ARGS = os.environ.get("GRPC_PYTHON_LDFLAGS", None) 166if EXTRA_ENV_COMPILE_ARGS is None: 167 EXTRA_ENV_COMPILE_ARGS = "" 168 if "win32" in sys.platform: 169 # MSVC by defaults uses C++14 so C11 needs to be specified. 170 EXTRA_ENV_COMPILE_ARGS += " /std:c11" 171 # We need to statically link the C++ Runtime, only the C runtime is 172 # available dynamically 173 EXTRA_ENV_COMPILE_ARGS += " /MT" 174 elif "linux" in sys.platform: 175 # GCC by defaults uses C17 so only C++14 needs to be specified. 176 EXTRA_ENV_COMPILE_ARGS += " -std=c++14" 177 EXTRA_ENV_COMPILE_ARGS += " -fno-wrapv -frtti" 178 # Reduce the optimization level from O3 (in many cases) to O1 to 179 # workaround gcc misalignment bug with MOVAPS (internal b/329134877) 180 EXTRA_ENV_COMPILE_ARGS += " -O1" 181 elif "darwin" in sys.platform: 182 # AppleClang by defaults uses C17 so only C++14 needs to be specified. 183 EXTRA_ENV_COMPILE_ARGS += " -std=c++14" 184 EXTRA_ENV_COMPILE_ARGS += " -fno-wrapv -frtti" 185 EXTRA_ENV_COMPILE_ARGS += " -stdlib=libc++ -DHAVE_UNISTD_H" 186if EXTRA_ENV_LINK_ARGS is None: 187 EXTRA_ENV_LINK_ARGS = "" 188 # This is needed for protobuf/main.cc 189 if "win32" in sys.platform: 190 EXTRA_ENV_LINK_ARGS += " Shell32.lib" 191 # NOTE(rbellevi): Clang on Mac OS will make all static symbols (both 192 # variables and objects) global weak symbols. When a process loads the 193 # protobuf wheel's shared object library before loading *this* C extension, 194 # the runtime linker will prefer the protobuf module's version of symbols. 195 # This results in the process using a mixture of symbols from the protobuf 196 # wheel and this wheel, which may be using different versions of 197 # libprotobuf. In the case that they *are* using different versions of 198 # libprotobuf *and* there has been a change in data layout (or in other 199 # invariants) segfaults, data corruption, or "bad things" may happen. 200 # 201 # This flag ensures that on Mac, the only global symbol is the one loaded by 202 # the Python interpreter. The problematic global weak symbols become local 203 # weak symbols. This is not required on Linux since the compiler does not 204 # produce global weak symbols. This is not required on Windows as our ".pyd" 205 # file does not contain any symbols. 206 # 207 # Finally, the leading underscore here is part of the Mach-O ABI. Unlike 208 # more modern ABIs (ELF et al.), Mach-O prepends an underscore to the names 209 # of C functions. 210 if "darwin" in sys.platform: 211 EXTRA_ENV_LINK_ARGS += " -Wl,-exported_symbol,_{}".format( 212 _EXT_INIT_SYMBOL 213 ) 214 if "linux" in sys.platform or "darwin" in sys.platform: 215 EXTRA_ENV_LINK_ARGS += " -lpthread" 216 if check_linker_need_libatomic(): 217 EXTRA_ENV_LINK_ARGS += " -latomic" 218 219# Explicitly link Core Foundation framework for MacOS to ensure no symbol is 220# missing when compiled using package managers like Conda. 221if "darwin" in sys.platform: 222 EXTRA_ENV_LINK_ARGS += " -framework CoreFoundation" 223 224EXTRA_COMPILE_ARGS = shlex.split(EXTRA_ENV_COMPILE_ARGS) 225EXTRA_LINK_ARGS = shlex.split(EXTRA_ENV_LINK_ARGS) 226 227if BUILD_WITH_STATIC_LIBSTDCXX: 228 EXTRA_LINK_ARGS.append("-static-libstdc++") 229 230CC_FILES = [os.path.normpath(cc_file) for cc_file in protoc_lib_deps.CC_FILES] 231PROTO_FILES = [ 232 os.path.normpath(proto_file) for proto_file in protoc_lib_deps.PROTO_FILES 233] 234CC_INCLUDES = [ 235 os.path.normpath(include_dir) for include_dir in protoc_lib_deps.CC_INCLUDES 236] 237PROTO_INCLUDE = os.path.normpath(protoc_lib_deps.PROTO_INCLUDE) 238 239GRPC_PYTHON_TOOLS_PACKAGE = "grpc_tools" 240GRPC_PYTHON_PROTO_RESOURCES_NAME = "_proto" 241 242DEFINE_MACROS = () 243if "win32" in sys.platform: 244 DEFINE_MACROS += ( 245 ("WIN32_LEAN_AND_MEAN", 1), 246 # avoid https://github.com/abseil/abseil-cpp/issues/1425 247 ("NOMINMAX", 1), 248 ) 249 if "64bit" in platform.architecture()[0]: 250 DEFINE_MACROS += (("MS_WIN64", 1),) 251elif "linux" in sys.platform or "darwin" in sys.platform: 252 DEFINE_MACROS += (("HAVE_PTHREAD", 1),) 253 254 255def package_data(): 256 tools_path = GRPC_PYTHON_TOOLS_PACKAGE.replace(".", os.path.sep) 257 proto_resources_path = os.path.join( 258 tools_path, GRPC_PYTHON_PROTO_RESOURCES_NAME 259 ) 260 proto_files = [] 261 for proto_file in PROTO_FILES: 262 source = os.path.join(PROTO_INCLUDE, proto_file) 263 target = os.path.join(proto_resources_path, proto_file) 264 relative_target = os.path.join( 265 GRPC_PYTHON_PROTO_RESOURCES_NAME, proto_file 266 ) 267 try: 268 os.makedirs(os.path.dirname(target)) 269 except OSError as error: 270 if error.errno == errno.EEXIST: 271 pass 272 else: 273 raise 274 shutil.copy(source, target) 275 proto_files.append(relative_target) 276 return {GRPC_PYTHON_TOOLS_PACKAGE: proto_files} 277 278 279def extension_modules(): 280 if BUILD_WITH_CYTHON: 281 plugin_sources = [os.path.join("grpc_tools", "_protoc_compiler.pyx")] 282 else: 283 plugin_sources = [os.path.join("grpc_tools", "_protoc_compiler.cpp")] 284 285 plugin_sources += [ 286 os.path.join("grpc_tools", "main.cc"), 287 os.path.join("grpc_root", "src", "compiler", "python_generator.cc"), 288 os.path.join("grpc_root", "src", "compiler", "proto_parser_helper.cc"), 289 ] + CC_FILES 290 291 plugin_ext = Extension( 292 name="grpc_tools._protoc_compiler", 293 sources=plugin_sources, 294 include_dirs=[ 295 ".", 296 "grpc_root", 297 os.path.join("grpc_root", "include"), 298 ] 299 + CC_INCLUDES, 300 define_macros=list(DEFINE_MACROS), 301 extra_compile_args=list(EXTRA_COMPILE_ARGS), 302 extra_link_args=list(EXTRA_LINK_ARGS), 303 ) 304 extensions = [plugin_ext] 305 if BUILD_WITH_CYTHON: 306 from Cython import Build 307 308 return Build.cythonize(extensions) 309 else: 310 return extensions 311 312 313setuptools.setup( 314 name="grpcio-tools", 315 version=grpc_version.VERSION, 316 description="Protobuf code generator for gRPC", 317 long_description_content_type="text/x-rst", 318 long_description=open(_README_PATH, "r").read(), 319 author="The gRPC Authors", 320 author_email="[email protected]", 321 url="https://grpc.io", 322 project_urls={ 323 "Source Code": "https://github.com/grpc/grpc/tree/master/tools/distrib/python/grpcio_tools", 324 "Bug Tracker": "https://github.com/grpc/grpc/issues", 325 }, 326 license="Apache License 2.0", 327 classifiers=CLASSIFIERS, 328 ext_modules=extension_modules(), 329 packages=setuptools.find_packages("."), 330 python_requires=">=3.8", 331 install_requires=[ 332 "protobuf>=5.26.1,<6.0dev", 333 "grpcio>={version}".format(version=grpc_version.VERSION), 334 "setuptools", 335 ], 336 package_data=package_data(), 337 cmdclass={ 338 "build_ext": BuildExt, 339 }, 340) 341