pw_protobuf_compiler: Decouple proto packaging from directory

- Allow specifying prefix and strip_prefix arguments for proto files.
  The proto directory tree is built as specified in the out directory.
- Only invoke protoc from the default toolchain. This prevents duplicate
  protoc invocations.
- Prevent duplicate pw_proto_library Python package definitions anywhere
  in the build.
- Replace implicit handling of standalone external protos with a
  python_package_as_module option.

Change-Id: Id37d8b4d83294f7d3142a389e74ceea96dd4d620
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/34640
Reviewed-by: Alexei Frolov <frolv@google.com>
diff --git a/pw_protobuf_compiler/proto.cmake b/pw_protobuf_compiler/proto.cmake
index 73b06b6..cc07ce0 100644
--- a/pw_protobuf_compiler/proto.cmake
+++ b/pw_protobuf_compiler/proto.cmake
@@ -30,11 +30,16 @@
 #   NAME - the base name of the libraries to create
 #   SOURCES - .proto source files
 #   DEPS - dependencies on other pw_proto_library targets
+#   PREFIX - prefix add to the proto files
+#   STRIP_PREFIX - prefix to remove from the proto files
+#   INPUTS - files to include along with the .proto files (such as Nanopb
+#       .options files
 #
 function(pw_proto_library NAME)
-  cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "SOURCES;DEPS")
+  cmake_parse_arguments(PARSE_ARGV 1 arg "" "STRIP_PREFIX;PREFIX"
+      "SOURCES;INPUTS;DEPS")
 
-  set(out_dir "${CMAKE_CURRENT_BINARY_DIR}/protos")
+  set(out_dir "${CMAKE_CURRENT_BINARY_DIR}/${NAME}")
 
   # Use INTERFACE libraries to track the proto include paths that are passed to
   # protoc.
@@ -46,33 +51,77 @@
   target_link_libraries("${NAME}._includes" INTERFACE ${include_deps})
 
   # Generate a file with all include paths needed by protoc.
-  set(include_file "${out_dir}/${NAME}.include_paths.txt")
+  set(include_file "${out_dir}/include_paths.txt")
   file(GENERATE OUTPUT "${include_file}"
      CONTENT
        "$<TARGET_PROPERTY:${NAME}._includes,INTERFACE_INCLUDE_DIRECTORIES>")
 
+  if("${arg_STRIP_PREFIX}" STREQUAL "")
+    set(arg_STRIP_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}")
+  endif()
+
+  foreach(path IN LISTS arg_SOURCES arg_INPUTS)
+    get_filename_component(abspath "${path}" ABSOLUTE)
+    list(APPEND files_to_mirror "${abspath}")
+  endforeach()
+
+  # Mirror the sources to the output directory with the specified prefix.
+  _pw_rebase_paths(
+      sources "${out_dir}/sources/${arg_PREFIX}" "${arg_STRIP_PREFIX}" "${arg_SOURCES}" "")
+  _pw_rebase_paths(
+      inputs "${out_dir}/sources/${arg_PREFIX}" "${arg_STRIP_PREFIX}" "${arg_INPUTS}" "")
+
+  add_custom_command(
+    COMMAND
+      python
+      "$ENV{PW_ROOT}/pw_build/py/pw_build/mirror_tree.py"
+      --source-root "${arg_STRIP_PREFIX}"
+      --directory "${out_dir}/sources/${arg_PREFIX}"
+      ${files_to_mirror}
+    DEPENDS
+      "$ENV{PW_ROOT}/pw_build/py/pw_build/mirror_tree.py"
+      ${files_to_mirror}
+      ${arg_DEPS}
+    OUTPUT
+      ${sources} ${inputs}
+  )
+
   # Create a protobuf target for each supported protobuf library.
   _pw_pwpb_library(
-      "${NAME}" "${arg_SOURCES}" "${arg_DEPS}" "${include_file}" "${out_dir}")
+      "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
   _pw_raw_rpc_library(
-      "${NAME}" "${arg_SOURCES}" "${arg_DEPS}" "${include_file}" "${out_dir}")
+      "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
   _pw_nanopb_library(
-      "${NAME}" "${arg_SOURCES}" "${arg_DEPS}" "${include_file}" "${out_dir}")
+      "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
   _pw_nanopb_rpc_library(
-      "${NAME}" "${arg_SOURCES}" "${arg_DEPS}" "${include_file}" "${out_dir}")
+      "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
 endfunction(pw_proto_library)
 
