xref: /aosp_15_r20/external/grpc-grpc/src/python/grpcio_observability/setup.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1# Copyright 2023 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 os
16import os.path
17import platform
18import re
19import shlex
20import subprocess
21from subprocess import PIPE
22import sys
23import sysconfig
24
25import setuptools
26from setuptools import Extension
27from setuptools.command import build_ext
28
29PYTHON_STEM = os.path.realpath(os.path.dirname(__file__))
30README_PATH = os.path.join(PYTHON_STEM, "README.rst")
31
32os.chdir(os.path.dirname(os.path.abspath(__file__)))
33sys.path.insert(0, os.path.abspath("."))
34
35import _parallel_compile_patch
36import observability_lib_deps
37
38import grpc_version
39
40_parallel_compile_patch.monkeypatch_compile_maybe()
41
42CLASSIFIERS = [
43    "Development Status :: 4 - Beta",
44    "Programming Language :: Python",
45    "Programming Language :: Python :: 3",
46    "License :: OSI Approved :: Apache Software License",
47]
48
49O11Y_CC_SRCS = [
50    "server_call_tracer.cc",
51    "client_call_tracer.cc",
52    "observability_util.cc",
53    "python_census_context.cc",
54    "sampler.cc",
55    "rpc_encoding.cc",
56]
57
58
59def _env_bool_value(env_name, default):
60    """Parses a bool option from an environment variable"""
61    return os.environ.get(env_name, default).upper() not in ["FALSE", "0", ""]
62
63
64def _is_alpine():
65    """Checks if it's building Alpine"""
66    os_release_content = ""
67    try:
68        with open("/etc/os-release", "r") as f:
69            os_release_content = f.read()
70        if "alpine" in os_release_content:
71            return True
72    except Exception:
73        return False
74
75
76# Environment variable to determine whether or not the Cython extension should
77# *use* Cython or use the generated C files. Note that this requires the C files
78# to have been generated by building first *with* Cython support.
79BUILD_WITH_CYTHON = _env_bool_value("GRPC_PYTHON_BUILD_WITH_CYTHON", "False")
80
81# Export this variable to force building the python extension with a statically linked libstdc++.
82# At least on linux, this is normally not needed as we can build manylinux-compatible wheels on linux just fine
83# without statically linking libstdc++ (which leads to a slight increase in the wheel size).
84# This option is useful when crosscompiling wheels for aarch64 where
85# it's difficult to ensure that the crosscompilation toolchain has a high-enough version
86# of GCC (we require >=5.1) but still uses old-enough libstdc++ symbols.
87# TODO(jtattermusch): remove this workaround once issues with crosscompiler version are resolved.
88BUILD_WITH_STATIC_LIBSTDCXX = _env_bool_value(
89    "GRPC_PYTHON_BUILD_WITH_STATIC_LIBSTDCXX", "False"
90)
91
92
93def check_linker_need_libatomic():
94    """Test if linker on system needs libatomic."""
95    code_test = (
96        b"#include <atomic>\n"
97        + b"int main() { return std::atomic<int64_t>{}; }"
98    )
99    cxx = shlex.split(os.environ.get("CXX", "c++"))
100    cpp_test = subprocess.Popen(
101        cxx + ["-x", "c++", "-std=c++14", "-"],
102        stdin=PIPE,
103        stdout=PIPE,
104        stderr=PIPE,
105    )
106    cpp_test.communicate(input=code_test)
107    if cpp_test.returncode == 0:
108        return False
109    # Double-check to see if -latomic actually can solve the problem.
110    # https://github.com/grpc/grpc/issues/22491
111    cpp_test = subprocess.Popen(
112        cxx + ["-x", "c++", "-std=c++14", "-", "-latomic"],
113        stdin=PIPE,
114        stdout=PIPE,
115        stderr=PIPE,
116    )
117    cpp_test.communicate(input=code_test)
118    return cpp_test.returncode == 0
119
120
121class BuildExt(build_ext.build_ext):
122    """Custom build_ext command."""
123
124    def get_ext_filename(self, ext_name):
125        # since python3.5, python extensions' shared libraries use a suffix that corresponds to the value
126        # of sysconfig.get_config_var('EXT_SUFFIX') and contains info about the architecture the library targets.
127        # E.g. on x64 linux the suffix is ".cpython-XYZ-x86_64-linux-gnu.so"
128        # When crosscompiling python wheels, we need to be able to override this suffix
129        # so that the resulting file name matches the target architecture and we end up with a well-formed
130        # wheel.
131        filename = build_ext.build_ext.get_ext_filename(self, ext_name)
132        orig_ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
133        new_ext_suffix = os.getenv("GRPC_PYTHON_OVERRIDE_EXT_SUFFIX")
134        if new_ext_suffix and filename.endswith(orig_ext_suffix):
135            filename = filename[: -len(orig_ext_suffix)] + new_ext_suffix
136        return filename
137
138
139# There are some situations (like on Windows) where CC, CFLAGS, and LDFLAGS are
140# entirely ignored/dropped/forgotten by distutils and its Cygwin/MinGW support.
141# We use these environment variables to thus get around that without locking
142# ourselves in w.r.t. the multitude of operating systems this ought to build on.
143# We can also use these variables as a way to inject environment-specific
144# compiler/linker flags. We assume GCC-like compilers and/or MinGW as a
145# reasonable default.
146EXTRA_ENV_COMPILE_ARGS = os.environ.get("GRPC_PYTHON_CFLAGS", None)
147EXTRA_ENV_LINK_ARGS = os.environ.get("GRPC_PYTHON_LDFLAGS", None)
148if EXTRA_ENV_COMPILE_ARGS is None:
149    EXTRA_ENV_COMPILE_ARGS = "-std=c++14"
150    if "win32" in sys.platform:
151        # We need to statically link the C++ Runtime, only the C runtime is
152        # available dynamically
153        EXTRA_ENV_COMPILE_ARGS += " /MT"
154    elif "linux" in sys.platform or "darwin" in sys.platform:
155        EXTRA_ENV_COMPILE_ARGS += " -fno-wrapv -frtti -fvisibility=hidden"
156
157if EXTRA_ENV_LINK_ARGS is None:
158    EXTRA_ENV_LINK_ARGS = ""
159    if "linux" in sys.platform or "darwin" in sys.platform:
160        EXTRA_ENV_LINK_ARGS += " -lpthread"
161        if check_linker_need_libatomic():
162            EXTRA_ENV_LINK_ARGS += " -latomic"
163
164# This enables the standard link-time optimizer, which help us prevent some undefined symbol errors by
165# remove some unused symbols from .so file.
166# Note that it does not work for MSCV on windows.
167if "win32" not in sys.platform:
168    EXTRA_ENV_COMPILE_ARGS += " -flto"
169    # Compile with fail with error: `lto-wrapper failed` when lto flag was enabled in Alpine using musl libc.
170    # As a work around we need to disable ipa-cp.
171    if _is_alpine():
172        EXTRA_ENV_COMPILE_ARGS += " -fno-ipa-cp"
173
174EXTRA_COMPILE_ARGS = shlex.split(EXTRA_ENV_COMPILE_ARGS)
175EXTRA_LINK_ARGS = shlex.split(EXTRA_ENV_LINK_ARGS)
176
177if BUILD_WITH_STATIC_LIBSTDCXX:
178    EXTRA_LINK_ARGS.append("-static-libstdc++")
179
180CC_FILES = [
181    os.path.normpath(cc_file) for cc_file in observability_lib_deps.CC_FILES
182]
183CC_INCLUDES = [
184    os.path.normpath(include_dir)
185    for include_dir in observability_lib_deps.CC_INCLUDES
186]
187
188DEFINE_MACROS = (("_WIN32_WINNT", 0x600),)
189
190if "win32" in sys.platform:
191    DEFINE_MACROS += (
192        ("WIN32_LEAN_AND_MEAN", 1),
193        ("CARES_STATICLIB", 1),
194        ("GRPC_ARES", 0),
195        ("NTDDI_VERSION", 0x06000000),
196        # avoid https://github.com/abseil/abseil-cpp/issues/1425
197        ("NOMINMAX", 1),
198    )
199    if "64bit" in platform.architecture()[0]:
200        DEFINE_MACROS += (("MS_WIN64", 1),)
201    else:
202        # For some reason, this is needed to get access to inet_pton/inet_ntop
203        # on msvc, but only for 32 bits
204        DEFINE_MACROS += (("NTDDI_VERSION", 0x06000000),)
205elif "linux" in sys.platform or "darwin" in sys.platform:
206    DEFINE_MACROS += (("HAVE_PTHREAD", 1),)
207
208# Fix for Cython build issue in aarch64.
209# It's required to define this macro before include <inttypes.h>.
210# <inttypes.h> was included in core/lib/channel/call_tracer.h.
211# This macro should already be defined in grpc/grpc.h through port_platform.h,
212# but we're still having issue in aarch64, so we manually define the macro here.
213# TODO(xuanwn): Figure out what's going on in the aarch64 build so we can support
214# gcc + Bazel.
215DEFINE_MACROS += (("__STDC_FORMAT_MACROS", None),)
216
217
218# Use `-fvisibility=hidden` will hide cython init symbol, we need that symbol exported
219# in order to import cython module.
220if "linux" in sys.platform or "darwin" in sys.platform:
221    pymodinit = 'extern "C" __attribute__((visibility ("default"))) PyObject*'
222    DEFINE_MACROS += (("PyMODINIT_FUNC", pymodinit),)
223
224
225def extension_modules():
226    if BUILD_WITH_CYTHON:
227        cython_module_files = [
228            os.path.join("grpc_observability", "_cyobservability.pyx")
229        ]
230    else:
231        cython_module_files = [
232            os.path.join("grpc_observability", "_cyobservability.cpp")
233        ]
234
235    plugin_include = [
236        ".",
237        "grpc_root",
238        os.path.join("grpc_root", "include"),
239    ] + CC_INCLUDES
240
241    plugin_sources = CC_FILES
242
243    O11Y_CC_PATHS = (
244        os.path.join("grpc_observability", f) for f in O11Y_CC_SRCS
245    )
246    plugin_sources += O11Y_CC_PATHS
247
248    plugin_sources += cython_module_files
249
250    plugin_ext = Extension(
251        name="grpc_observability._cyobservability",
252        sources=plugin_sources,
253        include_dirs=plugin_include,
254        language="c++",
255        define_macros=list(DEFINE_MACROS),
256        extra_compile_args=list(EXTRA_COMPILE_ARGS),
257        extra_link_args=list(EXTRA_LINK_ARGS),
258    )
259    extensions = [plugin_ext]
260    if BUILD_WITH_CYTHON:
261        from Cython import Build
262
263        return Build.cythonize(
264            extensions, compiler_directives={"language_level": "3"}
265        )
266    else:
267        return extensions
268
269
270PACKAGES = setuptools.find_packages(PYTHON_STEM)
271
272setuptools.setup(
273    name="grpcio-observability",
274    version=grpc_version.VERSION,
275    description="gRPC Python observability package",
276    long_description_content_type="text/x-rst",
277    long_description=open(README_PATH, "r").read(),
278    author="The gRPC Authors",
279    author_email="[email protected]",
280    url="https://grpc.io",
281    project_urls={
282        "Source Code": "https://github.com/grpc/grpc/tree/master/src/python/grpcio_observability",
283        "Bug Tracker": "https://github.com/grpc/grpc/issues",
284    },
285    license="Apache License 2.0",
286    classifiers=CLASSIFIERS,
287    ext_modules=extension_modules(),
288    packages=list(PACKAGES),
289    python_requires=">=3.8",
290    install_requires=[
291        "grpcio=={version}".format(version=grpc_version.VERSION),
292        "setuptools>=59.6.0",
293        "opentelemetry-api==1.21.0",
294    ],
295    cmdclass={
296        "build_ext": BuildExt,
297    },
298)
299