pw_protobuf_compiler: Python support

This extends pw_protobuf_compiler with the ability to compile Python
code for .proto files, creating an installable pw_python_package from
the generated code.

Change-Id: Iee4212531420299deaf816ac9210e53424cca03d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/26460
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 6331537..8d0a55e 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -16,6 +16,7 @@
 
 import("$dir_pw_build/error.gni")
 import("$dir_pw_build/input_group.gni")
+import("$dir_pw_build/python.gni")
 import("$dir_pw_build/python_action.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_third_party/nanopb/nanopb.gni")
@@ -219,6 +220,69 @@
   }
 }
 
+# Generates Python code for proto files, creating a pw_python_package containing
+# the generated files. This is internal and should not be used outside of this
+# file. Use pw_proto_library instead.
+template("_pw_python_proto_library") {
+  _target = target_name
+  _package_dir = ""
+
+  foreach(_rebased_proto_path, rebase_path(invoker.sources, ".")) {
+    _path_components = []
+    _path_components = string_split(_rebased_proto_path, "/")
+
+    assert(_path_components != [ _rebased_proto_path ] &&
+               _path_components[0] != "..",
+           "Sources in a pw_proto_library must live in subdirectories " +
+               "of where it is defined")
+
+    if (_package_dir == "") {
+      _package_dir = _path_components[0]
+    } else {
+      assert(_path_components[0] == _package_dir,
+             "All .proto sources in a pw_proto_library must live " +
+                 "in the same directory tree")
+    }
+  }
+
+  _pw_invoke_protoc(target_name) {
+    forward_variables_from(invoker, "*", _forwarded_vars)
+    language = "python"
+    output_extensions = [ "_pb2.py" ]
+  }
+
+  _setup_py = "${invoker.gen_dir}/setup.py"
+
+  # Create the setup and init files for the Python package.
+  pw_python_action(target_name + "._package_gen") {
+    script = "$dir_pw_protobuf_compiler/py/pw_protobuf_compiler/generate_python_package.py"
+    args = [
+             "--setup",
+             rebase_path(_setup_py),
+             "--package",
+             _package_dir,
+           ] + rebase_path(get_path_info(invoker.sources, "dir"), ".")
+    public_deps = [ ":$_target._gen" ]
+    stamp = true
+  }
+
+  # Create a Python package with the generated source files.
+  pw_python_package(target_name) {
+    forward_variables_from(invoker, _forwarded_vars)
+    setup = [ _setup_py ]
+    sources = get_target_outputs(":$target_name._gen")
+    python_deps = invoker.deps
+    other_deps = [ ":$_target._package_gen" ]
+    _pw_generated = true
+  }
+}
+
+declare_args() {
+  # Temporary build arg to allow projects to update their proto libraries to be
+  # Python-compatible.
+  pw_protobuf_compiler_DISABLE_PYTHON = false
+}
+
 # Generates protobuf code from .proto definitions for various languages.
 # For each supported generator, creates a sub-target named:
 #
@@ -322,6 +386,15 @@
     base_target = _common.base_target
   }
 
+  if (!pw_protobuf_compiler_DISABLE_PYTHON) {
+    _pw_python_proto_library("$target_name.python") {
+      sources = invoker.sources
+      forward_variables_from(_common, "*")
+      deps = process_file_template(_deps, "{{source}}.python")
+      base_target = _common.base_target
+    }
+  }
+
   # All supported pw_protobuf generators.
   _protobuf_generators = [
     "pwpb",
@@ -329,6 +402,7 @@
     "nanopb_rpc",
     "raw_rpc",
     "go",
+    "python",
   ]
 
   # If the user attempts to use the target directly instead of one of the