| #!/usr/bin/env python3 |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| A simple wrapper for protoc. |
| Script for //third_party/protobuf/proto_library.gni . |
| Features: |
| - Inserts #include for extra header automatically. |
| - Prevents bad proto names. |
| - Works around protoc's bad descriptor file generation. |
| Ninja expects the format: |
| target: deps |
| But protoc just outputs: |
| deps |
| This script adds the "target:" part. |
| """ |
| |
| from __future__ import print_function |
| import argparse |
| import os.path |
| import subprocess |
| import sys |
| |
| PROTOC_INCLUDE_POINT = "// @@protoc_insertion_point(includes)" |
| |
| |
| def FormatGeneratorOptions(options): |
| if not options: |
| return "" |
| if options.endswith(":"): |
| return options |
| return options + ":" |
| |
| |
| def VerifyProtoNames(protos): |
| for filename in protos: |
| if "-" in filename: |
| raise RuntimeError("Proto file names must not contain hyphens " |
| "(see http://crbug.com/386125 for more information).") |
| |
| |
| def StripProtoExtension(filename): |
| if not filename.endswith(".proto"): |
| raise RuntimeError(f"Invalid proto filename extension: {filename} .") |
| return filename.rsplit(".", 1)[0] |
| |
| |
| def WriteIncludes(headers, include): |
| for filename in headers: |
| include_point_found = False |
| contents = [] |
| with open(filename, encoding="utf-8") as f: |
| for line in f: |
| stripped_line = line.strip() |
| contents.append(stripped_line) |
| if stripped_line == PROTOC_INCLUDE_POINT: |
| if include_point_found: |
| raise RuntimeError("Multiple include points found.") |
| include_point_found = True |
| extra_statement = f'#include "{include}"' |
| contents.append(extra_statement) |
| |
| if not include_point_found: |
| raise RuntimeError(f"Include point not found in header: {filename} .") |
| |
| with open(filename, "w", encoding="utf-8") as f: |
| for line in contents: |
| print(line, file=f) |
| |
| |
| def main(argv): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "--protoc", required=True, help="Relative path to compiler.") |
| |
| parser.add_argument( |
| "--proto-in-dir", |
| required=True, |
| help="Base directory with source protos.") |
| parser.add_argument( |
| "--cc-out-dir", help="Output directory for standard C++ generator.") |
| parser.add_argument( |
| "--py-out-dir", help="Output directory for standard Python generator.") |
| parser.add_argument( |
| "--js-out-dir", help="Output directory for standard JS generator.") |
| parser.add_argument( |
| "--plugin-out-dir", help="Output directory for custom generator plugin.") |
| |
| parser.add_argument( |
| "--enable-kythe-annotations", |
| action="store_true", |
| help="Enable generation of Kythe kzip, used for " |
| "codesearch.") |
| parser.add_argument( |
| "--plugin", help="Relative path to custom generator plugin.") |
| parser.add_argument( |
| "--plugin-options", help="Custom generator plugin options.") |
| parser.add_argument("--cc-options", help="Standard C++ generator options.") |
| parser.add_argument( |
| "--include", help="Name of include to insert into generated headers.") |
| parser.add_argument( |
| "--import-dir", |
| action="append", |
| default=[], |
| help="Extra import directory for protos, can be repeated.") |
| parser.add_argument( |
| "--descriptor-set-out", help="Path to write a descriptor.") |
| parser.add_argument( |
| "--descriptor-set-dependency-file", |
| help="Path to write the dependency file for descriptor set.") |
| # The meaning of this flag is flipped compared to the corresponding protoc |
| # flag due to this script previously passing --include_imports. Removing the |
| # --include_imports is likely to have unintended consequences. |
| parser.add_argument( |
| "--exclude-imports", |
| help="Do not include imported files into generated descriptor.", |
| action="store_true", |
| default=False) |
| parser.add_argument( |
| "protos", nargs="+", help="Input protobuf definition file(s).") |
| |
| options = parser.parse_args(argv) |
| |
| proto_dir = os.path.relpath(options.proto_in_dir) |
| protoc_cmd = [os.path.realpath(options.protoc)] |
| |
| protos = options.protos |
| headers = [] |
| VerifyProtoNames(protos) |
| |
| if options.py_out_dir: |
| protoc_cmd += ["--python_out", options.py_out_dir] |
| |
| if options.js_out_dir: |
| protoc_cmd += [ |
| "--js_out", |
| "one_output_file_per_input_file,binary:" + options.js_out_dir |
| ] |
| |
| if options.cc_out_dir: |
| cc_out_dir = options.cc_out_dir |
| cc_options_list = [] |
| if options.enable_kythe_annotations: |
| cc_options_list.extend([ |
| "annotate_headers", "annotation_pragma_name=kythe_metadata", |
| "annotation_guard_name=KYTHE_IS_RUNNING" |
| ]) |
| |
| # cc_options will likely have trailing colon so needs to be inserted at the |
| # end. |
| if options.cc_options: |
| cc_options_list.append(options.cc_options) |
| |
| cc_options = FormatGeneratorOptions(",".join(cc_options_list)) |
| protoc_cmd += ["--cpp_out", cc_options + cc_out_dir] |
| for filename in protos: |
| stripped_name = StripProtoExtension(filename) |
| headers.append(os.path.join(cc_out_dir, stripped_name + ".pb.h")) |
| |
| if options.plugin_out_dir: |
| plugin_options = FormatGeneratorOptions(options.plugin_options) |
| protoc_cmd += [ |
| "--plugin", "protoc-gen-plugin=" + os.path.relpath(options.plugin), |
| "--plugin_out", plugin_options + options.plugin_out_dir |
| ] |
| |
| protoc_cmd += ["--proto_path", proto_dir] |
| for path in options.import_dir: |
| protoc_cmd += ["--proto_path", path] |
| |
| protoc_cmd += [os.path.join(proto_dir, name) for name in protos] |
| |
| if options.descriptor_set_out: |
| protoc_cmd += ["--descriptor_set_out", options.descriptor_set_out] |
| if not options.exclude_imports: |
| protoc_cmd += ["--include_imports"] |
| |
| dependency_file_data = None |
| if options.descriptor_set_out and options.descriptor_set_dependency_file: |
| protoc_cmd += ["--dependency_out", options.descriptor_set_dependency_file] |
| ret = subprocess.call(protoc_cmd) |
| |
| with open(options.descriptor_set_dependency_file, "rb") as f: |
| dependency_file_data = f.read().decode("utf-8") |
| |
| ret = subprocess.call(protoc_cmd) |
| if ret != 0: |
| if ret <= -100: |
| # Windows error codes such as 0xC0000005 and 0xC0000409 are much easier to |
| # recognize and differentiate in hex. In order to print them as unsigned |
| # hex we need to add 4 Gig to them. |
| error_number = f"0x{ret + (1 << 32):08X}" |
| else: |
| error_number = f"{ret}" |
| raise RuntimeError(f"Protoc has returned non-zero status: {error_number}") |
| |
| if dependency_file_data: |
| with open(options.descriptor_set_dependency_file, "w", encoding="utf-8") as f: #pylint: disable=line-too-long |
| f.write(options.descriptor_set_out + ":") |
| f.write(dependency_file_data) |
| |
| if options.include: |
| WriteIncludes(headers, options.include) |
| |
| |
| if __name__ == "__main__": |
| try: |
| main(sys.argv[1:]) |
| except RuntimeError as e: |
| print(e, file=sys.stderr) |
| sys.exit(1) |