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 Any, Generator, Iterable, NamedTuple, Optional, Tuple 23 24HEADER_PREFIX = textwrap.dedent("""\ 25 // This file is auto-generated; Do not hand-modify! 26 // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details. 27 28 #pragma once 29 30 #include <array> 31 #include <cstddef> 32 """) 33 34SOURCE_PREFIX_TEMPLATE = Template( 35 textwrap.dedent("""\ 36 // This file is auto-generated; Do not hand-modify! 37 // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details. 38 39 #include "$header_path" 40 41 #include <array> 42 #include <cstddef> 43 44 #include "pw_preprocessor/compiler.h" 45 """)) 46 47NAMESPACE_OPEN_TEMPLATE = Template('namespace ${namespace} {\n') 48 49NAMESPACE_CLOSE_TEMPLATE = Template('} // namespace ${namespace}\n') 50 51BLOB_DECLARATION_TEMPLATE = Template( 52 'extern const std::array<std::byte, ${size_bytes}> ${symbol_name};') 53 54LINKER_SECTION_TEMPLATE = Template('PW_PLACE_IN_SECTION("${linker_section}")') 55 56BLOB_DEFINITION_SINGLE_LINE = Template( 57 'const std::array<std::byte, ${size_bytes}> ${symbol_name}' 58 ' = { $bytes };') 59 60BLOB_DEFINITION_MULTI_LINE = Template( 61 'const std::array<std::byte, ${size_bytes}> ${symbol_name}' 62 ' = {\n${bytes_lines}\n};') 63 64BYTES_PER_LINE = 4 65 66 67class Blob(NamedTuple): 68 symbol_name: str 69 file_path: Path 70 linker_section: Optional[str] 71 72 73def parse_args(): 74 parser = argparse.ArgumentParser(description=__doc__) 75 parser.add_argument('--blob-file', 76 type=Path, 77 required=True, 78 help=('Path to json file containing the list of blobs ' 79 'to generate.')) 80 parser.add_argument('--out-source', 81 type=Path, 82 required=True, 83 help='Path at which to write .cpp file') 84 parser.add_argument('--out-header', 85 type=Path, 86 required=True, 87 help='Path at which to write .h file') 88 parser.add_argument('--namespace', 89 type=str, 90 required=False, 91 help=('Optional namespace that blobs will be scoped' 92 'within.')) 93 94 return parser.parse_args() 95 96 97def split_into_chunks( 98 data: Iterable[Any], 99 chunk_size: int) -> Generator[Tuple[Any, ...], None, None]: 100 """Splits an iterable into chunks of a given size.""" 101 data_iterator = iter(data) 102 chunk = tuple(itertools.islice(data_iterator, chunk_size)) 103 while chunk: 104 yield chunk 105 chunk = tuple(itertools.islice(data_iterator, chunk_size)) 106 107 108def header_from_blobs(blobs: Iterable[Blob], 109 namespace: Optional[str] = None) -> str: 110 """Generate the contents of a C++ header file from blobs.""" 111 lines = [HEADER_PREFIX] 112 if namespace: 113 lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace)) 114 for blob in blobs: 115 data = blob.file_path.read_bytes() 116 lines.append( 117 BLOB_DECLARATION_TEMPLATE.substitute(symbol_name=blob.symbol_name, 118 size_bytes=len(data))) 119 lines.append('') 120 if namespace: 121 lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace)) 122 123 return '\n'.join(lines) 124 125 126def array_def_from_blob_data(symbol_name: str, blob_data: bytes) -> str: 127 """Generates an array definition for the given blob data.""" 128 byte_strs = ['std::byte{{0x{:02X}}}'.format(b) for b in blob_data] 129 130 # Try to fit the blob definition on a single line first. 131 single_line_def = BLOB_DEFINITION_SINGLE_LINE.substitute( 132 symbol_name=symbol_name, 133 size_bytes=len(blob_data), 134 bytes=', '.join(byte_strs)) 135 if len(single_line_def) <= 80: 136 return single_line_def 137 138 # Blob definition didn't fit on a single line; do multi-line. 139 lines = [] 140 for byte_strs_for_line in split_into_chunks(byte_strs, BYTES_PER_LINE): 141 bytes_segment = ', '.join(byte_strs_for_line) 142 lines.append(f' {bytes_segment},') 143 # Removing the trailing comma from the final line of bytes. 144 lines[-1] = lines[-1].rstrip(',') 145 146 return BLOB_DEFINITION_MULTI_LINE.substitute(symbol_name=symbol_name, 147 size_bytes=len(blob_data), 148 bytes_lines='\n'.join(lines)) 149 150 151def source_from_blobs(blobs: Iterable[Blob], 152 header_path: Path, 153 namespace: Optional[str] = None) -> str: 154 """Generate the contents of a C++ source file from blobs.""" 155 lines = [SOURCE_PREFIX_TEMPLATE.substitute(header_path=header_path)] 156 if namespace: 157 lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace)) 158 for blob in blobs: 159 if blob.linker_section: 160 lines.append( 161 LINKER_SECTION_TEMPLATE.substitute( 162 linker_section=blob.linker_section)) 163 data = blob.file_path.read_bytes() 164 lines.append(array_def_from_blob_data(blob.symbol_name, data)) 165 lines.append('') 166 if namespace: 167 lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace)) 168 169 return '\n'.join(lines) 170 171 172def load_blobs(blob_file: Path) -> Iterable[Blob]: 173 with blob_file.open() as blob_fp: 174 return [ 175 Blob(b["symbol_name"], Path(b["file_path"]), 176 b.get("linker_section")) for b in json.load(blob_fp) 177 ] 178 179 180def main(blob_file: Path, 181 out_source: Path, 182 out_header: Path, 183 namespace: Optional[str] = None) -> None: 184 blobs = load_blobs(blob_file) 185 out_header.write_text(header_from_blobs(blobs, namespace)) 186 out_source.write_text(source_from_blobs(blobs, out_header, namespace)) 187 188 189if __name__ == '__main__': 190 main(**vars(parse_args())) 191