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