pw_build: Support building Python wheels

Implement the .wheel subtarget for pw_python_package. This builds a
wheel for that Python package. Python wheels can be collected in a
directory using pw_mirror_tree's path_data_keys option for the "wheels"
key.

Fixed: 239
Change-Id: I43d756ce9714ba834b4590de702efeafc666b9ee
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/36762
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Joe Ethier <jethier@google.com>
diff --git a/pw_build/python.gni b/pw_build/python.gni
index d04d027..2b1ce22 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -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
@@ -38,13 +38,11 @@
 #     - $name.lint.pylint - Runs pylint (if enabled).
 #   - $name.tests - Runs all tests for this package.
 #   - $name.install - Installs the package in a venv.
-#   - $name.wheel - Builds a Python wheel for the package. (Not implemented.)
+#   - $name.wheel - Builds a Python wheel for the package.
 #
 # All Python packages are instantiated with the default toolchain, regardless of
 # the current toolchain.
 #
-# TODO(pwbug/239): Implement wheel building.
-#
 # Args:
 #   setup: List of setup file paths (setup.py or pyproject.toml & setup.cfg),
 #       which must all be in the same directory.
@@ -68,6 +66,7 @@
 #       provided, mypy's default configuration file search is used. mypy is
 #       executed from the package's setup directory, so mypy.ini files in that
 #       directory will take precedence over others.
+#
 template("pw_python_package") {
   # The Python targets are always instantiated in the default toolchain. Use
   # fully qualified labels so that the toolchain is not lost.
@@ -340,18 +339,27 @@
         }
       }
 
-      # TODO(pwbug/239): Add support for building groups of wheels. The code below
-      #     is incomplete and untested.
+      # 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") {
-        script = "$dir_pw_build/py/pw_build/python_wheels.py"
+        metadata = {
+          pw_python_package_wheels = [ "$target_out_dir/$target_name" ]
+        }
+
+        module = "build"
 
         args = [
-          "--out_dir",
-          rebase_path(target_out_dir),
-        ]
-        args += rebase_path(_all_py_files)
+                 rebase_path(_setup_dir),
+                 "--wheel",
+                 "--no-isolation",
+                 "--outdir",
+               ] + rebase_path(metadata.pw_python_package_wheels)
 
-        deps = [ ":${invoker.target_name}.install" ]
+        deps = [ ":${invoker.target_name}" ]
+        foreach(dep, _python_deps) {
+          deps += [ string_replace(dep, "(", ".wheel(") ]
+        }
+
         stamp = true
       }
     } else {
@@ -467,6 +475,9 @@
           inputs = [ invoker.mypy_ini ]
         }
       }
+
+      # Generated packages with linting disabled never need the whole file list.
+      not_needed([ "_all_py_files" ])
     }
   } else {
     # Create groups with the public target names ($target_name, $target_name.lint,