• 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
39from typing import List, Dict, Tuple
40
41_ARM_COMPILER_PREFIX = 'arm-none-eabi'
42_ARM_COMPILER_NAME = _ARM_COMPILER_PREFIX + '-gcc'
43
44
45def _parse_args() -> argparse.Namespace:
46    """Parses arguments for this script, splitting out the command to run."""
47
48    parser = argparse.ArgumentParser(description=__doc__)
49    parser.add_argument(
50        '--gn-scope',
51        action='store_true',
52        help=("Formats the output like a GN scope so it can be ingested by "
53              "exec_script()"))
54    parser.add_argument('--cflags',
55                        action='store_true',
56                        help=('Include necessary C flags in the output'))
57    parser.add_argument('--ldflags',
58                        action='store_true',
59                        help=('Include necessary linker flags in the output'))
60    parser.add_argument(
61        'clang_flags',
62        nargs=argparse.REMAINDER,
63        help='Flags to pass to clang, which can affect library/include paths',
64    )
65    parsed_args = parser.parse_args()
66
67    assert parsed_args.clang_flags[0] == '--', 'arguments not correctly split'
68    parsed_args.clang_flags = parsed_args.clang_flags[1:]
69    return parsed_args
70
71
72def _compiler_info_command(print_command: str, cflags: List[str]) -> str:
73    command = [_ARM_COMPILER_NAME]
74    command.extend(cflags)
75    command.append(print_command)
76    result = subprocess.run(
77        command,
78        stdout=subprocess.PIPE,
79        stderr=subprocess.STDOUT,
80    )
81    result.check_returncode()
82    return result.stdout.decode().rstrip()
83
84
85def get_gcc_lib_dir(cflags: List[str]) -> Path:
86    return Path(_compiler_info_command('-print-libgcc-file-name',
87                                       cflags)).parent
88
89
90def get_compiler_info(cflags: List[str]) -> Dict[str, str]:
91    compiler_info: Dict[str, str] = {}
92    compiler_info['gcc_libs_dir'] = os.path.relpath(
93        str(get_gcc_lib_dir(cflags)), ".")
94    compiler_info['sysroot'] = os.path.relpath(
95        _compiler_info_command('-print-sysroot', cflags), ".")
96    compiler_info['version'] = _compiler_info_command('-dumpversion', cflags)
97    compiler_info['multi_dir'] = _compiler_info_command(
98        '-print-multi-directory', cflags)
99    return compiler_info
100
101
102def get_cflags(compiler_info: Dict[str, str]):
103    # TODO(amontanez): Make newlib-nano optional.
104    cflags = [
105        # TODO(amontanez): For some reason, -stdlib++-isystem and
106        # -isystem-after work, but emit unused argument errors. This is the only
107        # way to let the build succeed.
108        '-Qunused-arguments',
109        # Disable all default libraries.
110        "-nodefaultlibs",
111        '--target=arm-none-eabi'
112    ]
113
114    # Add sysroot info.
115    cflags.extend((
116        '--sysroot=' + compiler_info['sysroot'],
117        '-isystem' +
118        str(Path(compiler_info['sysroot']) / 'include' / 'newlib-nano'),
119        # This must be included after Clang's builtin headers.
120        '-isystem-after' + str(Path(compiler_info['sysroot']) / 'include'),
121        '-stdlib++-isystem' + str(
122            Path(compiler_info['sysroot']) / 'include' / 'c++' /
123            compiler_info['version']),
124        '-isystem' + str(
125            Path(compiler_info['sysroot']) / 'include' / 'c++' /
126            compiler_info['version'] / _ARM_COMPILER_PREFIX /
127            compiler_info['multi_dir']),
128    ))
129
130    return cflags
131
132
133def get_crt_objs(compiler_info: Dict[str, str]) -> Tuple[str, ...]:
134    return (
135        str(Path(compiler_info['gcc_libs_dir']) / 'crtfastmath.o'),
136        str(Path(compiler_info['gcc_libs_dir']) / 'crti.o'),
137        str(Path(compiler_info['gcc_libs_dir']) / 'crtn.o'),
138        str(
139            Path(compiler_info['sysroot']) / 'lib' /
140            compiler_info['multi_dir'] / 'crt0.o'),
141    )
142
143
144def get_ldflags(compiler_info: Dict[str, str]) -> List[str]:
145    ldflags: List[str] = [
146        '-lnosys',
147        # Add library search paths.
148        '-L' + compiler_info['gcc_libs_dir'],
149        '-L' + str(
150            Path(compiler_info['sysroot']) / 'lib' /
151            compiler_info['multi_dir']),
152        # Add libraries to link.
153        '-lc_nano',
154        '-lm',
155        '-lgcc',
156        '-lstdc++_nano',
157    ]
158
159    # Add C runtime object files.
160    objs = get_crt_objs(compiler_info)
161    ldflags.extend(objs)
162
163    return ldflags
164
165
166def main(
167    cflags: bool,
168    ldflags: bool,
169    gn_scope: bool,
170    clang_flags: List[str],
171) -> int:
172    """Script entry point."""
173    compiler_info = get_compiler_info(clang_flags)
174    if ldflags:
175        ldflag_list = get_ldflags(compiler_info)
176
177    if cflags:
178        cflag_list = get_cflags(compiler_info)
179
180    if not gn_scope:
181        flags = []
182        if cflags:
183            flags.extend(cflag_list)
184        if ldflags:
185            flags.extend(ldflag_list)
186        print(' '.join(flags))
187        return 0
188
189    if cflags:
190        print('cflags = [')
191        for flag in cflag_list:
192            print(f'  "{flag}",')
193        print(']')
194
195    if ldflags:
196        print('ldflags = [')
197        for flag in ldflag_list:
198            print(f'  "{flag}",')
199        print(']')
200    return 0
201
202
203if __name__ == '__main__':
204    sys.exit(main(**vars(_parse_args())))
205