pw_protobuf_compiler: Ensure nanopb_pb2.py is generated

Import the Nanopb Python package to ensure that nanopb_pb2.py is
generated prior generating any Nanopb protos. This prevents race
conditions in clean builds.

Change-Id: I4b07ceb5665c49d0bd73a35759af18ec275094ed
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/43741
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_build/python.gni b/pw_build/python.gni
index 50d247f..a68da03 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -655,6 +655,7 @@
     _python_deps = invoker.python_deps
   } else {
     _python_deps = []
+    not_needed([ "invoker" ])  # Allow empty groups.
   }
 
   group(target_name) {
diff --git a/pw_env_setup/BUILD.gn b/pw_env_setup/BUILD.gn
index 2a4528f..04d2752 100644
--- a/pw_env_setup/BUILD.gn
+++ b/pw_env_setup/BUILD.gn
@@ -52,6 +52,7 @@
 
     # Standalone scripts
     "$dir_pw_hdlc/rpc_example:example_script",
+    "$dir_pw_third_party/nanopb:generate_nanopb_proto",
   ]
 }
 
diff --git a/pw_protobuf_compiler/proto.cmake b/pw_protobuf_compiler/proto.cmake
index d05b960..2410c5d 100644
--- a/pw_protobuf_compiler/proto.cmake
+++ b/pw_protobuf_compiler/proto.cmake
@@ -246,6 +246,11 @@
         "${INPUTS}"
         "${DEPS}"
     )
+
+    # Ensure that nanopb_pb2.py is generated to avoid race conditions.
+    add_dependencies("${NAME}._generate.nanopb"
+        pw_third_party.nanopb.generate_proto
+    )
   endif()
 
   # Create the library with the generated source files.
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index c6348c9..51d443f 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -69,6 +69,10 @@
         deps += [ get_label_info(dep, "label_no_toolchain") + "._gen" ]
       }
 
+      if (defined(invoker.other_deps)) {
+        deps += invoker.other_deps
+      }
+
       args = [
                "--language",
                invoker.language,
@@ -192,6 +196,7 @@
       forward_variables_from(invoker, "*", _forwarded_vars)
       language = "nanopb"
       plugin = "$dir_pw_third_party_nanopb/generator/protoc-gen-nanopb"
+      other_deps = [ "$dir_pw_third_party/nanopb:generate_nanopb_proto.action" ]
     }
 
     # Create a library with the generated source files.
diff --git a/third_party/nanopb/BUILD.gn b/third_party/nanopb/BUILD.gn
index 77e2453..35620ac 100644
--- a/third_party/nanopb/BUILD.gn
+++ b/third_party/nanopb/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2021 The Pigweed Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
 # use this file except in compliance with the License. You may obtain a copy of
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/python.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_protobuf_compiler/proto.gni")
 import("nanopb.gni")
@@ -47,7 +48,19 @@
     sources = [ "$dir_pw_third_party_nanopb/generator/proto/nanopb.proto" ]
     python_module_as_package = "nanopb_pb2"
   }
+
+  # Generates nanopb_pb2.py, which is needed to compile protobufs with Nanopb.
+  pw_python_script("generate_nanopb_proto") {
+    sources = [ "generate_nanopb_proto.py" ]
+    pylintrc = "$dir_pigweed/.pylintrc"
+    action = {
+      args = [ rebase_path(dir_pw_third_party_nanopb) ]
+      stamp = true
+    }
+  }
 } else {
   group("nanopb") {
   }
+  pw_python_group("generate_nanopb_proto") {
+  }
 }
diff --git a/third_party/nanopb/CMakeLists.txt b/third_party/nanopb/CMakeLists.txt
index 7b5745e..3c2927b 100644
--- a/third_party/nanopb/CMakeLists.txt
+++ b/third_party/nanopb/CMakeLists.txt
@@ -31,3 +31,15 @@
   INTERFACE
     "${dir_pw_third_party_nanopb}"
 )
+
+# Generates nanopb_pb2.py, which is needed to compile protobufs with Nanopb.
+add_custom_command(
+  COMMAND
+    python "${CMAKE_CURRENT_LIST_DIR}/generate_nanopb_proto.py" "${dir_pw_third_party_nanopb}"
+  OUTPUT
+    "${dir_pw_third_party_nanopb}/generator/proto/nanopb_pb2.py"
+)
+add_custom_target(pw_third_party.nanopb.generate_proto
+  DEPENDS
+    "${dir_pw_third_party_nanopb}/generator/proto/nanopb_pb2.py"
+)
diff --git a/third_party/nanopb/generate_nanopb_proto.py b/third_party/nanopb/generate_nanopb_proto.py
new file mode 100644
index 0000000..d7b84d1
--- /dev/null
+++ b/third_party/nanopb/generate_nanopb_proto.py
@@ -0,0 +1,49 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Generates nanopb_pb2.py by importing the Nanopb proto module.
+
+The Nanopb repository generates nanopb_pb2.py dynamically when its Python
+package is imported if it does not exist. If multiple processes try to use
+Nanopb to compile simultaneously on a clean build, they can interfere with each
+other. One process might rewrite nanopb_pb2.py as another process is trying to
+access it, resulting in import errors.
+
+This script imports the Nanopb module so that nanopb_pb2.py is generated if it
+doesn't exist. All Nanopb proto compilation targets depend on this script so
+that nanopb_pb2.py is guaranteed to exist before they need it.
+"""
+
+import argparse
+import importlib.util
+from pathlib import Path
+import sys
+
+
+def generate_nanopb_proto(root: Path) -> None:
+    sys.path.append(str(root / 'generator'))
+
+    spec = importlib.util.spec_from_file_location(
+        'proto', root / 'generator' / 'proto' / '__init__.py')
+    proto_module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(proto_module)  # type: ignore[union-attr]
+
+
+def _parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('root', type=Path, help='Nanopb root')
+    return parser.parse_args()
+
+
+if __name__ == '__main__':
+    generate_nanopb_proto(**vars(_parse_args()))