pw_build: Add support for pw_python_wheels targets

Also adds missing __init__.py files so pw modules can be
built as wheels by setuptools.

Change-Id: Ic8407bed2dc20c206c4447306a32c2db82d2bc79
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/15680
Commit-Queue: Joe Ethier <jethier@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/.gitignore b/.gitignore
index e66aa11..ffdb22b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,8 @@
 .cache/
 .mypy_cache/
 __pycache__/
+build/
+dist/
 
 # PyOxidizer
 pw_env_setup/py/oxidizer/build
diff --git a/pw_allocator/py/pw_allocator/__init__.py b/pw_allocator/py/pw_allocator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_allocator/py/pw_allocator/__init__.py
diff --git a/pw_build/py/pw_build/__init__.py b/pw_build/py/pw_build/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_build/py/pw_build/__init__.py
diff --git a/pw_build/py/pw_build/python_wheels.py b/pw_build/py/pw_build/python_wheels.py
new file mode 100644
index 0000000..22ad3d5
--- /dev/null
+++ b/pw_build/py/pw_build/python_wheels.py
@@ -0,0 +1,65 @@
+# Copyright 2020 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.
+"""Wrapper for the CLI commands for Python .whl building."""
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+
+_LOG = logging.getLogger(__name__)
+
+
+def _parse_args():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        'setup_files',
+        nargs='+',
+        help='Path to a setup.py file to invoke to build wheels.')
+    parser.add_argument('--out_dir',
+                        help='Path where the build artifacts should be put.')
+
+    return parser.parse_args()
+
+
+def build_wheels(setup_files, out_dir):
+    """Build Python wheels by calling 'python setup.py bdist_wheel'."""
+    dist_dir = os.path.abspath(out_dir)
+
+    for filename in setup_files:
+        if not (filename.endswith('setup.py') and os.path.isfile(filename)):
+            raise RuntimeError(f'Unable to find setup.py file at {filename}.')
+
+        working_dir = os.path.dirname(filename)
+
+        cmd = [
+            sys.executable,
+            'setup.py',
+            'bdist_wheel',
+            '--dist-dir',
+            dist_dir,
+        ]
+        _LOG.debug('Running command:\n  %s', ' '.join(cmd))
+        subprocess.check_call(cmd, cwd=working_dir)
+
+
+def main():
+    build_wheels(**vars(_parse_args()))
+
+
+if __name__ == '__main__':
+    logging.basicConfig()
+    main()
+    sys.exit(0)
diff --git a/pw_build/py/setup.py b/pw_build/py/setup.py
index 784b8e0..f9f104c 100644
--- a/pw_build/py/setup.py
+++ b/pw_build/py/setup.py
@@ -22,4 +22,7 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Python scripts that support the GN build',
     packages=setuptools.find_packages(),
+    install_requires=[
+        'wheel',
+    ],
 )
diff --git a/pw_build/python_wheels.gni b/pw_build/python_wheels.gni
new file mode 100644
index 0000000..206831e
--- /dev/null
+++ b/pw_build/python_wheels.gni
@@ -0,0 +1,35 @@
+# Copyright 2020 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.
+
+# gn-format disable
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python_script.gni")
+
+# Builds a .whl from a Python package.
+template("pw_python_wheels") {
+  pw_python_script(target_name) {
+    forward_variables_from(invoker, [ "deps" ])
+
+    script = "$dir_pw_build/py/pw_build/python_wheels.py"
+
+    args = [
+      "--out_dir",
+      rebase_path("$target_out_dir/python_wheels"),
+    ]
+    args += rebase_path(invoker.inputs)
+
+    stamp = true
+  }
+}
diff --git a/pw_doctor/py/pw_doctor/__init__.py b/pw_doctor/py/pw_doctor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_doctor/py/pw_doctor/__init__.py
diff --git a/pw_module/py/pw_module/__init__.py b/pw_module/py/pw_module/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_module/py/pw_module/__init__.py
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/__init__.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/__init__.py
diff --git a/pw_rpc/py/pw_rpc/__init__.py b/pw_rpc/py/pw_rpc/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/__init__.py
diff --git a/pw_unit_test/py/pw_unit_test/__init__.py b/pw_unit_test/py/pw_unit_test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_unit_test/py/pw_unit_test/__init__.py
diff --git a/pw_watch/py/pw_watch/__init__.py b/pw_watch/py/pw_watch/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_watch/py/pw_watch/__init__.py