| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Finds files for a given product.""" |
| |
| from typing import Any, List, Optional, Set, Tuple |
| |
| import pathlib |
| import re |
| |
| |
| def parse_product_str(product_str: str) -> Tuple[str, Set[str], str]: |
| """Parses provided product string. |
| |
| Args: |
| product_str: target supplied product string. |
| |
| Returns: |
| (family, defines, name) where |
| `family` is the stm32 product family (ex. 'stm32l5xx') |
| `defines` is a list of potential product defines for the HAL. |
| There can be multiple because some products use a subfamily |
| for their define. |
| (ex. The only stm32f411 define is `STM32F411xE`) |
| The correct define can be validated using `select_define()` |
| `name` is the standardized name for the product string. |
| (ex. product_str = 'STM32F429', name = 'stm32f429xx') |
| This is the product name expected by the filename matching |
| functions (`match_filename()`, etc.) |
| |
| Raises: |
| ValueError if the product string does not start with 'stm32' or specify |
| at least the chip model (9 chars). |
| """ |
| product_name = product_str.lower() |
| if not product_name.startswith('stm32'): |
| raise ValueError("Product string must start with 'stm32'") |
| |
| if len(product_name) < 9: |
| raise ValueError( |
| "Product string too short. Must specify at least the chip model.") |
| |
| family = product_name[:7] + 'xx' |
| name = product_name |
| |
| # Pad the full name with 'x' to reach the max expected length. |
| name = product_name.ljust(11, 'x') |
| |
| # This generates all potential define suffixes for a given product name |
| # This is required because some boards have more specific defines |
| # ex. STM32F411xE, while most others are generic, ex. STM32F439xx |
| # So if the user specifies `stm32f207zgt6u`, this should generate the |
| # following as potential defines |
| # STM32F207xx, STM32F207Zx, STM32F207xG, STM32F207ZG |
| define_suffixes = ['xx'] |
| if name[9] != 'x': |
| define_suffixes.append(name[9].upper() + 'x') |
| if name[10] != 'x': |
| define_suffixes.append('x' + name[10].upper()) |
| if name[9] != 'x' and name[10] != 'x': |
| define_suffixes.append(name[9:11].upper()) |
| |
| defines = set(map(lambda x: product_name[:9].upper() + x, define_suffixes)) |
| return (family, defines, name) |
| |
| |
| def select_define(defines: Set[str], family_header: str) -> str: |
| """Selects valid define from set of potential defines. |
| |
| Looks for the defines in the family header to pick the correct one. |
| |
| Args: |
| defines: set of defines provided by `parse_product_str` |
| family_header: `{family}.h` read into a string |
| |
| Returns: |
| A single valid define |
| |
| Raises: |
| ValueError if exactly one define is not found. |
| """ |
| valid_defines = list( |
| filter( |
| lambda x: f'defined({x})' in family_header or f'defined ({x})' in |
| family_header, defines)) |
| |
| if len(valid_defines) != 1: |
| raise ValueError("Unable to select a valid define") |
| |
| return valid_defines[0] |
| |
| |
| def match_filename(product_name: str, filename: str): |
| """Matches linker and startup filenames with product name. |
| |
| Args: |
| product_name: the name standardized by `parse_product_str` |
| filename: a linker or startup filename |
| |
| Returns: |
| True if the filename could be associated with the product. |
| False otherwise. |
| """ |
| stm32_parts = list( |
| filter(lambda x: x.startswith('stm32'), |
| re.split(r'\.|_', filename.lower()))) |
| |
| if len(stm32_parts) != 1: |
| return False |
| |
| pattern = stm32_parts[0].replace('x', '.') |
| |
| return re.match(pattern, product_name) is not None |
| |
| |
| def find_linker_files( |
| product_name: str, files: List[str], stm32cube_path: pathlib.Path |
| ) -> Tuple[Optional[pathlib.Path], Optional[pathlib.Path]]: |
| """Finds linker file for the given product. |
| |
| This searches `files` for linker scripts by name. |
| |
| Args: |
| product_name: the name standardized by `parse_product_str` |
| files: list of file paths |
| stm32cube_path: the root path that `files` entries are relative to |
| |
| Returns: |
| (gcc_linker, iar_linker) where gcc_linker / iar_linker are paths to a |
| linker file or None |
| |
| Raises: |
| ValueError if `product_name` matches with no linker files, or with |
| multiple .ld/.icf files. |
| """ |
| linker_files = list( |
| filter( |
| lambda x: |
| (x.endswith('.ld') or x.endswith('.icf')) and '_flash.' in x.lower( |
| ), files)) |
| matching_linker_files = list( |
| filter(lambda x: match_filename(product_name, |
| pathlib.Path(x).name), linker_files)) |
| |
| matching_ld_files = list( |
| filter(lambda x: x.endswith('.ld'), matching_linker_files)) |
| matching_icf_files = list( |
| filter(lambda x: x.endswith('.icf'), matching_linker_files)) |
| |
| if len(matching_ld_files) > 1 or len(matching_icf_files) > 1: |
| raise ValueError( |
| f'Too many linker file matches for {product_name}.' + |
| ' Provide a more specific product string or your own linker script' |
| ) |
| if not matching_ld_files and not matching_icf_files: |
| raise ValueError(f'No linker script matching {product_name} found') |
| |
| return (stm32cube_path / |
| matching_ld_files[0] if matching_ld_files else None, |
| stm32cube_path / |
| matching_icf_files[0] if matching_icf_files else None) |
| |
| |
| def find_startup_file(product_name: str, files: List[str], |
| stm32cube_path: pathlib.Path) -> pathlib.Path: |
| """Finds startup file for the given product. |
| |
| Searches for gcc startup files. |
| |
| Args: |
| product_name: the name standardized by `parse_product_str` |
| files: list of file paths |
| stm32cube_path: the root path that `files` entries are relative to |
| |
| Returns: |
| Path to matching startup file |
| |
| Raises: |
| ValueError if no / > 1 matching startup files found. |
| """ |
| # ST provides startup files for gcc, iar, and arm compilers. They have the |
| # same filenames, so this looks for a 'gcc' folder in the path. |
| matching_startup_files = list( |
| filter( |
| lambda f: '/gcc/' in f and f.endswith('.s') and match_filename( |
| product_name, f), files)) |
| |
| if not matching_startup_files: |
| raise ValueError(f'No matching startup file found for {product_name}') |
| if len(matching_startup_files) == 1: |
| return stm32cube_path / matching_startup_files[0] |
| |
| raise ValueError( |
| f'Multiple matching startup files found for {product_name}') |
| |
| |
| _INCLUDE_DIRS = [ |
| 'hal_driver/Inc', |
| 'hal_driver/Inc/Legacy', |
| 'cmsis_device/Include', |
| 'cmsis_core/Include', |
| 'cmsis_core/DSP/Include', |
| ] |
| |
| |
| def get_include_dirs(stm32cube_path: pathlib.Path) -> List[pathlib.Path]: |
| """Get HAL include directories.""" |
| return list(map(lambda f: stm32cube_path / f, _INCLUDE_DIRS)) |
| |
| |
| def get_sources_and_headers( |
| files: List[str], |
| stm32cube_path: pathlib.Path) -> Tuple[List[str], List[str]]: |
| """Gets list of all sources and headers needed to build the stm32cube hal. |
| |
| Args: |
| files: list of file paths |
| stm32cube_path: the root path that `files` entries are relative to |
| |
| Returns: |
| (sources, headers) where |
| `sources` is a list of absolute paths to all core (non-template) |
| sources needed for the hal |
| `headers` is a list of absolute paths to all needed headers |
| """ |
| source_files = filter( |
| lambda f: f.startswith('hal_driver/Src') and f.endswith('.c') and |
| 'template' not in f, files) |
| |
| header_files = filter( |
| lambda f: (any(f.startswith(dir) |
| for dir in _INCLUDE_DIRS)) and f.endswith('.h'), files) |
| |
| rebase_path = lambda f: str(stm32cube_path / f) |
| return list(map(rebase_path, |
| source_files)), list(map(rebase_path, header_files)) |
| |
| |
| def parse_files_txt(stm32cube_path: pathlib.Path) -> List[str]: |
| """Reads files.txt into list.""" |
| with open(stm32cube_path / 'files.txt', 'r') as files: |
| return list( |
| filter(lambda x: not x.startswith('#'), |
| map(lambda f: f.strip(), files.readlines()))) |
| |
| |
| def _gn_str_out(name: str, val: Any): |
| """Outputs scoped string in GN format.""" |
| print(f'{name} = "{val}"') |
| |
| |
| def _gn_list_str_out(name: str, val: List[Any]): |
| """Outputs list of strings in GN format with correct escaping.""" |
| list_str = ','.join('"' + str(x).replace('"', r'\"').replace('$', r'\$') + |
| '"' for x in val) |
| print(f'{name} = [{list_str}]') |
| |
| |
| def find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool): |
| """Generates and outputs the required GN args for the build.""" |
| file_list = parse_files_txt(stm32cube_path) |
| |
| include_dirs = get_include_dirs(stm32cube_path) |
| sources, headers = get_sources_and_headers(file_list, stm32cube_path) |
| (family, defines, name) = parse_product_str(product_str) |
| |
| family_header_path = list( |
| filter(lambda p: p.endswith(f'/{family}.h'), headers))[0] |
| |
| with open(family_header_path, 'rb') as family_header: |
| family_header_str = family_header.read().decode('utf-8', |
| errors='ignore') |
| |
| define = select_define(defines, family_header_str) |
| |
| _gn_str_out('family', family) |
| _gn_str_out('product_define', define) |
| _gn_list_str_out('sources', sources) |
| _gn_list_str_out('headers', headers) |
| _gn_list_str_out('include_dirs', include_dirs) |
| |
| if init: |
| startup_file_path = find_startup_file(name, file_list, stm32cube_path) |
| gcc_linker, iar_linker = find_linker_files(name, file_list, |
| stm32cube_path) |
| |
| _gn_str_out('startup', startup_file_path) |
| _gn_str_out('gcc_linker', gcc_linker if gcc_linker else '') |
| _gn_str_out('iar_linker', iar_linker if iar_linker else '') |