• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        '--target=arm-none-eabi',
123    ]
124
125    # Add sysroot info.
126    cflags.extend(
127        (
128            '--sysroot=' + compiler_info['sysroot'],
129            '-isystem'
130            + str(Path(compiler_info['sysroot']) / 'include' / 'newlib-nano'),
131            # This must be included after Clang's builtin headers.
132            '-isystem-after' + str(Path(compiler_info['sysroot']) / 'include'),
133            '-stdlib++-isystem'
134            + str(
135                Path(compiler_info['sysroot'])
136                / 'include'
137                / 'c++'
138                / compiler_info['version']
139            ),
140            '-isystem'
141            + str(
142                Path(compiler_info['sysroot'])
143                / 'include'
144                / 'c++'
145                / compiler_info['version']
146                / _ARM_COMPILER_PREFIX
147                / compiler_info['multi_dir']
148            ),
149        )
150    )
151
152    return cflags
153
154
155def get_crt_objs(compiler_info: dict[str, str]) -> tuple[str, ...]:
156    return (
157        str(Path(compiler_info['gcc_libs_dir']) / 'crtfastmath.o'),
158        str(Path(compiler_info['gcc_libs_dir']) / 'crti.o'),
159        str(Path(compiler_info['gcc_libs_dir']) / 'crtn.o'),
160        str(
161            Path(compiler_info['sysroot'])
162            / 'lib'
163            / compiler_info['multi_dir']
164            / 'crt0.o'
165        ),
166    )
167
168
169def get_ldflags(compiler_info: dict[str, str]) -> list[str]:
170    ldflags: list[str] = [
171        # Add library search paths.
172        '-L' + compiler_info['gcc_libs_dir'],
173        '-L'
174        + str(
175            Path(compiler_info['sysroot']) / 'lib' / compiler_info['multi_dir']
176        ),
177    ]
178
179    # Add C runtime object files.
180    objs = get_crt_objs(compiler_info)
181    ldflags.extend(objs)
182
183    return ldflags
184
185
186def main(
187    cflags: bool,
188    ldflags: bool,
189    gn_scope: bool,
190    clang_flags: list[str],
191) -> int:
192    """Script entry point."""
193    compiler_info = get_compiler_info(clang_flags)
194    if ldflags:
195        ldflag_list = get_ldflags(compiler_info)
196
197    if cflags:
198        cflag_list = get_cflags(compiler_info)
199
200    if not gn_scope:
201        flags = []
202        if cflags:
203            flags.extend(cflag_list)
204        if ldflags:
205            flags.extend(ldflag_list)
206        print(' '.join(flags))
207        return 0
208
209    if cflags:
210        print('cflags = [')
211        for flag in cflag_list:
212            print(f'  "{flag}",')
213        print(']')
214
215    if ldflags:
216        print('ldflags = [')
217        for flag in ldflag_list:
218            print(f'  "{flag}",')
219        print(']')
220    return 0
221
222
223if __name__ == '__main__':
224    sys.exit(main(**vars(_parse_args())))
225