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