• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
16import argparse
17import itertools
18import json
19from pathlib import Path
20from string import Template
21import textwrap
22from typing import (
23    Any,
24    Generator,
25    Iterable,
26    NamedTuple,
27    Optional,
28    Sequence,
29    Tuple,
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${section_attr}'
82    '${alignas}constexpr std::array<std::byte, ${size_bytes}> ${symbol_name}'
83    ' = {\n${bytes_lines}\n};\n'
84)
85
86BYTES_PER_LINE = 4
87
88
89class Blob(NamedTuple):
90    symbol_name: str
91    file_path: Path
92    linker_section: Optional[str]
93    alignas: Optional[str] = None
94
95    @staticmethod
96    def from_dict(blob_dict: dict) -> 'Blob':
97        return Blob(
98            blob_dict['symbol_name'],
99            Path(blob_dict['file_path']),
100            blob_dict.get('linker_section'),
101            blob_dict.get('alignas'),
102        )
103
104
105def parse_args() -> dict:
106    """Parse arguments."""
107    parser = argparse.ArgumentParser(description=__doc__)
108    parser.add_argument(
109        '--blob-file',
110        type=Path,
111        required=True,
112        help=('Path to json file containing the list of blobs ' 'to generate.'),
113    )
114    parser.add_argument(
115        '--header-include',
116        required=True,
117        help='Path to use in #includes for the header',
118    )
119    parser.add_argument(
120        '--out-source',
121        type=Path,
122        required=True,
123        help='Path at which to write .cpp file',
124    )
125    parser.add_argument(
126        '--out-header',
127        type=Path,
128        required=True,
129        help='Path at which to write .h file',
130    )
131    parser.add_argument(
132        '--namespace',
133        type=str,
134        required=False,
135        help=('Optional namespace that blobs will be scoped' 'within.'),
136    )
137
138    return vars(parser.parse_args())
139
140
141def split_into_chunks(
142    data: Iterable[Any], chunk_size: int
143) -> Generator[Tuple[Any, ...], None, None]:
144    """Splits an iterable into chunks of a given size."""
145    data_iterator = iter(data)
146    chunk = tuple(itertools.islice(data_iterator, chunk_size))
147    while chunk:
148        yield chunk
149        chunk = tuple(itertools.islice(data_iterator, chunk_size))
150
151
152def header_from_blobs(
153    blobs: Iterable[Blob], namespace: Optional[str] = None
154) -> str:
155    """Generate the contents of a C++ header file from blobs."""
156    lines = [HEADER_PREFIX]
157    if namespace:
158        lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
159    for blob in blobs:
160        data = blob.file_path.read_bytes()
161        lines.append(
162            BLOB_DECLARATION_TEMPLATE.substitute(
163                symbol_name=blob.symbol_name, size_bytes=len(data)
164            )
165        )
166    if namespace:
167        lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))
168
169    return ''.join(lines)
170
171
172def array_def_from_blob_data(blob: Blob, blob_data: bytes) -> str:
173    """Generates an array definition for the given blob data."""
174    if blob.linker_section:
175        section_attr = LINKER_SECTION_TEMPLATE.substitute(
176            linker_section=blob.linker_section
177        )
178    else:
179        section_attr = ''
180
181    byte_strs = ['std::byte{{0x{:02X}}}'.format(b) for b in blob_data]
182
183    lines = []
184    for byte_strs_for_line in split_into_chunks(byte_strs, BYTES_PER_LINE):
185        bytes_segment = ', '.join(byte_strs_for_line)
186        lines.append(f'    {bytes_segment},')
187
188    return BLOB_DEFINITION_MULTI_LINE.substitute(
189        section_attr=section_attr,
190        alignas=f'alignas({blob.alignas}) ' if blob.alignas else '',
191        symbol_name=blob.symbol_name,
192        size_bytes=len(blob_data),
193        bytes_lines='\n'.join(lines),
194    )
195
196
197def source_from_blobs(
198    blobs: Iterable[Blob], header_path: str, namespace: Optional[str] = None
199) -> str:
200    """Generate the contents of a C++ source file from blobs."""
201    lines = [SOURCE_PREFIX_TEMPLATE.substitute(header_path=header_path)]
202    if namespace:
203        lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
204    for blob in blobs:
205        data = blob.file_path.read_bytes()
206        lines.append(array_def_from_blob_data(blob, data))
207    if namespace:
208        lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))
209
210    return ''.join(lines)
211
212
213def load_blobs(blob_file: Path) -> Sequence[Blob]:
214    with blob_file.open() as blob_fp:
215        return [Blob.from_dict(blob) for blob in json.load(blob_fp)]
216
217
218def main(
219    blob_file: Path,
220    header_include: str,
221    out_source: Path,
222    out_header: Path,
223    namespace: Optional[str] = None,
224) -> None:
225    blobs = load_blobs(blob_file)
226
227    out_header.parent.mkdir(parents=True, exist_ok=True)
228    out_header.write_text(header_from_blobs(blobs, namespace))
229
230    out_source.parent.mkdir(parents=True, exist_ok=True)
231    out_source.write_text(source_from_blobs(blobs, header_include, namespace))
232
233
234if __name__ == '__main__':
235    main(**parse_args())
236