blob: 470a28ae8962b2c3ea688288a07e8fd472a0a766 [file] [log] [blame]
#!/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)