1#!/usr/bin/env python3 2# Copyright 2020 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15"""pw_protobuf compiler plugin. 16 17This file implements a protobuf compiler plugin which generates C++ headers for 18protobuf messages in the pw_protobuf format. 19""" 20 21import sys 22from argparse import ArgumentParser, Namespace 23from pathlib import Path 24from shlex import shlex 25 26from google.protobuf.compiler import plugin_pb2 27 28from pw_protobuf import codegen_pwpb, options 29 30 31def parse_parameter_options(parameter: str) -> Namespace: 32 """Parses parameters passed through from protoc. 33 34 These parameters come in via passing `--${NAME}_out` parameters to protoc, 35 where protoc-gen-${NAME} is the supplied name of the plugin. At time of 36 writing, Blaze uses --pwpb_opt, whereas the script for GN uses --custom_opt. 37 """ 38 parser = ArgumentParser() 39 parser.add_argument( 40 '-I', 41 '--include-path', 42 dest='include_paths', 43 metavar='DIR', 44 action='append', 45 default=[], 46 type=Path, 47 help='Append DIR to options file search path', 48 ) 49 parser.add_argument( 50 '--no-legacy-namespace', 51 dest='no_legacy_namespace', 52 action='store_true', 53 help='If set, suppresses `using namespace` declarations, which ' 54 'disallows use of the legacy non-prefixed namespace', 55 ) 56 parser.add_argument( 57 '--exclude-legacy-snake-case-field-name-enums', 58 dest='exclude_legacy_snake_case_field_name_enums', 59 action='store_true', 60 help='Do not generate legacy SNAKE_CASE names for field name enums.', 61 ) 62 parser.add_argument( 63 '--import-prefix', 64 dest='import_prefix', 65 help='Path prefix expected to be prepended to proto_file. If set ' 66 'this prefix will be stripped from the proto filename before ' 67 'performing .options file lookup', 68 ) 69 70 # protoc passes the custom arguments in shell quoted form, separated by 71 # commas. Use shlex to split them, correctly handling quoted sections, with 72 # equivalent options to IFS="," 73 lex = shlex(parameter) 74 lex.whitespace_split = True 75 lex.whitespace = ',' 76 lex.commenters = '' 77 args = list(lex) 78 79 return parser.parse_args(args) 80 81 82def process_proto_request( 83 req: plugin_pb2.CodeGeneratorRequest, res: plugin_pb2.CodeGeneratorResponse 84) -> None: 85 """Handles a protoc CodeGeneratorRequest message. 86 87 Generates code for the files in the request and writes the output to the 88 specified CodeGeneratorResponse message. 89 90 Args: 91 req: A CodeGeneratorRequest for a proto compilation. 92 res: A CodeGeneratorResponse to populate with the plugin's output. 93 """ 94 args = parse_parameter_options(req.parameter) 95 for proto_file in req.proto_file: 96 proto_options = options.load_options( 97 args.include_paths, Path(proto_file.name), args.import_prefix 98 ) 99 output_files = codegen_pwpb.process_proto_file( 100 proto_file, 101 proto_options, 102 suppress_legacy_namespace=args.no_legacy_namespace, 103 exclude_legacy_snake_case_field_name_enums=( 104 args.exclude_legacy_snake_case_field_name_enums 105 ), 106 ) 107 for output_file in output_files: 108 fd = res.file.add() 109 fd.name = output_file.name() 110 fd.content = output_file.content() 111 112 113def main() -> int: 114 """Protobuf compiler plugin entrypoint. 115 116 Reads a CodeGeneratorRequest proto from stdin and writes a 117 CodeGeneratorResponse to stdout. 118 """ 119 data = sys.stdin.buffer.read() 120 request = plugin_pb2.CodeGeneratorRequest.FromString(data) 121 response = plugin_pb2.CodeGeneratorResponse() 122 process_proto_request(request, response) 123 124 # Declare that this plugin supports optional fields in proto3. 125 response.supported_features |= ( # type: ignore[attr-defined] 126 response.FEATURE_PROTO3_OPTIONAL 127 ) # type: ignore[attr-defined] 128 129 sys.stdout.buffer.write(response.SerializeToString()) 130 return 0 131 132 133if __name__ == '__main__': 134 sys.exit(main()) 135