blob: add698f9cc0bbddcfa1bbdc8f1bff6243f74fd5e [file] [log] [blame]
# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Script that invokes protoc to generate code for .proto files."""
import argparse
import logging
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from typing import Callable, Dict, List, Optional, Tuple, Union
# Make sure dependencies are optional, since this script may be run when
# installing Python package dependencies through GN.
try:
from pw_cli.log import install as setup_logging
except ImportError:
from logging import basicConfig as setup_logging # type: ignore
_LOG = logging.getLogger(__name__)
_COMMON_FLAGS = ('--experimental_allow_proto3_optional', )
def _argument_parser() -> argparse.ArgumentParser:
"""Registers the script's arguments on an argument parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--language',
required=True,
choices=DEFAULT_PROTOC_ARGS,
help='Output language')
parser.add_argument('--plugin-path',
type=Path,
help='Path to the protoc plugin')
parser.add_argument('--include-file',
type=argparse.FileType('r'),
help='File containing additional protoc include paths')
parser.add_argument('--out-dir',
type=Path,
required=True,
help='Output directory for generated code')
parser.add_argument('--compile-dir',
type=Path,
required=True,
help='Root path for compilation')
parser.add_argument('--sources',
type=Path,
nargs='+',
help='Input protobuf files')
return parser
def protoc_cc_args(args: argparse.Namespace) -> Tuple[str, ...]:
return _COMMON_FLAGS + (
'--plugin',
f'protoc-gen-custom={args.plugin_path}',
'--custom_out',
args.out_dir,
)
def protoc_go_args(args: argparse.Namespace) -> Tuple[str, ...]:
return _COMMON_FLAGS + (
'--go_out',
f'plugins=grpc:{args.out_dir}',
)
def protoc_nanopb_args(args: argparse.Namespace) -> Tuple[str, ...]:
# nanopb needs to know of the include path to parse *.options files
return _COMMON_FLAGS + (
'--plugin',
f'protoc-gen-nanopb={args.plugin_path}',
# nanopb_opt provides the flags to use for nanopb_out. Windows doesn't
# like when you merge the two using the `flag,...:out` syntax. Use
# Posix-style paths since backslashes on Windows are treated like
# escape characters.
f'--nanopb_opt=-I{args.compile_dir.as_posix()}',
f'--nanopb_out={args.out_dir}',
)
def protoc_nanopb_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
return _COMMON_FLAGS + (
'--plugin',
f'protoc-gen-custom={args.plugin_path}',
'--custom_out',
args.out_dir,
)
def protoc_raw_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
return _COMMON_FLAGS + (
'--plugin',
f'protoc-gen-custom={args.plugin_path}',
'--custom_out',
args.out_dir,
)
def protoc_python_args(args: argparse.Namespace) -> Tuple[str, ...]:
return _COMMON_FLAGS + (
'--python_out',
args.out_dir,
'--mypy_out',
args.out_dir,
)
_DefaultArgsFunction = Callable[[argparse.Namespace], Tuple[str, ...]]
# Default additional protoc arguments for each supported language.
# TODO(frolv): Make these overridable with a command-line argument.
DEFAULT_PROTOC_ARGS: Dict[str, _DefaultArgsFunction] = {
'pwpb': protoc_cc_args,
'go': protoc_go_args,
'nanopb': protoc_nanopb_args,
'nanopb_rpc': protoc_nanopb_rpc_args,
'raw_rpc': protoc_raw_rpc_args,
'python': protoc_python_args,
}
# Languages that protoc internally supports.
BUILTIN_PROTOC_LANGS = ('go', 'python')
def main() -> int:
"""Runs protoc as configured by command-line arguments."""
parser = _argument_parser()
args = parser.parse_args()
if args.plugin_path is None and args.language not in BUILTIN_PROTOC_LANGS:
parser.error(
f'--plugin-path is required for --language {args.language}')
args.out_dir.mkdir(parents=True, exist_ok=True)
include_paths: List[str] = []
if args.include_file:
include_paths = [f'-I{line.strip()}' for line in args.include_file]
wrapper_script: Optional[Path] = None
# On Windows, use a .bat version of the plugin if it exists or create a .bat
# wrapper to use if none exists.
if os.name == 'nt' and args.plugin_path:
if args.plugin_path.with_suffix('.bat').exists():
args.plugin_path = args.plugin_path.with_suffix('.bat')
_LOG.debug('Using Batch plugin %s', args.plugin_path)
else:
with tempfile.NamedTemporaryFile('w', suffix='.bat',
delete=False) as file:
file.write(f'@echo off\npython {args.plugin_path.resolve()}\n')
args.plugin_path = wrapper_script = Path(file.name)
_LOG.debug('Using generated plugin wrapper %s', args.plugin_path)
cmd: Tuple[Union[str, Path], ...] = (
'protoc',
f'-I{args.compile_dir}',
*include_paths,
*DEFAULT_PROTOC_ARGS[args.language](args),
*args.sources,
)
try:
process = subprocess.run(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
finally:
if wrapper_script:
wrapper_script.unlink()
if process.returncode != 0:
_LOG.error('Protocol buffer compilation failed!\n%s',
' '.join(str(c) for c in cmd))
sys.stderr.buffer.write(process.stdout)
sys.stderr.flush()
return process.returncode
if __name__ == '__main__':
setup_logging()
sys.exit(main())