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