blob: 6e233ef88b587b325285ce18bec9e50dbaa5bee4 [file] [log] [blame]
# 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 '')