xref: /aosp_15_r20/external/pigweed/pw_build/py/pw_build/generate_cc_blob_library.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"""Outputs the contents of blobs as a hard-coded arrays in a C++ library."""
15
16from __future__ import annotations
17
18import argparse
19import itertools
20import json
21from pathlib import Path
22from string import Template
23import textwrap
24from typing import (
25    Any,
26    Generator,
27    Iterable,
28    NamedTuple,
29    Sequence,
30)
31
32COMMENT = f"""\
33// This file was generated by {Path(__file__).name}.
34//
35// DO NOT EDIT!
36//
37// This file contains declarations for byte arrays created from files during the
38// build. The byte arrays are constant initialized and are safe to access at any
39// time, including before main().
40//
41// See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
42"""
43
44HEADER_PREFIX = COMMENT + textwrap.dedent(
45    """\
46    #pragma once
47
48    #include <array>
49    #include <cstddef>
50
51    """
52)
53
54SOURCE_PREFIX_TEMPLATE = Template(
55    COMMENT
56    + textwrap.dedent(
57        """\
58
59        #include "$header_path"
60
61        #include <array>
62        #include <cstddef>
63
64        #include "pw_preprocessor/compiler.h"
65
66        """
67    )
68)
69
70NAMESPACE_OPEN_TEMPLATE = Template('namespace ${namespace} {\n')
71
72NAMESPACE_CLOSE_TEMPLATE = Template('\n}  // namespace ${namespace}\n')
73
74BLOB_DECLARATION_TEMPLATE = Template(
75    '\nextern const std::array<std::byte, ${size_bytes}> ${symbol_name};\n'
76)
77
78LINKER_SECTION_TEMPLATE = Template('PW_PLACE_IN_SECTION("${linker_section}")\n')
79
80BLOB_DEFINITION_MULTI_LINE = Template(
81    '\n${alignas}'
82    '${section_attr}constexpr std::array<std::byte, ${size_bytes}>'
83    ' ${symbol_name}'
84    ' = {\n${bytes_lines}\n};\n'
85)
86
87BYTES_PER_LINE = 4
88
89
90class Blob(NamedTuple):
91    symbol_name: str
92    file_path: Path
93    linker_section: str | None
94    alignas: str | None = None
95
96    @staticmethod
97    def from_dict(blob_dict: dict) -> Blob:
98        return Blob(
99            blob_dict['symbol_name'],
100            Path(blob_dict['file_path']),
101            blob_dict.get('linker_section'),
102            blob_dict.get('alignas'),
103        )
104
105
106def parse_args() -> dict:
107    """Parse arguments."""
108    parser = argparse.ArgumentParser(description=__doc__)
109    parser.add_argument(
110        '--blob-file',
111        type=Path,
112        required=True,
113        help=('Path to json file containing the list of blobs ' 'to generate.'),
114    )
115    parser.add_argument(
116        '--header-include',
117        required=True,
118        help='Path to use in #includes for the header',
119    )
120    parser.add_argument(
121        '--out-source',
122        type=Path,
123        required=True,
124        help='Path at which to write .cpp file',
125    )
126    parser.add_argument(
127        '--out-header',
128        type=Path,
129        required=True,
130        help='Path at which to write .h file',
131    )
132    parser.add_argument(
133        '--namespace',
134        type=str,
135        required=False,
136        help=('Optional namespace that blobs will be scoped' 'within.'),
137    )
138
139    return vars(parser.parse_args())
140
141
142def split_into_chunks(
143    data: Iterable[Any], chunk_size: int
144) -> Generator[tuple[Any, ...], None, None]:
145    """Splits an iterable into chunks of a given size."""
146    data_iterator = iter(data)
147    chunk = tuple(itertools.islice(data_iterator, chunk_size))
148    while chunk:
149        yield chunk
150        chunk = tuple(itertools.islice(data_iterator, chunk_size))
151
152
153def header_from_blobs(
154    blobs: Iterable[Blob], namespace: str | None = None
155) -> str:
156    """Generate the contents of a C++ header file from blobs."""
157    lines = [HEADER_PREFIX]
158    if namespace:
159        lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
160    for blob in blobs:
161        data = blob.file_path.read_bytes()
162        lines.append(
163            BLOB_DECLARATION_TEMPLATE.substitute(
164                symbol_name=blob.symbol_name, size_bytes=len(data)
165            )
166        )
167    if namespace:
168        lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))
169
170    return ''.join(lines)
171
172
173def array_def_from_blob_data(blob: Blob, blob_data: bytes) -> str:
174    """Generates an array definition for the given blob data."""
175    if blob.linker_section:
176        section_attr = LINKER_SECTION_TEMPLATE.substitute(
177            linker_section=blob.linker_section
178        )
179    else:
180        section_attr = ''
181
182    byte_strs = ['std::byte{{0x{:02X}}}'.format(b) for b in blob_data]
183
184    lines = []
185    for byte_strs_for_line in split_into_chunks(byte_strs, BYTES_PER_LINE):
186        bytes_segment = ', '.join(byte_strs_for_line)
187        lines.append(f'    {bytes_segment},')
188
189    return BLOB_DEFINITION_MULTI_LINE.substitute(
190        section_attr=section_attr,
191        alignas=f'alignas({blob.alignas}) ' if blob.alignas else '',
192        symbol_name=blob.symbol_name,
193        size_bytes=len(blob_data),
194        bytes_lines='\n'.join(lines),
195    )
196
197
198def source_from_blobs(
199    blobs: Iterable[Blob], header_path: str, namespace: str | None = None
200) -> str:
201    """Generate the contents of a C++ source file from blobs."""
202    lines = [SOURCE_PREFIX_TEMPLATE.substitute(header_path=header_path)]
203    if namespace:
204        lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
205    for blob in blobs:
206        data = blob.file_path.read_bytes()
207        lines.append(array_def_from_blob_data(blob, data))
208    if namespace:
209        lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))
210
211    return ''.join(lines)
212
213
214def load_blobs(blob_file: Path) -> Sequence[Blob]:
215    with blob_file.open() as blob_fp:
216        return [Blob.from_dict(blob) for blob in json.load(blob_fp)]
217
218
219def main(
220    blob_file: Path,
221    header_include: str,
222    out_source: Path,
223    out_header: Path,
224    namespace: str | None = None,
225) -> None:
226    blobs = load_blobs(blob_file)
227
228    out_header.parent.mkdir(parents=True, exist_ok=True)
229    out_header.write_text(header_from_blobs(blobs, namespace))
230
231    out_source.parent.mkdir(parents=True, exist_ok=True)
232    out_source.write_text(source_from_blobs(blobs, header_include, namespace))
233
234
235if __name__ == '__main__':
236    main(**parse_args())
237