• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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