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