github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/parse/rules/proto_rules.build_defs (about)

     1  """Build rules for compiling protocol buffers & gRPC service stubs.
     2  
     3  Note that these are some of the most complex of our built-in build rules,
     4  because of their cross-language nature. Each proto_library rule declares a set of
     5  sub-rules to run protoc & the appropriate java_library, go_library rules etc. Users
     6  shouldn't worry about those sub-rules and just declare a dependency directly on
     7  the proto_library rule to get its appropriate outputs.
     8  
     9  It is possible to add extra languages to these for generation. This is accomplished
    10  via the 'languages' argument; this can be simply a list of languages to build, but
    11  can also be a mapping of language name -> definition of how to build it. The definition
    12  should be the return value of proto_language.
    13  """
    14  
    15  _DEFAULT_GRPC_LABELS = ['grpc']
    16  
    17  
    18  def proto_library(name:str, srcs:list, deps:list=[], visibility:list=None, labels:list&features&tags=[],
    19                    languages:list|dict=None, test_only:bool&testonly=False, root_dir:str='', protoc_flags:list=[]):
    20      """Compile a .proto file to generated code for various languages.
    21  
    22      Args:
    23        name (str): Name of the rule
    24        srcs (list): Input .proto files.
    25        deps (list): Dependencies
    26        visibility (list): Visibility specification for the rule.
    27        labels (list): List of labels to apply to this rule.
    28        languages (list | dict): List of languages to generate rules for, chosen from the set {cc, py, go, java, js}.
    29                                 Alternatively, a dict mapping the language name to a definition of how to build it
    30                                 (see proto_language for more details of the values).
    31        test_only (bool): If True, can only be used in test rules.
    32        root_dir (str): The directory that the protos are compiled relative to. Useful if your
    33                        proto files have import statements that are not relative to the repo root.
    34        protoc_flags (list): Additional flags to pass to protoc. Note that these are inherited by
    35                             further rules that depend on this one (because in nearly all cases that
    36                             will be necessary for them to build too).
    37      """
    38      languages = _merge_dicts(languages or CONFIG.PROTO_LANGUAGES, proto_languages())
    39  
    40      # We detect output names for normal sources, but will have to do a post-build rule for
    41      # any input rules. We could just do that for everything but it's nicer to avoid them
    42      # when possible since they obscure what's going on with the build graph.
    43      file_srcs = [src for src in srcs if src[0] not in [':', '/']]
    44      need_post_build = file_srcs != srcs
    45      provides = {'proto': ':_%s#proto' % name}
    46  
    47      lang_plugins = sorted(languages.items())
    48      plugins = [plugin for _, plugin in lang_plugins]
    49      file_extensions = []
    50      outs = {ext_lang: [src.replace('.proto', ext) for src in file_srcs for ext in exts]
    51                        if plugin['use_file_names'] else []
    52              for language, plugin in lang_plugins for ext_lang, exts in plugin['extensions'].items()}
    53      flags = [' '.join(plugin['protoc_flags']) for plugin in plugins] + protoc_flags
    54      tools = {lang: plugin.get('tools') for lang, plugin in lang_plugins}
    55      tools['protoc'] = [CONFIG.PROTOC_TOOL]
    56      cmd = '$TOOLS_PROTOC ' + ' '.join(flags)
    57      if root_dir:
    58          cmd = 'export RD="%s"; cd $RD; %s ${SRCS//$RD\\//} && cd $TMP_DIR' % (root_dir, cmd.replace('$TMP_DIR', '.'))
    59      else:
    60          cmd += ' ${SRCS}'
    61      cmds = [cmd, '(mv -f ${PKG}/* .; true)']
    62  
    63      # protoc_flags are applied transitively to dependent rules via labels.
    64      labels += ['protoc:' + flag for flag in protoc_flags]
    65  
    66      # TODO(pebers): genericise these bits?
    67      if ('py' in languages) and CONFIG.PROTO_PYTHON_PACKAGE:
    68          cmds.append('find . -name "*_pb2.py" | xargs sed -i -e "s/from google.protobuf/from %s/g"' %
    69                      CONFIG.PROTO_PYTHON_PACKAGE)
    70      if 'go' in languages:
    71          base_path = get_base_path()
    72          labels += ['proto:go-map: %s/%s=%s/%s' % (base_path, src, base_path, name) for src in srcs
    73                     if not src.startswith(':') and not src.startswith('/')]
    74  
    75      # Figure out which languages we need to detect output files for.
    76      # This always happens for Java, and will be needed for any other language where the inputs aren't plain files.
    77      post_build = None
    78      search_extensions = [(lang, exts) for plugin in plugins
    79                           for lang, exts in sorted(plugin['extensions'].items())
    80                           if need_post_build or not plugin['use_file_names']]
    81      if search_extensions:
    82          all_exts = [ext for _, exts in search_extensions for ext in exts]
    83          cmds.append('find . %s | sort' % ' -or '.join(['-name "*%s"' % ext for ext in all_exts]))
    84          post_build = _annotate_outs(search_extensions)
    85  
    86      # Plugins can declare their own pre-build functions. If there are any, we need to apply them all in sequence.
    87      pre_build_functions = [plugin['pre_build'] for plugin in plugins if plugin['pre_build']]
    88      pre_build_functions.append(_collect_transitive_labels)
    89      pre_build = lambda rule: [fn(rule) for fn in pre_build_functions]
    90      protoc_rule = build_rule(
    91          name = name,
    92          tag = 'protoc',
    93          srcs = srcs,
    94          outs = outs,
    95          cmd = ' && '.join(cmds),
    96          deps = deps,
    97          tools = tools,
    98          requires = ['proto'],
    99          pre_build = pre_build,
   100          post_build = post_build,
   101          labels = labels,
   102          needs_transitive_deps = True,
   103          test_only = test_only,
   104          visibility = visibility,
   105      )
   106  
   107      for language, plugin in lang_plugins:
   108          lang_name = '_%s#%s' % (name, language)
   109          provides[language] = plugin['func'](
   110              name = lang_name,
   111              srcs = ['%s|%s' % (protoc_rule, language)],
   112              deps = deps + plugin['deps'],
   113              test_only = test_only
   114          ) or (':' + lang_name)
   115          # TODO(pebers): find a way of genericising this too...
   116          if language == 'cc':
   117              provides['cc_hdrs'] = provides['cc'].replace('#cc', '#cc_hdrs')
   118  
   119      # This simply collects the sources, it's used for other proto_library rules to depend on.
   120      filegroup(
   121          name = '_%s#proto' % name,
   122          srcs = srcs,
   123          visibility = visibility,
   124          exported_deps = deps,
   125          labels = labels,
   126          requires = ['proto'],
   127          output_is_complete = False,
   128          test_only = test_only,
   129      )
   130      # This is the final rule that directs dependencies to the appropriate language.
   131      filegroup(
   132          name = name,
   133          deps = sorted(provides.values()),
   134          provides = provides,
   135          visibility = visibility,
   136          labels = labels,
   137          test_only = test_only,
   138      )
   139  
   140  
   141  def grpc_library(name:str, srcs:list, deps:list=None, visibility:list=None, languages:list|dict=None,
   142                   labels:list&features&tags=[], test_only:bool&testonly=False, root_dir:str='', protoc_flags:list=None):
   143      """Defines a rule for a grpc library.
   144  
   145      Args:
   146        name (str): Name of the rule
   147        srcs (list): Input .proto files.
   148        deps (list): Dependencies (other grpc_library or proto_library rules)
   149        visibility (list): Visibility specification for the rule.
   150        languages (list | dict): List of languages to generate rules for, chosen from the set {cc, py, go, java}.
   151                                 Alternatively, a dict mapping the language name to a definition of how to build it
   152                                 (see proto_language for more details of the values).
   153        labels (list): List of labels to apply to this rule.
   154        test_only (bool): If True, this rule can only be used by test rules.
   155        root_dir (str): The directory that the protos are compiled relative to. Useful if your
   156                        proto files have import statements that are not relative to the repo root.
   157        protoc_flags (list): Additional flags to pass to protoc.
   158      """
   159      proto_library(
   160          name = name,
   161          srcs = srcs,
   162          deps = deps,
   163          languages = _merge_dicts(languages or CONFIG.PROTO_LANGUAGES, grpc_languages()),
   164          visibility = visibility,
   165          labels = labels + _DEFAULT_GRPC_LABELS,
   166          test_only = test_only,
   167          root_dir = root_dir,
   168          protoc_flags = protoc_flags,
   169      )
   170  
   171  
   172  def _go_path_mapping(grpc):
   173      """Used to update the Go path mapping; by default it doesn't really import in the way we want."""
   174      grpc_plugin = 'plugins=grpc,' if grpc else ''
   175      def _map_go_paths(rule_name):
   176          mapping = ',M'.join(get_labels(rule_name, 'proto:go-map:'))
   177          cmd = get_command(rule_name)
   178          new_cmd = cmd.replace('--go_out=', '--go_out=%sM%s:' % (grpc_plugin, mapping))
   179          set_command(rule_name, new_cmd)
   180      return _map_go_paths
   181  
   182  
   183  def proto_language(language:str, extensions:list|dict, func:function, use_file_names:bool=True, protoc_flags:list=None,
   184                     tools:list=None, deps:list=None, pre_build:function=None, proto_language:str=''):
   185      """Returns the definition of how to build a particular language for proto_library.
   186  
   187      Args:
   188        language (str): Name of the language (as we would name it).
   189        extensions (list | dict): File extensions that will get generated.
   190        func (function): Function defining how to build the rule. It will receive the following arguments:
   191              name: Suggested name of the rule.
   192              srcs: Source files, as generated by protoc.
   193              deps: Suggested dependencies.
   194              test_only: True if the original rule was marked as test_only.
   195              It should return the name of any rule that it wants added to the final list of provides.
   196        use_file_names (bool): True if the output file names are normally predictable.
   197              This is the case for most languages but not e.g. Java where they depend on the
   198              declarations in the proto file. If False we'll attempt to detect them.
   199        protoc_flags (list): Additional flags for the protoc invocation for this rule.
   200        tools (list): Additional tools to apply to this rule.
   201        deps (list): Additional dependencies to apply to this rule.
   202        pre_build (function): Definition of pre-build function to apply to this language.
   203        proto_language (str): Name of the language (as protoc would name it). Defaults to the same as language.
   204      """
   205      return {
   206          'language': language,
   207          'proto_language': proto_language or language,
   208          'extensions': {language: extensions} if isinstance(extensions, list) else extensions,
   209          'func': func,
   210          'use_file_names': use_file_names,
   211          'protoc_flags': protoc_flags or [],
   212          'tools': tools or [],
   213          'deps': deps or [],
   214          'pre_build': pre_build,
   215      }
   216  
   217  
   218  def _parent_rule(name):
   219      """Returns the parent rule, i.e. strips the leading _ and trailing #hashtag."""
   220      before, _, _ = name.partition('#')
   221      return before.lstrip('_')
   222  
   223  
   224  def _annotate_outs(extensions):
   225      """Used to collect output files when we can't determine them without running the rule.
   226  
   227      For Java this is always the case because their location depends on the java_package option
   228      defined in the .proto file. For other languages we might not know if the sources come from
   229      another rule.
   230      """
   231      def _annotate_outs(rule_name, output):
   232          for out in output:
   233              for lang, exts in extensions:
   234                  for ext in exts:
   235                      if out.endswith(ext):
   236                          add_out(rule_name, lang, out.lstrip('./'))
   237      return _annotate_outs
   238  
   239  
   240  def _merge_dicts(a, b):
   241      """Merges dictionary a into dictionary b, overwriting where a's values are not None."""
   242      if not isinstance(a, dict):
   243          return {x: b[x] for x in a}  # Languages can be passed as just a list.
   244      return {k: v or b[k] for k, v in a.items()}
   245  
   246  
   247  def _collect_transitive_labels(rule):
   248      """Defines a pre-build function that updates a build command with transitive protoc flags."""
   249      labels = get_labels(rule, 'protoc:')
   250      if labels:
   251          cmd = get_command(rule)
   252          set_command(rule, cmd.replace('$TOOLS_PROTOC ', '$TOOLS_PROTOC %s ' % ' '.join(labels)))
   253  
   254  
   255  def proto_languages():
   256      """Returns the known set of proto language definitions.
   257  
   258      Due to technical reasons this can't just be a global (if you must know: the lambdas need
   259      to bind to the set of globals for the BUILD file, not the set when we load the rules).
   260      TODO(pebers): This seems a bit ugly and might be slow if we're creating a lot of temporaries.
   261                    Find a way to persist these...
   262      """
   263      return {
   264          'cc': proto_language(
   265              language = 'cc',
   266              proto_language = 'cpp',
   267              extensions = {'cc': ['.pb.cc'], 'cc_hdrs': ['.pb.h']},
   268              func = lambda name, srcs, deps, test_only: cc_library(
   269                  name = name,
   270                  srcs = srcs,
   271                  hdrs = [srcs[0] + '_hdrs'],
   272                  deps = deps,
   273                  test_only = test_only,
   274                  pkg_config_libs = ['protobuf'],
   275                  compiler_flags = ['-I$PKG'],
   276              ),
   277              protoc_flags = ['--cpp_out=$TMP_DIR'],
   278          ),
   279          'java': proto_language(
   280              language = 'java',
   281              extensions = ['.java'],
   282              use_file_names = False,
   283              func = lambda name, srcs, deps, test_only: java_library(
   284                  name = name,
   285                  srcs = srcs,
   286                  exported_deps = deps,
   287                  test_only = test_only,
   288              ),
   289              protoc_flags = ['--java_out=$TMP_DIR'],
   290              deps = [CONFIG.PROTO_JAVA_DEP],
   291          ),
   292          'go': proto_language(
   293              language = 'go',
   294              extensions = ['.pb.go'],
   295              func = lambda name, srcs, deps, test_only: go_library(
   296                  name = name,
   297                  srcs = srcs,
   298                  out = _parent_rule(name) + '.a',
   299                  deps = deps,
   300                  test_only = test_only,
   301              ),
   302              protoc_flags = ['--go_out=$TMP_DIR', '--plugin=protoc-gen-go=$TOOLS_GO'],
   303              tools = [CONFIG.PROTOC_GO_PLUGIN],
   304              deps = [CONFIG.PROTO_GO_DEP],
   305              pre_build = _go_path_mapping(False),
   306          ),
   307          'js': proto_language(
   308              language = 'js',
   309              extensions = ['_pb.js'],
   310              func = lambda name, srcs, deps, test_only: filegroup(
   311                  name = name,
   312                  srcs = srcs,
   313                  deps = deps,
   314                  test_only = test_only,
   315                  requires = ['js'],
   316                  output_is_complete = False,
   317              ),
   318              protoc_flags = ['--js_out=import_style=commonjs,binary:$TMP_DIR'],
   319              deps = [CONFIG.PROTO_JS_DEP],
   320          ),
   321          'py': proto_language(
   322              language = 'py',
   323              proto_language = 'python',
   324              extensions = ['_pb2.py'],
   325              func = python_library,
   326              protoc_flags = ['--python_out=$TMP_DIR'],
   327              deps = [CONFIG.PROTO_PYTHON_DEP],
   328          ),
   329      }
   330  
   331  
   332  def grpc_languages():
   333      """Returns the predefined set of gRPC languages."""
   334      return {
   335          'cc': proto_language(
   336              language = 'cc',
   337              proto_language = 'cpp',
   338              extensions = {'cc': ['.pb.cc', '.grpc.pb.cc'], 'cc_hdrs': ['.pb.h', '.grpc.pb.h']},
   339              func = lambda name, srcs, deps, test_only: cc_library(
   340                  name = name,
   341                  srcs = srcs,
   342                  hdrs = [srcs[0] + '_hdrs'],
   343                  deps = deps,
   344                  test_only = test_only,
   345                  pkg_config_libs = ['grpc++', 'grpc', 'protobuf'],
   346                  compiler_flags = ['-I$PKG', '-Wno-unused-parameter'],  # Generated gRPC code is not robust to this.
   347              ),
   348              protoc_flags = ['--cpp_out=$TMP_DIR', '--plugin=protoc-gen-grpc-cc=$TOOLS_CC', '--grpc-cc_out=$TMP_DIR'],
   349              tools = [CONFIG.GRPC_CC_PLUGIN],
   350          ),
   351          'py': proto_language(
   352              language = 'py',
   353              proto_language = 'python',
   354              extensions = ['_pb2.py', '_pb2_grpc.py'],
   355              func = python_library,
   356              protoc_flags = ['--python_out=$TMP_DIR', '--plugin=protoc-gen-grpc-python=$TOOLS_PY', '--grpc-python_out=$TMP_DIR'],
   357              tools = [CONFIG.GRPC_PYTHON_PLUGIN],
   358              deps = [CONFIG.PROTO_PYTHON_DEP, CONFIG.GRPC_PYTHON_DEP],
   359          ),
   360          'java': proto_language(
   361              language = 'java',
   362              extensions = ['.java'],
   363              use_file_names = False,
   364              func = lambda name, srcs, deps, test_only: java_library(
   365                  name = name,
   366                  srcs = srcs,
   367                  exported_deps = deps,
   368                  test_only = test_only,
   369              ),
   370              protoc_flags = ['--java_out=$TMP_DIR', '--plugin=protoc-gen-grpc-java=$TOOLS_JAVA', '--grpc-java_out=$TMP_DIR'],
   371              tools = [CONFIG.GRPC_JAVA_PLUGIN],
   372              deps = [CONFIG.GRPC_JAVA_DEP, CONFIG.PROTO_JAVA_DEP],
   373          ),
   374          'go': proto_language(
   375              language = 'go',
   376              extensions = ['.pb.go'],
   377              func = lambda name, srcs, deps, test_only: go_library(
   378                  name = name,
   379                  srcs = srcs,
   380                  out = _parent_rule(name) + '.a',
   381                  deps = deps,
   382                  test_only = test_only,
   383              ),
   384              protoc_flags = ['--go_out=$TMP_DIR', '--plugin=protoc-gen-go=$TOOLS_GO'],
   385              tools = [CONFIG.PROTOC_GO_PLUGIN],
   386              deps = [CONFIG.PROTO_GO_DEP, CONFIG.GRPC_GO_DEP],
   387              pre_build = _go_path_mapping(True),
   388          ),
   389          # We don't really support grpc-js right now, so this is the same as proto-js.
   390          'js': proto_language(
   391              language = 'js',
   392              extensions = ['_pb.js'],
   393              func = lambda name, srcs, deps, test_only: filegroup(
   394                  name = name,
   395                  srcs = srcs,
   396                  deps = deps,
   397                  test_only = test_only,
   398                  requires = ['js'],
   399                  output_is_complete = False,
   400              ),
   401              protoc_flags = ['--js_out=import_style=commonjs,binary:$TMP_DIR'],
   402              deps = [CONFIG.PROTO_JS_DEP],
   403          ),
   404      }
   405  
   406  
   407  def protoc_binary(name, version, hashes=None, deps=None, visibility=None):
   408      """Downloads a precompiled protoc binary.
   409  
   410      You will obviously need to choose a version that is available on Github - there aren't
   411      necessarily protoc downloads for every protobuf release.
   412  
   413      Args:
   414        name (str): Name of the rule
   415        version (str): Version of protoc to download (e.g. '3.4.0').
   416        hashes (list): Hashes to verify the download against.
   417        deps (list): Any other dependencies
   418        visibility (list): Visibility of the rule.
   419      """
   420      download_rule = remote_file(
   421          name = name,
   422          _tag = 'download',
   423          url = 'https://github.com/google/protobuf/releases/download/v%s/protoc-%s-${XOS}-${XARCH}.zip' % (version, version),
   424          out = 'protoc-%s.zip' % version,
   425          hashes = hashes,
   426          deps = deps,
   427      )
   428      return genrule(
   429          name = name,
   430          srcs = [download_rule],
   431          outs = ['protoc'],
   432          tools = [CONFIG.JARCAT_TOOL],
   433          binary = True,
   434          cmd = '$TOOL x $SRCS bin/protoc',
   435          visibility = visibility,
   436      )