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, edition_constants, 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 '--options-file', 64 dest='options_files', 65 metavar='FILE', 66 action='append', 67 default=[], 68 type=Path, 69 help='Append FILE to options file list', 70 ) 71 parser.add_argument( 72 '--no-oneof-callbacks', 73 dest='oneof_callbacks', 74 action='store_false', 75 help='Generate legacy inline oneof members instead of callbacks', 76 ) 77 parser.add_argument( 78 '--no-generic-options-files', 79 dest='generic_options_files', 80 action='store_false', 81 help='If set, only permits the usage of the `.pwpb_options` extension ' 82 'for options files instead of the generic `.options`', 83 ) 84 85 # protoc passes the custom arguments in shell quoted form, separated by 86 # commas. Use shlex to split them, correctly handling quoted sections, with 87 # equivalent options to IFS="," 88 lex = shlex(parameter) 89 lex.whitespace_split = True 90 lex.whitespace = ',' 91 lex.commenters = '' 92 args = list(lex) 93 94 return parser.parse_args(args) 95 96 97def process_proto_request( 98 req: plugin_pb2.CodeGeneratorRequest, res: plugin_pb2.CodeGeneratorResponse 99) -> bool: 100 """Handles a protoc CodeGeneratorRequest message. 101 102 Generates code for the files in the request and writes the output to the 103 specified CodeGeneratorResponse message. 104 105 Args: 106 req: A CodeGeneratorRequest for a proto compilation. 107 res: A CodeGeneratorResponse to populate with the plugin's output. 108 """ 109 110 success = True 111 112 args = parse_parameter_options(req.parameter) 113 for proto_file in req.proto_file: 114 proto_options = options.load_options( 115 args.include_paths, 116 Path(proto_file.name), 117 args.options_files, 118 allow_generic_options_extension=args.generic_options_files, 119 ) 120 121 codegen_options = codegen_pwpb.GeneratorOptions( 122 oneof_callbacks=args.oneof_callbacks, 123 suppress_legacy_namespace=args.no_legacy_namespace, 124 exclude_legacy_snake_case_field_name_enums=( 125 args.exclude_legacy_snake_case_field_name_enums 126 ), 127 ) 128 129 output_files = codegen_pwpb.process_proto_file( 130 proto_file, 131 proto_options, 132 codegen_options, 133 ) 134 135 if output_files is not None: 136 for output_file in output_files: 137 fd = res.file.add() 138 fd.name = output_file.name() 139 fd.content = output_file.content() 140 else: 141 success = False 142 143 return success 144 145 146def main() -> int: 147 """Protobuf compiler plugin entrypoint. 148 149 Reads a CodeGeneratorRequest proto from stdin and writes a 150 CodeGeneratorResponse to stdout. 151 """ 152 data = sys.stdin.buffer.read() 153 request = plugin_pb2.CodeGeneratorRequest.FromString(data) 154 response = plugin_pb2.CodeGeneratorResponse() 155 156 # Declare that this plugin supports optional fields in proto3. 157 response.supported_features |= ( # type: ignore[attr-defined] 158 response.FEATURE_PROTO3_OPTIONAL 159 ) # type: ignore[attr-defined] 160 161 response.supported_features |= edition_constants.FEATURE_SUPPORTS_EDITIONS 162 163 if hasattr(response, 'minimum_edition'): 164 response.minimum_edition = ( # type: ignore[attr-defined] 165 edition_constants.Edition.EDITION_PROTO2.value 166 ) 167 response.maximum_edition = ( # type: ignore[attr-defined] 168 edition_constants.Edition.EDITION_2023.value 169 ) 170 171 if not process_proto_request(request, response): 172 print('pwpb failed to generate protobuf code', file=sys.stderr) 173 return 1 174 175 sys.stdout.buffer.write(response.SerializeToString()) 176 return 0 177 178 179if __name__ == '__main__': 180 sys.exit(main()) 181