1# Copyright 2020 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14 15import("//build_overrides/pigweed.gni") 16 17import("$dir_pw_build/error.gni") 18import("$dir_pw_build/input_group.gni") 19import("$dir_pw_build/mirror_tree.gni") 20import("$dir_pw_build/python.gni") 21import("$dir_pw_build/python_action.gni") 22import("$dir_pw_build/target_types.gni") 23import("$dir_pw_third_party/nanopb/nanopb.gni") 24 25# Variables forwarded from the public pw_proto_library template to the final 26# pw_source_set. 27_forwarded_vars = [ 28 "testonly", 29 "visibility", 30] 31 32# Internal template that invokes protoc with a pw_python_action. This should not 33# be used outside of this file; use pw_proto_library instead. 34# 35# This creates the internal GN target $target_name.$language._gen that compiles 36# proto files with protoc. 37template("_pw_invoke_protoc") { 38 if (current_toolchain == default_toolchain) { 39 if (defined(invoker.out_dir)) { 40 _out_dir = invoker.out_dir 41 } else { 42 _out_dir = "${invoker.base_out_dir}/${invoker.language}" 43 if (defined(invoker.module_as_package) && 44 invoker.module_as_package != "") { 45 assert(invoker.language == "python") 46 _out_dir = "$_out_dir/${invoker.module_as_package}" 47 } 48 } 49 50 _includes = 51 rebase_path(get_target_outputs(":${invoker.base_target}._includes")) 52 53 pw_python_action("$target_name._gen") { 54 script = 55 "$dir_pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py" 56 57 python_deps = [ "$dir_pw_protobuf_compiler/py" ] 58 if (defined(invoker.python_deps)) { 59 python_deps += invoker.python_deps 60 } 61 62 deps = [ 63 ":${invoker.base_target}._includes", 64 ":${invoker.base_target}._sources", 65 ] 66 67 foreach(dep, invoker.deps) { 68 deps += [ get_label_info(dep, "label_no_toolchain") + "._gen" ] 69 } 70 71 args = [ 72 "--language", 73 invoker.language, 74 "--include-file", 75 _includes[0], 76 "--compile-dir", 77 rebase_path(invoker.compile_dir), 78 "--out-dir", 79 rebase_path(_out_dir), 80 "--sources", 81 ] + rebase_path(invoker.sources) 82 83 if (defined(invoker.plugin)) { 84 inputs = [ invoker.plugin ] 85 args += [ "--plugin-path=" + rebase_path(invoker.plugin) ] 86 } 87 88 if (defined(invoker.outputs)) { 89 outputs = invoker.outputs 90 } else { 91 stamp = true 92 } 93 94 if (defined(invoker.metadata)) { 95 metadata = invoker.metadata 96 } else { 97 metadata = { 98 protoc_outputs = rebase_path(outputs) 99 root = [ rebase_path(_out_dir) ] 100 } 101 } 102 } 103 } else { 104 # protoc is only ever invoked from the default toolchain. 105 not_needed([ "target_name" ]) 106 not_needed(invoker, "*") 107 } 108} 109 110# Generates pw_protobuf C++ code for proto files, creating a source_set of the 111# generated files. This is internal and should not be used outside of this file. 112# Use pw_proto_library instead. 113template("_pw_pwpb_proto_library") { 114 _pw_invoke_protoc(target_name) { 115 forward_variables_from(invoker, "*", _forwarded_vars) 116 language = "pwpb" 117 plugin = "$dir_pw_protobuf/py/pw_protobuf/plugin.py" 118 python_deps = [ "$dir_pw_protobuf/py" ] 119 } 120 121 # Create a library with the generated source files. 122 config("$target_name._include_path") { 123 include_dirs = [ "${invoker.base_out_dir}/pwpb" ] 124 visibility = [ ":*" ] 125 } 126 127 pw_source_set(target_name) { 128 forward_variables_from(invoker, _forwarded_vars) 129 public_configs = [ ":$target_name._include_path" ] 130 deps = [ ":$target_name._gen($default_toolchain)" ] 131 public_deps = [ dir_pw_protobuf ] + invoker.deps 132 sources = invoker.outputs 133 public = filter_include(sources, [ "*.pwpb.h" ]) 134 } 135} 136 137# Generates nanopb RPC code for proto files, creating a source_set of the 138# generated files. This is internal and should not be used outside of this file. 139# Use pw_proto_library instead. 140template("_pw_nanopb_rpc_proto_library") { 141 # Create a target which runs protoc configured with the nanopb_rpc plugin to 142 # generate the C++ proto RPC headers. 143 _pw_invoke_protoc(target_name) { 144 forward_variables_from(invoker, "*", _forwarded_vars) 145 language = "nanopb_rpc" 146 plugin = "$dir_pw_rpc/py/pw_rpc/plugin_nanopb.py" 147 python_deps = [ "$dir_pw_rpc/py" ] 148 } 149 150 # Create a library with the generated source files. 151 config("$target_name._include_path") { 152 include_dirs = [ "${invoker.base_out_dir}/nanopb_rpc" ] 153 visibility = [ ":*" ] 154 } 155 156 pw_source_set(target_name) { 157 forward_variables_from(invoker, _forwarded_vars) 158 public_configs = [ ":$target_name._include_path" ] 159 deps = [ ":$target_name._gen($default_toolchain)" ] 160 public_deps = [ 161 ":${invoker.base_target}.nanopb", 162 "$dir_pw_rpc:server", 163 "$dir_pw_rpc/nanopb:method_union", 164 "$dir_pw_third_party/nanopb", 165 ] + invoker.deps 166 public = invoker.outputs 167 } 168} 169 170# Generates nanopb code for proto files, creating a source_set of the generated 171# files. This is internal and should not be used outside of this file. Use 172# pw_proto_library instead. 173template("_pw_nanopb_proto_library") { 174 # When compiling with the Nanopb plugin, the nanopb.proto file is already 175 # compiled internally, so skip recompiling it with protoc. 176 if (rebase_path(invoker.sources, invoker.compile_dir) == [ "nanopb.proto" ]) { 177 group("$target_name._gen") { 178 deps = [ ":${invoker.base_target}._sources" ] 179 } 180 181 group("$target_name") { 182 deps = invoker.deps + [ ":$target_name._gen($default_toolchain)" ] 183 } 184 } else { 185 # Create a target which runs protoc configured with the nanopb plugin to 186 # generate the C proto sources. 187 _pw_invoke_protoc(target_name) { 188 forward_variables_from(invoker, "*", _forwarded_vars) 189 language = "nanopb" 190 plugin = "$dir_pw_third_party_nanopb/generator/protoc-gen-nanopb" 191 } 192 193 # Create a library with the generated source files. 194 config("$target_name._include_path") { 195 include_dirs = [ "${invoker.base_out_dir}/nanopb" ] 196 visibility = [ ":*" ] 197 } 198 199 pw_source_set(target_name) { 200 forward_variables_from(invoker, _forwarded_vars) 201 public_configs = [ ":$target_name._include_path" ] 202 deps = [ ":$target_name._gen($default_toolchain)" ] 203 public_deps = [ "$dir_pw_third_party/nanopb" ] + invoker.deps 204 sources = invoker.outputs 205 public = filter_include(sources, [ "*.pb.h" ]) 206 } 207 } 208} 209 210# Generates raw RPC code for proto files, creating a source_set of the generated 211# files. This is internal and should not be used outside of this file. Use 212# pw_proto_library instead. 213template("_pw_raw_rpc_proto_library") { 214 # Create a target which runs protoc configured with the nanopb_rpc plugin to 215 # generate the C++ proto RPC headers. 216 _pw_invoke_protoc(target_name) { 217 forward_variables_from(invoker, "*", _forwarded_vars) 218 language = "raw_rpc" 219 plugin = "$dir_pw_rpc/py/pw_rpc/plugin_raw.py" 220 python_deps = [ "$dir_pw_rpc/py" ] 221 } 222 223 # Create a library with the generated source files. 224 config("$target_name._include_path") { 225 include_dirs = [ "${invoker.base_out_dir}/raw_rpc" ] 226 visibility = [ ":*" ] 227 } 228 229 pw_source_set(target_name) { 230 forward_variables_from(invoker, _forwarded_vars) 231 public_configs = [ ":$target_name._include_path" ] 232 deps = [ ":$target_name._gen($default_toolchain)" ] 233 public_deps = [ 234 "$dir_pw_rpc:server", 235 "$dir_pw_rpc/raw:method_union", 236 ] + invoker.deps 237 public = invoker.outputs 238 } 239} 240 241# Generates Go code for proto files, listing the proto output directory in the 242# metadata variable GOPATH. Internal use only. 243template("_pw_go_proto_library") { 244 _proto_gopath = "$root_gen_dir/go" 245 246 _pw_invoke_protoc(target_name) { 247 forward_variables_from(invoker, "*") 248 language = "go" 249 metadata = { 250 gopath = [ "GOPATH+=" + rebase_path(_proto_gopath) ] 251 external_deps = [ 252 "github.com/golang/protobuf/proto", 253 "google.golang.org/grpc", 254 ] 255 } 256 257 # Override the default "$base_out_dir/$language" output path. 258 out_dir = "$_proto_gopath/src" 259 } 260 261 group(target_name) { 262 deps = invoker.deps + [ ":$target_name._gen($default_toolchain)" ] 263 } 264} 265 266# Generates Python code for proto files, creating a pw_python_package containing 267# the generated files. This is internal and should not be used outside of this 268# file. Use pw_proto_library instead. 269template("_pw_python_proto_library") { 270 _pw_invoke_protoc(target_name) { 271 forward_variables_from(invoker, "*", _forwarded_vars + [ "python_package" ]) 272 language = "python" 273 python_deps = [ "$dir_pw_protobuf_compiler:protobuf_requirements" ] 274 } 275 276 if (defined(invoker.python_package) && invoker.python_package != "") { 277 # If nested in a Python package, write the package's name to a file so 278 # pw_python_package can check that the dependencies are correct. 279 write_file("${invoker.base_out_dir}/python_package.txt", 280 get_label_info(invoker.python_package, "label_no_toolchain")) 281 282 # If anyone attempts to depend on this Python package, print an error. 283 pw_error(target_name) { 284 _pkg = get_label_info(invoker.python_package, "label_no_toolchain") 285 message_lines = [ 286 "This proto Python package is embedded in the $_pkg Python package.", 287 "It cannot be used directly; instead, depend on $_pkg.", 288 ] 289 } 290 foreach(subtarget, pw_python_package_subtargets) { 291 group("$target_name.$subtarget") { 292 deps = [ ":${invoker.target_name}" ] 293 } 294 } 295 } else { 296 write_file("${invoker.base_out_dir}/python_package.txt", "") 297 298 # Create a Python package with the generated source files. 299 pw_python_package(target_name) { 300 forward_variables_from(invoker, _forwarded_vars) 301 generate_setup = { 302 name = invoker._package_dir 303 version = "0.0.1" # TODO(hepler): Need to be able to set this verison. 304 } 305 sources = invoker.outputs 306 strip_prefix = "${invoker.base_out_dir}/python" 307 python_deps = invoker.deps 308 other_deps = [ ":$target_name._gen($default_toolchain)" ] 309 static_analysis = [] 310 311 _pw_module_as_package = invoker.module_as_package != "" 312 } 313 } 314} 315 316# Generates protobuf code from .proto definitions for various languages. 317# For each supported generator, creates a sub-target named: 318# 319# <target_name>.<generator> 320# 321# GN permits using abbreviated labels when the target name matches the directory 322# name (e.g. //foo for //foo:foo). For consistency with this, the sub-targets 323# for each generator are aliased to the directory when the target name is the 324# same. For example, these two labels are equivalent: 325# 326# //path/to/my_protos:my_protos.pwpb 327# //path/to/my_protos:pwpb 328# 329# pw_protobuf_library targets generate Python packages. As such, they must have 330# globally unique package names. The first directory of the prefix or the first 331# common directory of the sources is used as the Python package. 332# 333# Args: 334# sources: List of input .proto files. 335# deps: List of other pw_proto_library dependencies. 336# inputs: Other files on which the protos depend (e.g. nanopb .options files). 337# prefix: A prefix to add to the source protos prior to compilation. For 338# example, a source called "foo.proto" with prefix = "nested" will be 339# compiled with protoc as "nested/foo.proto". 340# strip_prefix: Remove this prefix from the source protos. All source and 341# input files must be nested under this path. 342# python_package: Label of Python package to which to add the proto modules. 343# 344template("pw_proto_library") { 345 assert(defined(invoker.sources) && invoker.sources != [], 346 "pw_proto_library requires .proto source files") 347 348 if (defined(invoker.python_module_as_package)) { 349 _module_as_package = invoker.python_module_as_package 350 351 _must_be_one_source = invoker.sources 352 assert([ _must_be_one_source[0] ] == _must_be_one_source, 353 "'python_module_as_package' requires exactly one source file") 354 assert(_module_as_package != "", 355 "'python_module_as_package' cannot be be empty") 356 assert(string_split(_module_as_package, "/") == [ _module_as_package ], 357 "'python_module_as_package' cannot contain slashes") 358 assert(!defined(invoker.prefix), 359 "'prefix' cannot be provided with 'python_module_as_package'") 360 } else { 361 _module_as_package = "" 362 } 363 364 if (defined(invoker.strip_prefix)) { 365 _source_root = get_path_info(invoker.strip_prefix, "abspath") 366 } else { 367 _source_root = get_path_info(".", "abspath") 368 } 369 370 if (defined(invoker.prefix)) { 371 _prefix = invoker.prefix 372 } else { 373 _prefix = "" 374 } 375 376 _common = { 377 base_target = target_name 378 379 # This is the output directory for all files related to this proto library. 380 # Sources are mirrored to "$base_out_dir/sources" and protoc puts outputs in 381 # "$base_out_dir/$language" by default. 382 base_out_dir = 383 get_label_info(":$target_name($default_toolchain)", "target_gen_dir") + 384 "/$target_name.proto_library" 385 386 compile_dir = "$base_out_dir/sources" 387 388 # Refer to the source files as the are mirrored to the output directory. 389 sources = [] 390 foreach(file, rebase_path(invoker.sources, _source_root)) { 391 sources += [ "$compile_dir/$_prefix/$file" ] 392 } 393 } 394 395 _package_dir = "" 396 _source_names = [] 397 398 # Determine the Python package name to use for these protos. If there is no 399 # prefix, the first directory the sources are nested under is used. 400 foreach(source, rebase_path(invoker.sources, _source_root)) { 401 _path_components = [] 402 _path_components = string_split(source, "/") 403 404 if (_package_dir == "") { 405 _package_dir = _path_components[0] 406 } else { 407 assert(_prefix != "" || _path_components[0] == _package_dir, 408 "Unless 'prefix' is supplied, all .proto sources in a " + 409 "pw_proto_library must be in the same directory tree") 410 } 411 412 _source_names += 413 [ get_path_info(source, "dir") + "/" + get_path_info(source, "name") ] 414 } 415 416 # If the 'prefix' was supplied, use that for the package directory. 417 if (_prefix != "") { 418 _prefix_path_components = string_split(_prefix, "/") 419 _package_dir = _prefix_path_components[0] 420 } 421 422 assert(_package_dir != "" && _package_dir != "." && _package_dir != "..", 423 "Either a 'prefix' must be specified or all sources must be nested " + 424 "under a common directory") 425 426 # Define an action that is never executed to prevent duplicate proto packages 427 # from being declared. The target name and the output file include only the 428 # package directory, so different targets that use the same proto package name 429 # will conflict. 430 action("pw_proto_library.$_package_dir") { 431 script = "$dir_pw_build/py/pw_build/nop.py" 432 visibility = [] 433 434 # Place an error message in the output path (which is never created). If the 435 # package name conflicts occur in different BUILD.gn files, this results in 436 # an otherwise cryptic Ninja error, rather than a GN error. 437 outputs = [ "$root_out_dir/ " + 438 "ERROR - Multiple pw_proto_library targets create the " + 439 "'$_package_dir' package. Change the package name by setting " + 440 "the \"prefix\" arg or move the protos to a different " + 441 "directory, then re-run gn gen." ] 442 } 443 444 if (defined(invoker.deps)) { 445 _deps = invoker.deps 446 } else { 447 _deps = [] 448 } 449 450 # For each proto target, create a file which collects the base directories of 451 # all of its dependencies to list as include paths to protoc. 452 generated_file("$target_name._includes") { 453 # Collect metadata from the include path files of each dependency. 454 455 deps = [] 456 foreach(dep, _deps) { 457 _base = get_label_info(dep, "label_no_toolchain") 458 deps += [ "$_base._includes(" + get_label_info(dep, "toolchain") + ")" ] 459 } 460 461 data_keys = [ "protoc_includes" ] 462 outputs = [ "${_common.base_out_dir}/includes.txt" ] 463 464 # Indicate this library's base directory for its dependents. 465 metadata = { 466 protoc_includes = [ rebase_path(_common.compile_dir) ] 467 } 468 } 469 470 # Mirror the proto sources to the output directory with the prefix added. 471 pw_mirror_tree("$target_name._sources") { 472 source_root = _source_root 473 sources = invoker.sources 474 475 if (defined(invoker.inputs)) { 476 sources += invoker.inputs 477 } 478 479 directory = "${_common.compile_dir}/$_prefix" 480 } 481 482 # Enumerate all of the protobuf generator targets. 483 484 _pw_pwpb_proto_library("$target_name.pwpb") { 485 forward_variables_from(invoker, _forwarded_vars) 486 forward_variables_from(_common, "*") 487 488 deps = [] 489 foreach(dep, _deps) { 490 _base = get_label_info(dep, "label_no_toolchain") 491 deps += [ "$_base.pwpb(" + get_label_info(dep, "toolchain") + ")" ] 492 } 493 494 outputs = [] 495 foreach(name, _source_names) { 496 outputs += [ "$base_out_dir/pwpb/$_prefix/${name}.pwpb.h" ] 497 } 498 } 499 500 if (dir_pw_third_party_nanopb != "") { 501 _pw_nanopb_rpc_proto_library("$target_name.nanopb_rpc") { 502 forward_variables_from(invoker, _forwarded_vars) 503 forward_variables_from(_common, "*") 504 505 deps = [] 506 foreach(dep, _deps) { 507 _lbl = get_label_info(dep, "label_no_toolchain") 508 deps += [ "$_lbl.nanopb_rpc(" + get_label_info(dep, "toolchain") + ")" ] 509 } 510 511 outputs = [] 512 foreach(name, _source_names) { 513 outputs += [ "$base_out_dir/nanopb_rpc/$_prefix/${name}.rpc.pb.h" ] 514 } 515 } 516 517 _pw_nanopb_proto_library("$target_name.nanopb") { 518 forward_variables_from(invoker, _forwarded_vars) 519 forward_variables_from(_common, "*") 520 521 deps = [] 522 foreach(dep, _deps) { 523 _base = get_label_info(dep, "label_no_toolchain") 524 deps += [ "$_base.nanopb(" + get_label_info(dep, "toolchain") + ")" ] 525 } 526 527 outputs = [] 528 foreach(name, _source_names) { 529 outputs += [ 530 "$base_out_dir/nanopb/$_prefix/${name}.pb.h", 531 "$base_out_dir/nanopb/$_prefix/${name}.pb.c", 532 ] 533 } 534 } 535 } else { 536 pw_error("$target_name.nanopb_rpc") { 537 message = 538 "\$dir_pw_third_party_nanopb must be set to generate nanopb RPC code." 539 } 540 541 pw_error("$target_name.nanopb") { 542 message = 543 "\$dir_pw_third_party_nanopb must be set to compile nanopb protobufs." 544 } 545 } 546 547 _pw_raw_rpc_proto_library("$target_name.raw_rpc") { 548 forward_variables_from(invoker, _forwarded_vars) 549 forward_variables_from(_common, "*") 550 551 deps = [] 552 foreach(dep, _deps) { 553 _base = get_label_info(dep, "label_no_toolchain") 554 deps += [ "$_base.raw_rpc(" + get_label_info(dep, "toolchain") + ")" ] 555 } 556 557 outputs = [] 558 foreach(name, _source_names) { 559 outputs += [ "$base_out_dir/raw_rpc/$_prefix/${name}.raw_rpc.pb.h" ] 560 } 561 } 562 563 _pw_go_proto_library("$target_name.go") { 564 sources = _common.sources 565 566 deps = [] 567 foreach(dep, _deps) { 568 _base = get_label_info(dep, "label_no_toolchain") 569 deps += [ "$_base.go(" + get_label_info(dep, "toolchain") + ")" ] 570 } 571 572 forward_variables_from(_common, "*") 573 } 574 575 _pw_python_proto_library("$target_name.python") { 576 forward_variables_from(_common, "*") 577 forward_variables_from(invoker, [ "python_package" ]) 578 module_as_package = _module_as_package 579 580 deps = [] 581 foreach(dep, _deps) { 582 _base = get_label_info(dep, "label_no_toolchain") 583 deps += [ "$_base.python(" + get_label_info(dep, "toolchain") + ")" ] 584 } 585 586 if (module_as_package == "") { 587 _python_prefix = "$base_out_dir/python/$_prefix" 588 } else { 589 _python_prefix = "$base_out_dir/python/$module_as_package" 590 } 591 592 outputs = [] 593 foreach(name, _source_names) { 594 outputs += [ 595 "$_python_prefix/${name}_pb2.py", 596 "$_python_prefix/${name}_pb2.pyi", 597 ] 598 } 599 } 600 601 # All supported pw_protobuf generators. 602 _protobuf_generators = [ 603 "pwpb", 604 "nanopb", 605 "nanopb_rpc", 606 "raw_rpc", 607 "go", 608 "python", 609 ] 610 611 # If the label matches the directory name, alias the subtargets to the 612 # directory (e.g. //foo:nanopb is an alias for //foo:foo.nanopb). 613 if (get_label_info(":$target_name", "name") == 614 get_path_info(get_label_info(":$target_name", "dir"), "name")) { 615 foreach(_generator, _protobuf_generators - [ "python" ]) { 616 group(_generator) { 617 public_deps = [ ":${invoker.target_name}.$_generator" ] 618 } 619 } 620 621 pw_python_group("python") { 622 python_deps = [ ":${invoker.target_name}.python" ] 623 } 624 } 625 626 # If the user attempts to use the target directly instead of one of the 627 # generator targets, run a script which prints a nice error message. 628 pw_python_action(target_name) { 629 script = string_join("/", 630 [ 631 dir_pw_protobuf_compiler, 632 "py", 633 "pw_protobuf_compiler", 634 "proto_target_invalid.py", 635 ]) 636 args = [ 637 "--target", 638 target_name, 639 "--dir", 640 get_path_info(".", "abspath"), 641 "--root", 642 "//", 643 ] + _protobuf_generators 644 stamp = true 645 } 646} 647