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, Set 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 54 family = product_name[:7] + 'xx' 55 name = product_name 56 57 # Pad the full name with 'x' to reach the max expected length. 58 name = product_name.ljust(11, 'x') 59 60 # This generates all potential define suffixes for a given product name 61 # This is required because some boards have more specific defines 62 # ex. STM32F411xE, while most others are generic, ex. STM32F439xx 63 # So if the user specifies `stm32f207zgt6u`, this should generate the 64 # following as potential defines 65 # STM32F207xx, STM32F207Zx, STM32F207xG, STM32F207ZG 66 define_suffixes = ['xx'] 67 if name[9] != 'x': 68 define_suffixes.append(name[9].upper() + 'x') 69 if name[10] != 'x': 70 define_suffixes.append('x' + name[10].upper()) 71 if name[9] != 'x' and name[10] != 'x': 72 define_suffixes.append(name[9:11].upper()) 73 74 defines = set(map(lambda x: product_name[:9].upper() + x, define_suffixes)) 75 return (family, defines, name) 76 77 78def select_define(defines: Set[str], family_header: str) -> str: 79 """Selects valid define from set of potential defines. 80 81 Looks for the defines in the family header to pick the correct one. 82 83 Args: 84 defines: set of defines provided by `parse_product_str` 85 family_header: `{family}.h` read into a string 86 87 Returns: 88 A single valid define 89 90 Raises: 91 ValueError if exactly one define is not found. 92 """ 93 valid_defines = list( 94 filter( 95 lambda x: f'defined({x})' in family_header 96 or f'defined ({x})' in family_header, 97 defines, 98 ) 99 ) 100 101 if len(valid_defines) != 1: 102 raise ValueError("Unable to select a valid define") 103 104 return valid_defines[0] 105 106 107def match_filename(product_name: str, filename: str): 108 """Matches linker and startup filenames with product name. 109 110 Args: 111 product_name: the name standardized by `parse_product_str` 112 filename: a linker or startup filename 113 114 Returns: 115 True if the filename could be associated with the product. 116 False otherwise. 117 """ 118 stm32_parts = list( 119 filter( 120 lambda x: x.startswith('stm32'), re.split(r'\.|_', filename.lower()) 121 ) 122 ) 123 124 if len(stm32_parts) != 1: 125 return False 126 127 pattern = stm32_parts[0].replace('x', '.') 128 129 return re.match(pattern, product_name) is not None 130 131 132def find_linker_files( 133 product_name: str, files: list[str], stm32cube_path: pathlib.Path 134) -> tuple[pathlib.Path | None, pathlib.Path | None]: 135 """Finds linker file for the given product. 136 137 This searches `files` for linker scripts by name. 138 139 Args: 140 product_name: the name standardized by `parse_product_str` 141 files: list of file paths 142 stm32cube_path: the root path that `files` entries are relative to 143 144 Returns: 145 (gcc_linker, iar_linker) where gcc_linker / iar_linker are paths to a 146 linker file or None 147 148 Raises: 149 ValueError if `product_name` matches with no linker files, or with 150 multiple .ld/.icf files. 151 """ 152 linker_files = list( 153 filter( 154 lambda x: (x.endswith('.ld') or x.endswith('.icf')) 155 and '_flash.' in x.lower(), 156 files, 157 ) 158 ) 159 matching_linker_files = list( 160 filter( 161 lambda x: match_filename(product_name, pathlib.Path(x).name), 162 linker_files, 163 ) 164 ) 165 166 matching_ld_files = list( 167 filter(lambda x: x.endswith('.ld'), matching_linker_files) 168 ) 169 matching_icf_files = list( 170 filter(lambda x: x.endswith('.icf'), matching_linker_files) 171 ) 172 173 if len(matching_ld_files) > 1 or len(matching_icf_files) > 1: 174 raise ValueError( 175 f'Too many linker file matches for {product_name}. ' 176 'Provide a more specific product string or your own linker script' 177 ) 178 if not matching_ld_files and not matching_icf_files: 179 raise ValueError(f'No linker script matching {product_name} found') 180 181 return ( 182 stm32cube_path / matching_ld_files[0] if matching_ld_files else None, 183 stm32cube_path / matching_icf_files[0] if matching_icf_files else None, 184 ) 185 186 187def find_startup_file( 188 product_name: str, files: list[str], stm32cube_path: pathlib.Path 189) -> pathlib.Path: 190 """Finds startup file for the given product. 191 192 Searches for gcc startup files. 193 194 Args: 195 product_name: the name standardized by `parse_product_str` 196 files: list of file paths 197 stm32cube_path: the root path that `files` entries are relative to 198 199 Returns: 200 Path to matching startup file 201 202 Raises: 203 ValueError if no / > 1 matching startup files found. 204 """ 205 # ST provides startup files for gcc, iar, and arm compilers. They have the 206 # same filenames, so this looks for a 'gcc' folder in the path. 207 matching_startup_files = list( 208 filter( 209 lambda f: '/gcc/' in f 210 and f.endswith('.s') 211 and match_filename(product_name, f), 212 files, 213 ) 214 ) 215 216 if not matching_startup_files: 217 raise ValueError(f'No matching startup file found for {product_name}') 218 if len(matching_startup_files) == 1: 219 return stm32cube_path / matching_startup_files[0] 220 221 raise ValueError( 222 f'Multiple matching startup files found for {product_name}' 223 ) 224 225 226_INCLUDE_DIRS = [ 227 'hal_driver/Inc', 228 'hal_driver/Inc/Legacy', 229 'cmsis_device/Include', 230 'cmsis_core/Include', 231 'cmsis_core/DSP/Include', 232] 233 234 235def get_include_dirs(stm32cube_path: pathlib.Path) -> list[pathlib.Path]: 236 """Get HAL include directories.""" 237 return list(map(lambda f: stm32cube_path / f, _INCLUDE_DIRS)) 238 239 240def get_sources_and_headers( 241 files: list[str], stm32cube_path: pathlib.Path 242) -> tuple[list[pathlib.Path], list[pathlib.Path]]: 243 """Gets list of all sources and headers needed to build the stm32cube hal. 244 245 Args: 246 files: list of file paths 247 stm32cube_path: the root path that `files` entries are relative to 248 249 Returns: 250 (sources, headers) where 251 `sources` is a list of absolute paths to all core (non-template) 252 sources needed for the hal 253 `headers` is a list of absolute paths to all needed headers 254 """ 255 source_files = filter( 256 lambda f: f.startswith('hal_driver/Src') 257 and f.endswith('.c') 258 and 'template' not in f, 259 files, 260 ) 261 262 header_files = filter( 263 lambda f: (any(f.startswith(dir) for dir in _INCLUDE_DIRS)) 264 and f.endswith('.h'), 265 files, 266 ) 267 268 def rebase_path(f): 269 return pathlib.Path(stm32cube_path / f) 270 271 return list(map(rebase_path, source_files)), list( 272 map(rebase_path, header_files) 273 ) 274 275 276def parse_files_txt(stm32cube_path: pathlib.Path) -> list[str]: 277 """Reads files.txt into list.""" 278 with open(stm32cube_path / 'files.txt', 'r') as files: 279 return list( 280 filter( 281 lambda x: not x.startswith('#'), 282 map(lambda f: f.strip(), files.readlines()), 283 ) 284 ) 285 286 287def _gn_str_out(name: str, val: Any): 288 """Outputs scoped string in GN format.""" 289 print(f'{name} = "{val}"') 290 291 292def _gn_list_str_out(name: str, val: list[Any]): 293 """Outputs list of strings in GN format with correct escaping.""" 294 list_str = ','.join( 295 '"' + str(x).replace('"', r'\"').replace('$', r'\$') + '"' for x in val 296 ) 297 print(f'{name} = [{list_str}]') 298 299 300def find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool): 301 """Generates and outputs the required GN args for the build.""" 302 file_list = parse_files_txt(stm32cube_path) 303 304 include_dirs = get_include_dirs(stm32cube_path) 305 sources, headers = get_sources_and_headers(file_list, stm32cube_path) 306 (family, defines, name) = parse_product_str(product_str) 307 308 family_header_path = list( 309 filter(lambda p: p.name == f'{family}.h', headers) 310 )[0] 311 312 family_header_str = family_header_path.read_text('utf-8', errors='ignore') 313 314 define = select_define(defines, family_header_str) 315 316 _gn_str_out('family', family) 317 _gn_str_out('product_define', define) 318 _gn_list_str_out('sources', sources) 319 _gn_list_str_out('headers', headers) 320 _gn_list_str_out('include_dirs', include_dirs) 321 322 if init: 323 startup_file_path = find_startup_file(name, file_list, stm32cube_path) 324 gcc_linker, iar_linker = find_linker_files( 325 name, file_list, stm32cube_path 326 ) 327 328 _gn_str_out('startup', startup_file_path) 329 _gn_str_out('gcc_linker', gcc_linker if gcc_linker else '') 330 _gn_str_out('iar_linker', iar_linker if iar_linker else '') 331