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