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