+function(_pw_rebase_paths VAR OUT_DIR ROOT FILES EXTENSIONS)
+  foreach(file IN LISTS FILES)
+    get_filename_component(file "${file}" ABSOLUTE)
+    file(RELATIVE_PATH file "${ROOT}" "${file}")
+
+    if ("${EXTENSIONS}" STREQUAL "")
+      list(APPEND mirrored_files "${OUT_DIR}/${file}")
+    else()
+      foreach(ext IN LISTS EXTENSIONS)
+        get_filename_component(dir "${file}" DIRECTORY)
+        get_filename_component(name "${file}" NAME_WE)
+        list(APPEND mirrored_files "${OUT_DIR}/${dir}/${name}${ext}")
+      endforeach()
+    endif()
+  endforeach()
+
+  set("${VAR}" "${mirrored_files}" PARENT_SCOPE)
+endfunction(_pw_rebase_paths)
+
 # Internal function that invokes protoc through generate_protos.py.
 function(_pw_generate_protos
-      TARGET LANGUAGE PLUGIN OUTPUT_EXTS INCLUDE_FILE OUT_DIR SOURCES DEPS)
-  # Determine the names of the output files.
-  foreach(extension IN LISTS OUTPUT_EXTS)
-    foreach(source_file IN LISTS SOURCES)
-      get_filename_component(dir "${source_file}" DIRECTORY)
-      get_filename_component(name "${source_file}" NAME_WE)
-      list(APPEND outputs "${OUT_DIR}/${dir}/${name}${extension}")
-    endforeach()
-  endforeach()
+    TARGET LANGUAGE PLUGIN OUTPUT_EXTS INCLUDE_FILE OUT_DIR SOURCES INPUTS DEPS)
+  # Determine the names of the compiled output files.
+  _pw_rebase_paths(outputs
+      "${OUT_DIR}/${LANGUAGE}" "${OUT_DIR}/sources" "${SOURCES}" "${OUTPUT_EXTS}")
 
   # Export the output files to the caller's scope so it can use them if needed.
   set(generated_outputs "${outputs}" PARENT_SCOPE)
@@ -90,14 +139,14 @@
       "${script}"
       --language "${LANGUAGE}"
       --plugin-path "${PLUGIN}"
-      --include-path "${CMAKE_CURRENT_SOURCE_DIR}"
       --include-file "${INCLUDE_FILE}"
-      --out-dir "${OUT_DIR}"
-      ${ARGN}
-      ${SOURCES}
+      --compile-dir "${OUT_DIR}/sources"
+      --out-dir "${OUT_DIR}/${LANGUAGE}"
+      --sources ${SOURCES}
     DEPENDS
-      ${SOURCES}
       ${script}
+      ${SOURCES}
+      ${INPUTS}
       ${DEPS}
     OUTPUT
       ${outputs}
@@ -106,7 +155,7 @@
 endfunction(_pw_generate_protos)
 
 # Internal function that creates a pwpb proto library.
