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 )