| #!/usr/bin/env python3 | |
| # Copyright (C) 2026 The Android Open Source Project | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Emit a C++ header containing a constexpr std::array<uint8_t, N> blob. | |
| Importable from other build-time codegen tools, or runnable as a CLI: | |
| python3 cpp_blob_emitter.py \\ | |
| --input data.bin --output out.h \\ | |
| --namespace foo [--symbol kFoo] [--include-guard FOO_H_] \\ | |
| [--symbol-suffix Descriptor] [--gen-dir path/to/gen] [--compress] | |
| If --symbol is omitted, it is derived from the basename of --output: the | |
| substring before the first '.' is title-cased with underscores stripped, then | |
| --symbol-suffix is appended (e.g. test_messages.descriptor.h with suffix | |
| 'Descriptor' produces TestMessagesDescriptor). | |
| If --include-guard is omitted, it is derived from --output: when --gen-dir is | |
| given the path is taken relative to it, and the result has separators/dots | |
| replaced with underscores, is uppercased, and gets a trailing '_'. | |
| """ | |
| from __future__ import absolute_import | |
| from __future__ import division | |
| from __future__ import print_function | |
| import argparse | |
| import os | |
| import re | |
| import sys | |
| import textwrap | |
| import zlib | |
| _HEADER_TEMPLATE = """/* | |
| * Copyright (C) 2020 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #ifndef {include_guard} | |
| #define {include_guard} | |
| #include <stddef.h> | |
| #include <stdint.h> | |
| #include <array> | |
| namespace {namespace} {{ | |
| inline constexpr std::array<uint8_t, {size}> k{symbol}{{ | |
| {binary}}}; | |
| }} // namespace {namespace} | |
| #endif // {include_guard} | |
| """ | |
| def _format_byte_literal(data): | |
| # Match the historical output of gen_cc_proto_descriptor.py exactly so | |
| # consumers see byte-identical headers after the extraction. | |
| try: | |
| ord(data[0]) | |
| ordinal = ord | |
| except TypeError: | |
| ordinal = lambda x: x | |
| binary = '{' + ', '.join('{0:#04x}'.format(ordinal(c)) for c in data) + '}' | |
| return textwrap.fill( | |
| binary, width=80, initial_indent=' ', subsequent_indent=' ') | |
| def derive_symbol(output_path, suffix=''): | |
| """Title-case the part of basename(output_path) before the first '.'. | |
| e.g. 'test_messages.descriptor.h' with suffix 'Descriptor' → | |
| 'TestMessagesDescriptor'. | |
| """ | |
| base = os.path.basename(output_path).split('.', 1)[0] | |
| return base.title().replace('_', '') + suffix | |
| def derive_include_guard(output_path, gen_dir=''): | |
| """Compute a C++ include guard token from output_path. | |
| When gen_dir is provided, the output is taken relative to it first. The | |
| resulting path has '/', '\\\\' and '.' replaced with '_', is uppercased, | |
| and gets a trailing '_'. | |
| """ | |
| rel = os.path.relpath(output_path, | |
| gen_dir) if gen_dir else os.path.basename(output_path) | |
| return re.sub(r'[^A-Z0-9_]', '_', rel.upper()) + '_' | |
| def emit_array(data, output_path, *, symbol, namespace, include_guard): | |
| """Write `data` to `output_path` as a constexpr std::array<uint8_t, N>.""" | |
| binary = _format_byte_literal(data) | |
| with open(output_path, 'wb') as f: | |
| f.write( | |
| _HEADER_TEMPLATE.format( | |
| include_guard=include_guard, | |
| namespace=namespace, | |
| symbol=symbol, | |
| size=len(data), | |
| binary=binary, | |
| ).encode()) | |
| def emit_compressed_array(data, | |
| output_path, | |
| *, | |
| symbol, | |
| namespace, | |
| include_guard, | |
| level=9): | |
| """Like emit_array, but zlib-compresses `data` first. | |
| Consumers must inflate the resulting array at runtime. The uncompressed | |
| size is not encoded — pass `level=zlib.Z_BEST_COMPRESSION` for max ratio | |
| (the default). | |
| """ | |
| emit_array( | |
| zlib.compress(data, level), | |
| output_path, | |
| symbol=symbol, | |
| namespace=namespace, | |
| include_guard=include_guard) | |
| def _main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument('--output', required=True, help='Path to .h to emit.') | |
| parser.add_argument( | |
| '--namespace', required=True, help='C++ namespace for the array.') | |
| parser.add_argument( | |
| '--symbol', | |
| default=None, | |
| help='Array symbol name (sans leading "k"). ' | |
| 'Default: derived from --output.') | |
| parser.add_argument( | |
| '--symbol-suffix', | |
| default='', | |
| help='Suffix appended when --symbol is derived from --output.') | |
| parser.add_argument( | |
| '--include-guard', | |
| default=None, | |
| help='Include guard token. Default: derived from --output.') | |
| parser.add_argument( | |
| '--gen-dir', | |
| default='', | |
| help='Build gen dir; used to make --output relative when ' | |
| 'deriving --include-guard.') | |
| parser.add_argument( | |
| '--compress', | |
| action='store_true', | |
| help='zlib-compress the bytes before embedding.') | |
| parser.add_argument('input', help='Path to bytes file.') | |
| args = parser.parse_args() | |
| symbol = args.symbol if args.symbol is not None else derive_symbol( | |
| args.output, args.symbol_suffix) | |
| include_guard = ( | |
| args.include_guard if args.include_guard is not None else | |
| derive_include_guard(args.output, args.gen_dir)) | |
| with open(args.input, 'rb') as f: | |
| data = f.read() | |
| emit = emit_compressed_array if args.compress else emit_array | |
| emit( | |
| data, | |
| args.output, | |
| symbol=symbol, | |
| namespace=args.namespace, | |
| include_guard=include_guard) | |
| return 0 | |
| if __name__ == '__main__': | |
| sys.exit(_main()) |