-function(_pw_pwpb_library NAME SOURCES DEPS INCLUDE_FILE OUT_DIR)
+function(_pw_pwpb_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
   list(TRANSFORM DEPS APPEND .pwpb)
 
   _pw_generate_protos("${NAME}.generate.pwpb"
@@ -116,18 +165,19 @@
       "${INCLUDE_FILE}"
       "${OUT_DIR}"
       "${SOURCES}"
+      "${INPUTS}"
       "${DEPS}"
   )
 
   # Create the library with the generated source files.
   add_library("${NAME}.pwpb" INTERFACE)
-  target_include_directories("${NAME}.pwpb" INTERFACE "${OUT_DIR}")
+  target_include_directories("${NAME}.pwpb" INTERFACE "${OUT_DIR}/pwpb")
   target_link_libraries("${NAME}.pwpb" INTERFACE pw_protobuf ${DEPS})
   add_dependencies("${NAME}.pwpb" "${NAME}.generate.pwpb")
 endfunction(_pw_pwpb_library)
 
 # Internal function that creates a raw_rpc proto library.
-function(_pw_raw_rpc_library NAME SOURCES DEPS INCLUDE_FILE OUT_DIR)
+function(_pw_raw_rpc_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
   list(TRANSFORM DEPS APPEND .raw_rpc)
 
   _pw_generate_protos("${NAME}.generate.raw_rpc"
@@ -137,12 +187,13 @@
       "${INCLUDE_FILE}"
       "${OUT_DIR}"
       "${SOURCES}"
+      "${INPUTS}"
       "${DEPS}"
   )
 
   # Create the library with the generated source files.
   add_library("${NAME}.raw_rpc" INTERFACE)
-  target_include_directories("${NAME}.raw_rpc" INTERFACE "${OUT_DIR}")
+  target_include_directories("${NAME}.raw_rpc" INTERFACE "${OUT_DIR}/raw_rpc")
   target_link_libraries("${NAME}.raw_rpc"
     INTERFACE
       pw_rpc.raw
@@ -153,7 +204,7 @@
 endfunction(_pw_raw_rpc_library)
 
 # Internal function that creates a nanopb proto library.
-function(_pw_nanopb_library NAME SOURCES DEPS INCLUDE_FILE OUT_DIR)
+function(_pw_nanopb_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
   list(TRANSFORM DEPS APPEND .nanopb)
 
   set(nanopb_dir "$<TARGET_PROPERTY:$<IF:$<TARGET_EXISTS:protobuf-nanopb-static>,protobuf-nanopb-static,pw_build.empty>,SOURCE_DIR>")
@@ -167,18 +218,19 @@
       "${INCLUDE_FILE}"
       "${OUT_DIR}"
       "${SOURCES}"
+      "${INPUTS}"
       "${DEPS}"
   )
 
   # Create the library with the generated source files.
   add_library("${NAME}.nanopb" EXCLUDE_FROM_ALL ${generated_outputs})
-  target_include_directories("${NAME}.nanopb" PUBLIC "${OUT_DIR}")
+  target_include_directories("${NAME}.nanopb" PUBLIC "${OUT_DIR}/nanopb")
   target_link_libraries("${NAME}.nanopb" PUBLIC pw_third_party.nanopb ${DEPS})
   add_dependencies("${NAME}.nanopb" "${NAME}.generate.nanopb")
 endfunction(_pw_nanopb_library)
 
 # Internal function that creates a nanopb_rpc library.
-function(_pw_nanopb_rpc_library NAME SOURCES DEPS INCLUDE_FILE OUT_DIR)
+function(_pw_nanopb_rpc_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
   # Determine the names of the output files.
   list(TRANSFORM DEPS APPEND .nanopb_rpc)
 
@@ -189,12 +241,16 @@
       "${INCLUDE_FILE}"
       "${OUT_DIR}"
       "${SOURCES}"
+      "${INPUTS}"
       "${DEPS}"
   )
 
   # Create the library with the generated source files.
   add_library("${NAME}.nanopb_rpc" INTERFACE)
-  target_include_directories("${NAME}.nanopb_rpc" INTERFACE "${OUT_DIR}")
+  target_include_directories("${NAME}.nanopb_rpc"
+    INTERFACE
+      "${OUT_DIR}/nanopb_rpc"
+  )
   target_link_libraries("${NAME}.nanopb_rpc"
     INTERFACE
       "${NAME}.nanopb"