pw_build: Dependency fixes

- Handle public_deps as well as deps in pw_mirror_tree.
- Have the Python .install target avoid unnecessary reinstalls, but
  have dependents re-run when any files change. This is done by
  splitting the installs into a separate target and having .install
  depend on it and the target that represents the source files.

Change-Id: Ie41de8cfdb84fff691e183a346465099d98c609f
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/39202
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
diff --git a/pw_build/mirror_tree.gni b/pw_build/mirror_tree.gni
index dc27b66..5e78de8 100644
--- a/pw_build/mirror_tree.gni
+++ b/pw_build/mirror_tree.gni
@@ -57,15 +57,20 @@
     _deps += invoker.deps
   }
 
-  if (defined(invoker.path_data_keys)) {
-    assert(_deps != [],
-           "'path_data_keys' requires at least one dependency in 'deps'")
+  _public_deps = []
+  if (defined(invoker.public_deps)) {
+    _public_deps += invoker.public_deps
+  }
 
+  if (defined(invoker.path_data_keys)) {
     generated_file("$target_name._path_list") {
       data_keys = invoker.path_data_keys
       rebase = root_build_dir
-      deps = _deps
       outputs = [ "$target_gen_dir/$target_name.txt" ]
+      deps = _deps + _public_deps
+
+      assert(deps != [],
+             "'path_data_keys' requires at least one dependency in 'deps'")
     }
 
     _deps += [ ":$target_name._path_list" ]
@@ -93,13 +98,15 @@
     }
 
     deps = _deps
+    public_deps = _public_deps
 
     _ignore_args = [
       "script",
       "args",
       "outputs",
-      "deps",
       "directory",
+      "deps",
+      "public_deps",
     ]
     forward_variables_from(invoker, "*", _ignore_args)
   }
diff --git a/pw_build/python.gni b/pw_build/python.gni
index bdde00f..550f592 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -26,6 +26,10 @@
   "lint.pylint",
   "install",
   "wheel",
+
+  # Internal targets that directly depend on one another.
+  "_run_pip_install",
+  "_build_wheel",
 ]
 
 # Internal template that runs Mypy.
@@ -398,8 +402,8 @@
 
     if (_is_package) {
       # Install this Python package and its dependencies in the current Python
-      # environment.
-      pw_python_action("$target_name.install") {
+      # environment using pip.
+      pw_python_action("$target_name._run_pip_install") {
         module = "pip"
         public_deps = []
 
@@ -428,13 +432,13 @@
           # formatted as "//path/to:target(toolchain)", so we can't just append
           # ".subtarget". Instead, we replace the opening parenthesis of the
           # toolchain with ".suffix(".
-          public_deps += [ string_replace(dep, "(", ".install(") ]
+          public_deps += [ string_replace(dep, "(", "._run_pip_install(") ]
         }
       }
 
       # Builds a Python wheel for this package. Records the output directory
       # in the pw_python_package_wheels metadata key.
-      pw_python_action("$target_name.wheel") {
+      pw_python_action("$target_name._build_wheel") {
         metadata = {
           pw_python_package_wheels = [ "$target_out_dir/$target_name" ]
         }
@@ -456,27 +460,49 @@
         stamp = true
       }
     } else {
-      # If this is not a package, install or build wheels for its deps only.
-      group("$target_name.install") {
-        deps = []
-        foreach(dep, _python_deps) {
-          deps += [ string_replace(dep, "(", ".install(") ]
-        }
+      # Stubs for non-package targets.
+      group("$target_name._run_pip_install") {
       }
-      group("$target_name.wheel") {
-        deps = []
-        foreach(dep, _python_deps) {
-          deps += [ string_replace(dep, "(", ".wheel(") ]
-        }
+      group("$target_name._build_wheel") {
+      }
+    }
+
+    # Create the .install and .wheel targets. To limit unnecessary pip
+    # executions, non-generated packages are only reinstalled when their
+    # setup.py changes. However, targets that depend on the .install subtarget
+    # re-run whenever any source files change.
+    #
+    # These targets just represent the source files if this isn't a package.
+    group("$target_name.install") {
+      public_deps = [ ":${invoker.target_name}" ]
+
+      if (_is_package) {
+        public_deps += [ ":${invoker.target_name}._run_pip_install" ]
+      }
+
+      foreach(dep, _python_deps) {
+        public_deps += [ string_replace(dep, "(", ".install(") ]
+      }
+    }
+
+    group("$target_name.wheel") {
+      public_deps = [ ":${invoker.target_name}.install" ]
+
+      if (_is_package) {
+        public_deps += [ ":${invoker.target_name}._build_wheel" ]
+      }
+
+      foreach(dep, _python_deps) {
+        public_deps += [ string_replace(dep, "(", ".wheel(") ]
       }
     }
 
     # Define the static analysis targets for this package.
     group("$target_name.lint") {
-      deps = [
-        ":${invoker.target_name}.lint.mypy",
-        ":${invoker.target_name}.lint.pylint",
-      ]
+      deps = []
+      foreach(_tool, _supported_static_analysis_tools) {
+        deps += [ ":${invoker.target_name}.lint.$_tool" ]
+      }
     }
 
     if (_static_analysis != [] || _test_sources != []) {
diff --git a/pw_build/python_action.gni b/pw_build/python_action.gni
index 87f4bc2..05b1e90 100644
--- a/pw_build/python_action.gni
+++ b/pw_build/python_action.gni
@@ -152,7 +152,8 @@
 
   if (defined(invoker.python_deps)) {
     foreach(dep, invoker.python_deps) {
-      _deps += [ get_label_info(dep, "label_no_toolchain") + ".install" ]
+      _deps += [ get_label_info(dep, "label_no_toolchain") + ".install(" +
+                 get_label_info(dep, "toolchain") + ")" ]
     }
   }