xref: /aosp_15_r20/external/pigweed/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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