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