xref: /aosp_15_r20/external/grpc-grpc/tools/distrib/python/grpcio_tools/setup.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
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