1 # Copyright 2020 The Pigweed Authors
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 # use this file except in compliance with the License. You may obtain a copy of
7 # https://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations under
14 """Script that invokes protoc to generate code for .proto files."""
19 from pathlib import Path
24 from typing import Callable, Dict, Optional, Tuple
26 # Make sure dependencies are optional, since this script may be run when
27 # installing Python package dependencies through GN.
29 from pw_cli.log import install as setup_logging
31 from logging import basicConfig as setup_logging # type: ignore
33 _LOG = logging.getLogger(__name__)
35 _COMMON_FLAGS = ('--experimental_allow_proto3_optional', )
39 parser: Optional[argparse.ArgumentParser] = None
40 ) -> argparse.ArgumentParser:
41 """Registers the script's arguments on an argument parser."""
44 parser = argparse.ArgumentParser(description=__doc__)
46 parser.add_argument('--language',
48 choices=DEFAULT_PROTOC_ARGS,
49 help='Output language')
50 parser.add_argument('--plugin-path',
52 help='Path to the protoc plugin')
53 parser.add_argument('--include-path',
55 help='Include path for proto compilation')
56 parser.add_argument('--include-file',
57 type=argparse.FileType('r'),
58 help='File containing additional protoc include paths')
59 parser.add_argument('--out-dir',
61 help='Output directory for generated code')
62 parser.add_argument('protos',
65 help='Input protobuf files')
70 def protoc_cc_args(args: argparse.Namespace) -> Tuple[str, ...]:
71 return _COMMON_FLAGS + (
73 f'protoc-gen-custom={args.plugin_path}',
79 def protoc_go_args(args: argparse.Namespace) -> Tuple[str, ...]:
80 return _COMMON_FLAGS + (
82 f'plugins=grpc:{args.out_dir}',
86 def protoc_nanopb_args(args: argparse.Namespace) -> Tuple[str, ...]:
87 # nanopb needs to know of the include path to parse *.options files
88 return _COMMON_FLAGS + (
90 f'protoc-gen-nanopb={args.plugin_path}',
91 # nanopb_opt provides the flags to use for nanopb_out. Windows doesn't
92 # like when you merge the two using the `flag,...:out` syntax.
93 f'--nanopb_opt=-I{args.include_path}',
94 f'--nanopb_out={args.out_dir}',
98 def protoc_nanopb_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
99 return _COMMON_FLAGS + (
101 f'protoc-gen-custom={args.plugin_path}',
107 def protoc_raw_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
108 return _COMMON_FLAGS + (
110 f'protoc-gen-custom={args.plugin_path}',
116 def protoc_python_args(args: argparse.Namespace) -> Tuple[str, ...]:
117 return _COMMON_FLAGS + (
125 _DefaultArgsFunction = Callable[[argparse.Namespace], Tuple[str, ...]]
127 # Default additional protoc arguments for each supported language.
128 # TODO(frolv): Make these overridable with a command-line argument.
129 DEFAULT_PROTOC_ARGS: Dict[str, _DefaultArgsFunction] = {
130 'pwpb': protoc_cc_args,
131 'go': protoc_go_args,
132 'nanopb': protoc_nanopb_args,
133 'nanopb_rpc': protoc_nanopb_rpc_args,
134 'raw_rpc': protoc_raw_rpc_args,
135 'python': protoc_python_args,
138 # Languages that protoc internally supports.
139 BUILTIN_PROTOC_LANGS = ('go', 'python')
143 """Runs protoc as configured by command-line arguments."""
145 parser = argument_parser()
146 args = parser.parse_args()
148 if args.plugin_path is None and args.language not in BUILTIN_PROTOC_LANGS:
150 f'--plugin-path is required for --language {args.language}')
152 os.makedirs(args.out_dir, exist_ok=True)
154 include_paths = [f'-I{line.strip()}' for line in args.include_file]
156 wrapper_script: Optional[Path] = None
158 # On Windows, use a .bat version of the plugin if it exists or create a .bat
159 # wrapper to use if none exists.
160 if os.name == 'nt' and args.plugin_path:
161 if args.plugin_path.with_suffix('.bat').exists():
162 args.plugin_path = args.plugin_path.with_suffix('.bat')
163 _LOG.debug('Using Batch plugin %s', args.plugin_path)
165 with tempfile.NamedTemporaryFile('w', suffix='.bat',
166 delete=False) as file:
167 file.write(f'@echo off\npython {args.plugin_path.resolve()}\n')
169 args.plugin_path = wrapper_script = Path(file.name)
170 _LOG.debug('Using generated plugin wrapper %s', args.plugin_path)
173 process = subprocess.run(
176 f'-I{args.include_path}',
178 *DEFAULT_PROTOC_ARGS[args.language](args),
181 stdout=subprocess.PIPE,
182 stderr=subprocess.STDOUT,
186 wrapper_script.unlink()
188 if process.returncode != 0:
189 sys.stderr.buffer.write(process.stdout)
192 return process.returncode
195 if __name__ == '__main__':