1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Finds files for a given product.""" 15 16from typing import Any, List, Optional, Set, Tuple 17 18import pathlib 19import re 20 21 22def parse_product_str(product_str: str) -> Tuple[str, Set[str], str]: 23 """Parses provided product string. 24 25 Args: 26 product_str: target supplied product string. 27 28 Returns: 29 (family, defines, name) where 30 `family` is the stm32 product family (ex. 'stm32l5xx') 31 `defines` is a list of potential product defines for the HAL. 32 There can be multiple because some products use a subfamily 33 for their define. 34 (ex. The only stm32f411 define is `STM32F411xE`) 35 The correct define can be validated using `select_define()` 36 `name` is the standardized name for the product string. 37 (ex. product_str = 'STM32F429', name = 'stm32f429xx') 38 This is the product name expected by the filename matching 39 functions (`match_filename()`, etc.) 40 41 Raises: 42 ValueError if the product string does not start with 'stm32' or specify 43 at least the chip model (9 chars). 44 """ 45 product_name = product_str.lower() 46 if not product_name.startswith('stm32'): 47 raise ValueError("Product string must start with 'stm32'") 48 49 if len(product_name) < 9: 50 raise ValueError( 51 "Product string too short. Must specify at least the chip model.") 52 53 family = product_name[:7] + 'xx' 54 name = product_name 55 56 # Pad the full name with 'x' to reach the max expected length. 57 name = product_name.ljust(11, 'x') 58 59 # This generates all potential define suffixes for a given product name 60 # This is required because some boards have more specific defines 61 # ex. STM32F411xE, while most others are generic, ex. STM32F439xx 62 # So if the user specifies `stm32f207zgt6u`, this should generate the 63 # following as potential defines 64 # STM32F207xx, STM32F207Zx, STM32F207xG, STM32F207ZG 65 define_suffixes = ['xx'] 66 if name[9] != 'x': 67 define_suffixes.append(name[9].upper() + 'x') 68 if name[10] != 'x': 69 define_suffixes.append('x' + name[10].upper()) 70 if name[9] != 'x' and name[10] != 'x': 71 define_suffixes.append(name[9:11].upper()) 72 73 defines = set(map(lambda x: product_name[:9].upper() + x, define_suffixes)) 74 return (family, defines, name) 75 76 77def select_define(defines: Set[str], family_header: str) -> str: 78 """Selects valid define from set of potential defines. 79 80 Looks for the defines in the family header to pick the correct one. 81 82 Args: 83 defines: set of defines provided by `parse_product_str` 84 family_header: `{family}.h` read into a string 85 86 Returns: 87 A single valid define 88 89 Raises: 90 ValueError if exactly one define is not found. 91 """ 92 valid_defines = list( 93 filter( 94 lambda x: f'defined({x})' in family_header or f'defined ({x})' in 95 family_header, defines)) 96 97 if len(valid_defines) != 1: 98 raise ValueError("Unable to select a valid define") 99 100 return valid_defines[0] 101 102 103def match_filename(product_name: str, filename: str): 104 """Matches linker and startup filenames with product name. 105 106 Args: 107 product_name: the name standardized by `parse_product_str` 108 filename: a linker or startup filename 109 110 Returns: 111 True if the filename could be associated with the product. 112 False otherwise. 113 """ 114 stm32_parts = list( 115 filter(lambda x: x.startswith('stm32'), 116 re.split(r'\.|_', filename.lower()))) 117 118 if len(stm32_parts) != 1: 119 return False 120 121 pattern = stm32_parts[0].replace('x', '.') 122 123 return re.match(pattern, product_name) is not None 124 125 126def find_linker_files( 127 product_name: str, files: List[str], stm32cube_path: pathlib.Path 128) -> Tuple[Optional[pathlib.Path], Optional[pathlib.Path]]: 129 """Finds linker file for the given product. 130 131 This searches `files` for linker scripts by name. 132 133 Args: 134 product_name: the name standardized by `parse_product_str` 135 files: list of file paths 136 stm32cube_path: the root path that `files` entries are relative to 137 138 Returns: 139 (gcc_linker, iar_linker) where gcc_linker / iar_linker are paths to a 140 linker file or None 141 142 Raises: 143 ValueError if `product_name` matches with no linker files, or with 144 multiple .ld/.icf files. 145 """ 146 linker_files = list( 147 filter( 148 lambda x: 149 (x.endswith('.ld') or x.endswith('.icf')) and '_flash.' in x.lower( 150 ), files)) 151 matching_linker_files = list( 152 filter(lambda x: match_filename(product_name, 153 pathlib.Path(x).name), linker_files)) 154 155 matching_ld_files = list( 156 filter(lambda x: x.endswith('.ld'), matching_linker_files)) 157 matching_icf_files = list( 158 filter(lambda x: x.endswith('.icf'), matching_linker_files)) 159 160 if len(matching_ld_files) > 1 or len(matching_icf_files) > 1: 161 raise ValueError( 162 f'Too many linker file matches for {product_name}.' + 163 ' Provide a more specific product string or your own linker script' 164 ) 165 if not matching_ld_files and not matching_icf_files: 166 raise ValueError(f'No linker script matching {product_name} found') 167 168 return (stm32cube_path / 169 matching_ld_files[0] if matching_ld_files else None, 170 stm32cube_path / 171 matching_icf_files[0] if matching_icf_files else None) 172 173 174def find_startup_file(product_name: str, files: List[str], 175 stm32cube_path: pathlib.Path) -> pathlib.Path: 176 """Finds startup file for the given product. 177 178 Searches for gcc startup files. 179 180 Args: 181 product_name: the name standardized by `parse_product_str` 182 files: list of file paths 183 stm32cube_path: the root path that `files` entries are relative to 184 185 Returns: 186 Path to matching startup file 187 188 Raises: 189 ValueError if no / > 1 matching startup files found. 190 """ 191 # ST provides startup files for gcc, iar, and arm compilers. They have the 192 # same filenames, so this looks for a 'gcc' folder in the path. 193 matching_startup_files = list( 194 filter( 195 lambda f: '/gcc/' in f and f.endswith('.s') and match_filename( 196 product_name, f), files)) 197 198 if not matching_startup_files: 199 raise ValueError(f'No matching startup file found for {product_name}') 200 if len(matching_startup_files) == 1: 201 return stm32cube_path / matching_startup_files[0] 202 203 raise ValueError( 204 f'Multiple matching startup files found for {product_name}') 205 206 207_INCLUDE_DIRS = [ 208 'hal_driver/Inc', 209 'hal_driver/Inc/Legacy', 210 'cmsis_device/Include', 211 'cmsis_core/Include', 212 'cmsis_core/DSP/Include', 213] 214 215 216def get_include_dirs(stm32cube_path: pathlib.Path) -> List[pathlib.Path]: 217 """Get HAL include directories.""" 218 return list(map(lambda f: stm32cube_path / f, _INCLUDE_DIRS)) 219 220 221def get_sources_and_headers( 222 files: List[str], 223 stm32cube_path: pathlib.Path) -> Tuple[List[str], List[str]]: 224 """Gets list of all sources and headers needed to build the stm32cube hal. 225 226 Args: 227 files: list of file paths 228 stm32cube_path: the root path that `files` entries are relative to 229 230 Returns: 231 (sources, headers) where 232 `sources` is a list of absolute paths to all core (non-template) 233 sources needed for the hal 234 `headers` is a list of absolute paths to all needed headers 235 """ 236 source_files = filter( 237 lambda f: f.startswith('hal_driver/Src') and f.endswith('.c') and 238 'template' not in f, files) 239 240 header_files = filter( 241 lambda f: (any(f.startswith(dir) 242 for dir in _INCLUDE_DIRS)) and f.endswith('.h'), files) 243 244 rebase_path = lambda f: str(stm32cube_path / f) 245 return list(map(rebase_path, 246 source_files)), list(map(rebase_path, header_files)) 247 248 249def parse_files_txt(stm32cube_path: pathlib.Path) -> List[str]: 250 """Reads files.txt into list.""" 251 with open(stm32cube_path / 'files.txt', 'r') as files: 252 return list( 253 filter(lambda x: not x.startswith('#'), 254 map(lambda f: f.strip(), files.readlines()))) 255 256 257def _gn_str_out(name: str, val: Any): 258 """Outputs scoped string in GN format.""" 259 print(f'{name} = "{val}"') 260 261 262def _gn_list_str_out(name: str, val: List[Any]): 263 """Outputs list of strings in GN format with correct escaping.""" 264 list_str = ','.join('"' + str(x).replace('"', r'\"').replace('$', r'\$') + 265 '"' for x in val) 266 print(f'{name} = [{list_str}]') 267 268 269def find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool): 270 """Generates and outputs the required GN args for the build.""" 271 file_list = parse_files_txt(stm32cube_path) 272 273 include_dirs = get_include_dirs(stm32cube_path) 274 sources, headers = get_sources_and_headers(file_list, stm32cube_path) 275 (family, defines, name) = parse_product_str(product_str) 276 277 family_header_path = list( 278 filter(lambda p: p.endswith(f'/{family}.h'), headers))[0] 279 280 with open(family_header_path, 'rb') as family_header: 281 family_header_str = family_header.read().decode('utf-8', 282 errors='ignore') 283 284 define = select_define(defines, family_header_str) 285 286 _gn_str_out('family', family) 287 _gn_str_out('product_define', define) 288 _gn_list_str_out('sources', sources) 289 _gn_list_str_out('headers', headers) 290 _gn_list_str_out('include_dirs', include_dirs) 291 292 if init: 293 startup_file_path = find_startup_file(name, file_list, stm32cube_path) 294 gcc_linker, iar_linker = find_linker_files(name, file_list, 295 stm32cube_path) 296 297 _gn_str_out('startup', startup_file_path) 298 _gn_str_out('gcc_linker', gcc_linker if gcc_linker else '') 299 _gn_str_out('iar_linker', iar_linker if iar_linker else '') 300