1# SPDX-License-Identifier: GPL-2.0 2"""Setup script for perf python extension. 3 4This script is used to build and install the perf python binding. 5It handles compiler-specific flags, especially for clang, and configures 6the setuptools Extension. 7""" 8 9import os 10import shlex 11import shutil 12import subprocess 13import sysconfig 14 15from setuptools import setup, Extension 16from setuptools.command.build_ext import build_ext as _build_ext 17from setuptools.command.install_lib import install_lib as _install_lib 18 19 20def clang_has_option(cc: str, cc_args: list[str], src_feature_tests: str, option: str) -> bool: 21 """Check if clang supports a specific option. 22 23 Args: 24 cc: The compiler executable. 25 cc_args: Compiler arguments from CC environment variable. 26 src_feature_tests: Path to the feature tests directory. 27 option: The compiler option to check (e.g., "-mcet"). 28 29 Returns: 30 True if the option is supported, False otherwise. 31 """ 32 error_substrings = ( 33 b"unknown argument", 34 b"is not supported", 35 b"unknown warning option" 36 ) 37 cmd = [cc] + cc_args + [ 38 option, 39 "-o", "/dev/null", 40 os.path.join(src_feature_tests, "test-hello.c") 41 ] 42 try: 43 res = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=False) 44 cc_output = res.stderr.splitlines() 45 except OSError: 46 return False 47 return not any(any(error in line for error in error_substrings) for line in cc_output) 48 49 50def filter_clang_options(cc: str, cc_args: list[str], src_feature_tests: str) -> None: 51 """Filter out unsupported clang options from sysconfig CFLAGS and OPT. 52 53 Args: 54 cc: The compiler executable. 55 cc_args: Compiler arguments from CC environment variable. 56 src_feature_tests: Path to the feature tests directory. 57 """ 58 config_vars = sysconfig.get_config_vars() 59 for var in ('CFLAGS', 'OPT'): 60 if var not in config_vars: 61 continue 62 63 # Split into individual flags using shlex to preserve quoted arguments 64 flags = shlex.split(config_vars[var]) 65 66 # Remove -specs=... 67 flags = [f for f in flags if not f.startswith("-specs=")] 68 69 options = ( 70 "-mcet", 71 "-fcf-protection", 72 "-fstack-clash-protection", 73 "-fstack-protector-strong", 74 "-fno-semantic-interposition", 75 "-ffat-lto-objects", 76 "-ftree-loop-distribute-patterns", 77 "-gno-variable-location-views" 78 ) 79 for option in options: 80 if not clang_has_option(cc, cc_args, src_feature_tests, option): 81 # Remove the option and any variant (e.g. -option=...) 82 flags = [f for f in flags if not f.startswith(option)] 83 84 # Re-join flags preserving quoting 85 config_vars[var] = shlex.join(flags) 86 87 88class BuildExt(_build_ext): 89 """Custom build_ext command to set output directories.""" 90 91 def __init__(self, *args, **kwargs): 92 self.build_lib = None 93 self.build_temp = None 94 super().__init__(*args, **kwargs) 95 96 def finalize_options(self) -> None: 97 _build_ext.finalize_options(self) 98 build_lib = os.getenv('PYTHON_EXTBUILD_LIB') 99 build_tmp = os.getenv('PYTHON_EXTBUILD_TMP') 100 if build_lib: 101 self.build_lib = build_lib 102 if build_tmp: 103 self.build_temp = build_tmp 104 105 106class InstallLib(_install_lib): 107 """Custom install_lib command to set output directory.""" 108 109 def __init__(self, *args, **kwargs): 110 self.build_dir = None 111 super().__init__(*args, **kwargs) 112 113 def finalize_options(self) -> None: 114 _install_lib.finalize_options(self) 115 build_lib = os.getenv('PYTHON_EXTBUILD_LIB') 116 if build_lib: 117 self.build_dir = build_lib 118 119 def run(self): 120 _install_lib.run(self) 121 srctree = os.getenv('srctree', '.') 122 src_perf = os.path.join(srctree, 'tools/perf') 123 shutil.copy2(os.path.join(src_perf, 'python/perf.pyi'), self.install_dir) 124 125 126def main() -> None: 127 """Main entry point for the setup script.""" 128 cc_env = os.getenv("CC") 129 assert cc_env, "Environment variable CC not set" 130 131 # Safe parsing of CC environment variable which might contain options/quotes 132 cc_tokens = shlex.split(cc_env) 133 cc = cc_tokens[0] 134 cc_args = cc_tokens[1:] 135 136 # Run CC -v to check if it is clang. 137 try: 138 cc_info = subprocess.run( 139 [cc, "-v"], stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=False 140 ) 141 cc_is_clang = b"clang version" in cc_info.stderr 142 except OSError as e: 143 raise RuntimeError(f"Failed to execute compiler '{cc}': {e}") from e 144 145 srctree = os.getenv('srctree') 146 assert srctree, "Environment variable srctree, for the Linux sources, not set" 147 src_feature_tests = f'{srctree}/tools/build/feature' 148 149 if cc_is_clang: 150 filter_clang_options(cc, cc_args, src_feature_tests) 151 152 # switch off several checks (need to be at the end of cflags list) 153 cflags = [ 154 '-fno-strict-aliasing', 155 '-Wno-write-strings', 156 '-Wno-unused-parameter', 157 '-Wno-redundant-decls' 158 ] 159 if cc_is_clang: 160 cflags += ["-Wno-unused-command-line-argument"] 161 if clang_has_option( 162 cc, cc_args, src_feature_tests, "-Wno-cast-function-type-mismatch" 163 ): 164 cflags += ["-Wno-cast-function-type-mismatch"] 165 else: 166 cflags += ['-Wno-cast-function-type'] 167 168 # The python headers have mixed code with declarations (decls after asserts, for instance) 169 cflags += ["-Wno-declaration-after-statement"] 170 171 src_perf = f'{srctree}/tools/perf' 172 173 perf = Extension( 174 'perf', 175 sources=[os.path.join(src_perf, 'util/python.c')], 176 include_dirs=['util/include'], 177 extra_compile_args=cflags, 178 ) 179 180 setup( 181 name='perf', 182 version='0.1', 183 description='Interface with the Linux profiling infrastructure', 184 author='Arnaldo Carvalho de Melo', 185 author_email='acme@redhat.com', 186 license='GPLv2', 187 url='http://perf.wiki.kernel.org', 188 ext_modules=[perf], 189 cmdclass={'build_ext': BuildExt, 'install_lib': InstallLib}, 190 ) 191 192 193if __name__ == '__main__': 194 main() 195