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 63 # protoc passes the custom arguments in shell quoted form, separated by 64 # commas. Use shlex to split them, correctly handling quoted sections, with 65 # equivalent options to IFS="," 66 lex = shlex(parameter) 67 lex.whitespace_split = True 68 lex.whitespace = ',' 69 lex.commenters = '' 70 args = list(lex) 71 72 return parser.parse_args(args) 73 74 75def process_proto_request( 76 req: plugin_pb2.CodeGeneratorRequest, res: plugin_pb2.CodeGeneratorResponse 77) -> None: 78 """Handles a protoc CodeGeneratorRequest message. 79 80 Generates code for the files in the request and writes the output to the 81 specified CodeGeneratorResponse message. 82 83 Args: 84 req: A CodeGeneratorRequest for a proto compilation. 85 res: A CodeGeneratorResponse to populate with the plugin's output. 86 """ 87 args = parse_parameter_options(req.parameter) 88 for proto_file in req.proto_file: 89 proto_options = options.load_options( 90 args.include_paths, Path(proto_file.name) 91 ) 92 output_files = codegen_pwpb.process_proto_file( 93 proto_file, 94 proto_options, 95 suppress_legacy_namespace=args.no_legacy_namespace, 96 exclude_legacy_snake_case_field_name_enums=( 97 args.exclude_legacy_snake_case_field_name_enums 98 ), 99 ) 100 for output_file in output_files: 101 fd = res.file.add() 102 fd.name = output_file.name() 103 fd.content = output_file.content() 104 105 106def main() -> int: 107 """Protobuf compiler plugin entrypoint. 108 109 Reads a CodeGeneratorRequest proto from stdin and writes a 110 CodeGeneratorResponse to stdout. 111 """ 112 data = sys.stdin.buffer.read() 113 request = plugin_pb2.CodeGeneratorRequest.FromString(data) 114 response = plugin_pb2.CodeGeneratorResponse() 115 process_proto_request(request, response) 116 117 # Declare that this plugin supports optional fields in proto3. 118 response.supported_features |= ( # type: ignore[attr-defined] 119 response.FEATURE_PROTO3_OPTIONAL 120 ) # type: ignore[attr-defined] 121 122 sys.stdout.buffer.write(response.SerializeToString()) 123 return 0 124 125 126if __name__ == '__main__': 127 sys.exit(main()) 128