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