xref: /aosp_15_r20/external/pigweed/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1#!/usr/bin/env python3
2# Copyright 2021 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""Generates flags needed for an ARM build using clang.
16
17Using clang on Cortex-M cores isn't intuitive as the end-to-end experience isn't
18quite completely in LLVM. LLVM doesn't yet provide compatible C runtime
19libraries or C/C++ standard libraries. To work around this, this script pulls
20the missing bits from an arm-none-eabi-gcc compiler on the system path. This
21lets clang do the heavy lifting while only relying on some headers provided by
22newlib/arm-none-eabi-gcc in addition to a small assortment of needed libraries.
23
24To use this script, specify what flags you want from the script, and run with
25the required architecture flags like you would with gcc:
26
27  python -m pw_toolchain.clang_arm_toolchain --cflags -- -mthumb -mcpu=cortex-m3
28
29The script will then print out the additional flags you need to pass to clang to
30get a working build.
31"""
32
33import argparse
34import sys
35import os
36import subprocess
37
38from pathlib import Path
39
40_ARM_COMPILER_PREFIX = 'arm-none-eabi'
41_ARM_COMPILER_NAME = _ARM_COMPILER_PREFIX + '-gcc'
42
43
44def _parse_args() -> argparse.Namespace:
45    """Parses arguments for this script, splitting out the command to run."""
46
47    parser = argparse.ArgumentParser(description=__doc__)
48    parser.add_argument(
49        '--gn-scope',
50        action='store_true',
51        help=(
52            "Formats the output like a GN scope so it can be ingested by "
53            "exec_script()"
54        ),
55    )
56    parser.add_argument(
57        '--cflags',
58        action='store_true',
59        help=('Include necessary C flags in the output'),
60    )
61    parser.add_argument(
62        '--ldflags',
63        action='store_true',
64        help=('Include necessary linker flags in the output'),
65    )
66    parser.add_argument(
67        'clang_flags',
68        nargs=argparse.REMAINDER,
69        help='Flags to pass to clang, which can affect library/include paths',
70    )
71    parsed_args = parser.parse_args()
72
73    assert parsed_args.clang_flags[0] == '--', 'arguments not correctly split'
74    parsed_args.clang_flags = parsed_args.clang_flags[1:]
75    return parsed_args
76
77
78def _compiler_info_command(print_command: str, cflags: list[str]) -> str:
79    command = [_ARM_COMPILER_NAME]
80    command.extend(cflags)
81    command.append(print_command)
82    result = subprocess.run(
83        command,
84        stdout=subprocess.PIPE,
85        stderr=subprocess.STDOUT,
86    )
87    result.check_returncode()
88    return result.stdout.decode().rstrip()
89
90
91def get_gcc_lib_dir(cflags: list[str]) -> Path:
92    return Path(
93        _compiler_info_command('-print-libgcc-file-name', cflags)
94    ).parent
95
96
97def get_compiler_info(cflags: list[str]) -> dict[str, str]:
98    compiler_info: dict[str, str] = {}
99    compiler_info['gcc_libs_dir'] = os.path.relpath(
100        str(get_gcc_lib_dir(cflags)), "."
101    )
102    compiler_info['sysroot'] = os.path.relpath(
103        _compiler_info_command('-print-sysroot', cflags), "."
104    )
105    compiler_info['version'] = _compiler_info_command('-dumpversion', cflags)
106    compiler_info['multi_dir'] = _compiler_info_command(
107        '-print-multi-directory', cflags
108    )
109    return compiler_info
110
111
112def get_cflags(compiler_info: dict[str, str]):
113    """TODO(amontanez): Add docstring."""
114    # TODO(amontanez): Make newlib-nano optional.
115    cflags = [
116        # TODO(amontanez): For some reason, -stdlib++-isystem and
117        # -isystem-after work, but emit unused argument errors. This is the only
118        # way to let the build succeed.
119        '-Qunused-arguments',
120        # Disable all default libraries.
121        "-nodefaultlibs",
122        # Exclude start files from included in the link.
123        "-nostartfiles",
124        '--target=arm-none-eabi',
125    ]
126
127    # Add sysroot info.
128    cflags.extend(
129        (
130            '--sysroot=' + compiler_info['sysroot'],
131            '-isystem'
132            + str(Path(compiler_info['sysroot']) / 'include' / 'newlib-nano'),
133            # This must be included after Clang's builtin headers.
134            '-isystem-after' + str(Path(compiler_info['sysroot']) / 'include'),
135            '-stdlib++-isystem'
136            + str(
137                Path(compiler_info['sysroot'])
138                / 'include'
139                / 'c++'
140                / compiler_info['version']
141            ),
142            '-isystem'
143            + str(
144                Path(compiler_info['sysroot'])
145                / 'include'
146                / 'c++'
147                / compiler_info['version']
148                / _ARM_COMPILER_PREFIX
149                / compiler_info['multi_dir']
150            ),
151        )
152    )
153
154    return cflags
155
156
157def get_crt_objs(compiler_info: dict[str, str]) -> tuple[str, ...]:
158    return (
159        str(Path(compiler_info['gcc_libs_dir']) / 'crtfastmath.o'),
160        str(Path(compiler_info['gcc_libs_dir']) / 'crti.o'),
161        str(Path(compiler_info['gcc_libs_dir']) / 'crtn.o'),
162        str(
163            Path(compiler_info['sysroot'])
164            / 'lib'
165            / compiler_info['multi_dir']
166            / 'crt0.o'
167        ),
168    )
169
170
171def get_ldflags(compiler_info: dict[str, str]) -> list[str]:
172    ldflags: list[str] = [
173        # Add library search paths.
174        '-L' + compiler_info['gcc_libs_dir'],
175        '-L'
176        + str(
177            Path(compiler_info['sysroot']) / 'lib' / compiler_info['multi_dir']
178        ),
179    ]
180
181    # Add C runtime object files.
182    objs = get_crt_objs(compiler_info)
183    ldflags.extend(objs)
184
185    return ldflags
186
187
188def main(
189    cflags: bool,
190    ldflags: bool,
191    gn_scope: bool,
192    clang_flags: list[str],
193) -> int:
194    """Script entry point."""
195    compiler_info = get_compiler_info(clang_flags)
196    if ldflags:
197        ldflag_list = get_ldflags(compiler_info)
198
199    if cflags:
200        cflag_list = get_cflags(compiler_info)
201
202    if not gn_scope:
203        flags = []
204        if cflags:
205            flags.extend(cflag_list)
206        if ldflags:
207            flags.extend(ldflag_list)
208        print(' '.join(flags))
209        return 0
210
211    if cflags:
212        print('cflags = [')
213        for flag in cflag_list:
214            print(f'  "{flag}",')
215        print(']')
216
217    if ldflags:
218        print('ldflags = [')
219        for flag in ldflag_list:
220            print(f'  "{flag}",')
221        print(']')
222    return 0
223
224
225if __name__ == '__main__':
226    sys.exit(main(**vars(_parse_args())))
227