xref: /linux/tools/perf/util/setup.py (revision 473f6c8f437b049f8ec015d57cd59bb983b1d85c)
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