1# Protocol Buffers - Google's data interchange format 2# Copyright 2008 Google Inc. All rights reserved. 3# 4# Use of this source code is governed by a BSD-style 5# license that can be found in the LICENSE file or at 6# https://developers.google.com/open-source/licenses/bsd 7 8"""Implements the generate_py_protobufs command.""" 9 10__author__ = 'dlj@google.com (David L. Jones)' 11 12import glob 13import os 14import shutil 15import subprocess 16import sys 17from setuptools import Command 18from setuptools.errors import OptionError 19 20class generate_py_protobufs(Command): 21 """Generates Python sources for .proto files.""" 22 23 description = 'Generate Python sources for .proto files' 24 user_options = [ 25 ('extra-proto-paths=', None, 26 'Additional paths to resolve imports in .proto files.'), 27 28 ('protoc=', None, 29 'Path to a specific `protoc` command to use.'), 30 ] 31 boolean_options = ['recurse'] 32 33 def initialize_options(self): 34 """Sets the defaults for the command options.""" 35 self.source_dir = None 36 self.proto_root_path = None 37 self.extra_proto_paths = [] 38 self.output_dir = '.' 39 self.proto_files = None 40 self.recurse = True 41 self.protoc = None 42 43 def finalize_options(self): 44 """Sets the final values for the command options. 45 46 Defaults were set in `initialize_options`, but could have been changed 47 by command-line options or by other commands. 48 """ 49 self.ensure_dirname('source_dir') 50 self.ensure_string_list('extra_proto_paths') 51 52 if self.output_dir is None: 53 self.output_dir = '.' 54 self.ensure_dirname('output_dir') 55 56 # SUBTLE: if 'source_dir' is a subdirectory of any entry in 57 # 'extra_proto_paths', then in general, the shortest --proto_path prefix 58 # (and the longest relative .proto filenames) must be used for 59 # correctness. For example, consider: 60 # 61 # source_dir = 'a/b/c' 62 # extra_proto_paths = ['a/b', 'x/y'] 63 # 64 # In this case, we must ensure that a/b/c/d/foo.proto resolves 65 # canonically as c/d/foo.proto, not just d/foo.proto. Otherwise, this 66 # import: 67 # 68 # import "c/d/foo.proto"; 69 # 70 # would result in different FileDescriptor.name keys from "d/foo.proto". 71 # That will cause all the definitions in the file to be flagged as 72 # duplicates, with an error similar to: 73 # 74 # c/d/foo.proto: "packagename.MessageName" is already defined in file "d/foo.proto" 75 # 76 # For paths in self.proto_files, we transform them to be relative to 77 # self.proto_root_path, which may be different from self.source_dir. 78 # 79 # Although the order of --proto_paths is significant, shadowed filenames 80 # are errors: if 'a/b/c.proto' resolves to different files under two 81 # different --proto_path arguments, then the path is rejected as an 82 # error. (Implementation note: this is enforced in protoc's 83 # DiskSourceTree class.) 84 85 if self.proto_root_path is None: 86 self.proto_root_path = os.path.normpath(self.source_dir) 87 for root_candidate in self.extra_proto_paths: 88 root_candidate = os.path.normpath(root_candidate) 89 if self.proto_root_path.startswith(root_candidate): 90 self.proto_root_path = root_candidate 91 if self.proto_root_path != self.source_dir: 92 self.announce('using computed proto_root_path: ' + self.proto_root_path, level=2) 93 94 if not self.source_dir.startswith(self.proto_root_path): 95 raise OptionError( 96 'source_dir ' 97 + self.source_dir 98 + ' is not under proto_root_path ' 99 + self.proto_root_path 100 ) 101 102 if self.proto_files is None: 103 files = glob.glob(os.path.join(self.source_dir, '*.proto')) 104 if self.recurse: 105 files.extend( 106 glob.glob( 107 os.path.join(self.source_dir, '**', '*.proto'), recursive=True 108 ) 109 ) 110 self.proto_files = [ 111 f.partition(self.proto_root_path + os.path.sep)[-1] for f in files 112 ] 113 if not self.proto_files: 114 raise OptionError('no .proto files were found under ' + self.source_dir) 115 116 self.ensure_string_list('proto_files') 117 118 if self.protoc is None: 119 self.protoc = os.getenv('PROTOC') 120 if self.protoc is None: 121 self.protoc = shutil.which('protoc') 122 123 def run(self): 124 # All proto file paths were adjusted in finalize_options to be relative 125 # to self.proto_root_path. 126 proto_paths = ['--proto_path=' + self.proto_root_path] 127 proto_paths.extend(['--proto_path=' + x for x in self.extra_proto_paths]) 128 129 # Run protoc. 130 subprocess.run( 131 [ 132 self.protoc, 133 '--python_out=' + self.output_dir, 134 ] 135 + proto_paths 136 + self.proto_files 137 ) 138