Graduate wheel-building code out of //experimental (#418)

* Move wheelmaker from //experimental/tools into //tools.

* Move wheel-building rules from //experimental/python to //python.
Rename from wheel.bzl to packaging.bzl to avoid confusion with existing whl.bzl
Keep a stub wheel.bzl file in the old location for backwards compatibility.

* Move wheel building examples out of experimental.
diff --git a/python/BUILD b/python/BUILD
index 124ddd0..b3f9e1c 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -127,6 +127,7 @@
 # ========= Packaging rules =========
 
 exports_files([
+    "packaging.bzl",
     "pip.bzl",
     "whl.bzl",
 ])
diff --git a/python/packaging.bzl b/python/packaging.bzl
new file mode 100644
index 0000000..3b81137
--- /dev/null
+++ b/python/packaging.bzl
@@ -0,0 +1,343 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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
+#
+#    http://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.
+
+"""Rules for building wheels."""
+
+def _path_inside_wheel(input_file):
+    # input_file.short_path is sometimes relative ("../${repository_root}/foobar")
+    # which is not a valid path within a zip file. Fix that.
+    short_path = input_file.short_path
+    if short_path.startswith("..") and len(short_path) >= 3:
+        # Path separator. '/' on linux.
+        separator = short_path[2]
+
+        # Consume '../' part.
+        short_path = short_path[3:]
+
+        # Find position of next '/' and consume everything up to that character.
+        pos = short_path.find(separator)
+        short_path = short_path[pos + 1:]
+    return short_path
+
+def _input_file_to_arg(input_file):
+    """Converts a File object to string for --input_file argument to wheelmaker"""
+    return "%s;%s" % (_path_inside_wheel(input_file), input_file.path)
+
+def _py_package_impl(ctx):
+    inputs = depset(
+        transitive = [dep[DefaultInfo].data_runfiles.files for dep in ctx.attr.deps] +
+                     [dep[DefaultInfo].default_runfiles.files for dep in ctx.attr.deps],
+    )
+
+    # TODO: '/' is wrong on windows, but the path separator is not available in starlark.
+    # Fix this once ctx.configuration has directory separator information.
+    packages = [p.replace(".", "/") for p in ctx.attr.packages]
+    if not packages:
+        filtered_inputs = inputs
+    else:
+        filtered_files = []
+
+        # TODO: flattening depset to list gives poor performance,
+        for input_file in inputs.to_list():
+            wheel_path = _path_inside_wheel(input_file)
+            for package in packages:
+                if wheel_path.startswith(package):
+                    filtered_files.append(input_file)
+        filtered_inputs = depset(direct = filtered_files)
+
+    return [DefaultInfo(
+        files = filtered_inputs,
+    )]
+
+py_package = rule(
+    implementation = _py_package_impl,
+    doc = """
+A rule to select all files in transitive dependencies of deps which
+belong to given set of Python packages.
+
+This rule is intended to be used as data dependency to py_wheel rule
+""",
+    attrs = {
+        "deps": attr.label_list(),
+        "packages": attr.string_list(
+            mandatory = False,
+            allow_empty = True,
+            doc = """\
+List of Python packages to include in the distribution.
+Sub-packages are automatically included.
+""",
+        ),
+    },
+)
+
+def _py_wheel_impl(ctx):
+    outfile = ctx.actions.declare_file("-".join([
+        ctx.attr.distribution,
+        ctx.attr.version,
+        ctx.attr.python_tag,
+        ctx.attr.abi,
+        ctx.attr.platform,
+    ]) + ".whl")
+
+    inputs_to_package = depset(
+        direct = ctx.files.deps,
+    )
+
+    # Inputs to this rule which are not to be packaged.
+    # Currently this is only the description file (if used).
+    other_inputs = []
+
+    # Wrap the inputs into a file to reduce command line length.
+    packageinputfile = ctx.actions.declare_file(ctx.attr.name + "_target_wrapped_inputs.txt")
+    content = ""
+    for input_file in inputs_to_package.to_list():
+        content += _input_file_to_arg(input_file) + "\n"
+    ctx.actions.write(output = packageinputfile, content = content)
+    other_inputs.append(packageinputfile)
+
+    args = ctx.actions.args()
+    args.add("--name", ctx.attr.distribution)
+    args.add("--version", ctx.attr.version)
+    args.add("--python_tag", ctx.attr.python_tag)
+    args.add("--python_requires", ctx.attr.python_requires)
+    args.add("--abi", ctx.attr.abi)
+    args.add("--platform", ctx.attr.platform)
+    args.add("--out", outfile.path)
+    args.add_all(ctx.attr.strip_path_prefixes, format_each = "--strip_path_prefix=%s")
+
+    args.add("--input_file_list", packageinputfile)
+
+    extra_headers = []
+    if ctx.attr.author:
+        extra_headers.append("Author: %s" % ctx.attr.author)
+    if ctx.attr.author_email:
+        extra_headers.append("Author-email: %s" % ctx.attr.author_email)
+    if ctx.attr.homepage:
+        extra_headers.append("Home-page: %s" % ctx.attr.homepage)
+    if ctx.attr.license:
+        extra_headers.append("License: %s" % ctx.attr.license)
+
+    for h in extra_headers:
+        args.add("--header", h)
+
+    for c in ctx.attr.classifiers:
+        args.add("--classifier", c)
+
+    for r in ctx.attr.requires:
+        args.add("--requires", r)
+
+    for option, requirements in ctx.attr.extra_requires.items():
+        for r in requirements:
+            args.add("--extra_requires", r + ";" + option)
+
+    # Merge console_scripts into entry_points.
+    entrypoints = dict(ctx.attr.entry_points)  # Copy so we can mutate it
+    if ctx.attr.console_scripts:
+        # Copy a console_scripts group that may already exist, so we can mutate it.
+        console_scripts = list(entrypoints.get("console_scripts", []))
+        entrypoints["console_scripts"] = console_scripts
+        for name, ref in ctx.attr.console_scripts.items():
+            console_scripts.append("{name} = {ref}".format(name = name, ref = ref))
+
+    # If any entry_points are provided, construct the file here and add it to the files to be packaged.
+    # see: https://packaging.python.org/specifications/entry-points/
+    if entrypoints:
+        lines = []
+        for group, entries in sorted(entrypoints.items()):
+            if lines:
+                # Blank line between groups
+                lines.append("")
+            lines.append("[{group}]".format(group = group))
+            lines += sorted(entries)
+        entry_points_file = ctx.actions.declare_file(ctx.attr.name + "_entry_points.txt")
+        content = "\n".join(lines)
+        ctx.actions.write(output = entry_points_file, content = content)
+        other_inputs.append(entry_points_file)
+        args.add("--entry_points_file", entry_points_file)
+
+    if ctx.attr.description_file:
+        description_file = ctx.file.description_file
+        args.add("--description_file", description_file)
+        other_inputs.append(description_file)
+
+    ctx.actions.run(
+        inputs = depset(direct = other_inputs, transitive = [inputs_to_package]),
+        outputs = [outfile],
+        arguments = [args],
+        executable = ctx.executable._wheelmaker,
+        progress_message = "Building wheel",
+    )
+    return [DefaultInfo(
+        files = depset([outfile]),
+        data_runfiles = ctx.runfiles(files = [outfile]),
+    )]
+
+def _concat_dicts(*dicts):
+    result = {}
+    for d in dicts:
+        result.update(d)
+    return result
+
+_distribution_attrs = {
+    "abi": attr.string(
+        default = "none",
+        doc = "Python ABI tag. 'none' for pure-Python wheels.",
+    ),
+    "distribution": attr.string(
+        mandatory = True,
+        doc = """
+Name of the distribution.
+
+This should match the project name onm PyPI. It's also the name that is used to
+refer to the package in other packages' dependencies.
+""",
+    ),
+    "platform": attr.string(
+        default = "any",
+        doc = """\
+Supported platform. Use 'any' for pure-Python wheel.
+
+If you have included platform-specific data, such as a .pyd or .so
+extension module, you will need to specify the platform in standard
+pip format. If you support multiple platforms, you can define
+platform constraints, then use a select() to specify the appropriate
+specifier, eg:
+
+    platform = select({
+        "//platforms:windows_x86_64": "win_amd64",
+        "//platforms:macos_x86_64": "macosx_10_7_x86_64",
+        "//platforms:linux_x86_64": "manylinux2014_x86_64",
+    })
+""",
+    ),
+    "python_tag": attr.string(
+        default = "py3",
+        doc = "Supported Python version(s), eg 'py3', 'cp35.cp36', etc",
+    ),
+    "version": attr.string(
+        mandatory = True,
+        doc = "Version number of the package",
+    ),
+}
+
+_requirement_attrs = {
+    "extra_requires": attr.string_list_dict(
+        doc = "List of optional requirements for this package",
+    ),
+    "requires": attr.string_list(
+        doc = "List of requirements for this package",
+    ),
+}
+
+_entrypoint_attrs = {
+    "console_scripts": attr.string_dict(
+        doc = """\
+Deprecated console_script entry points, e.g. {'main': 'examples.wheel.main:main'}.
+
+Deprecated: prefer the `entry_points` attribute, which supports `console_scripts` as well as other entry points.
+""",
+    ),
+    "entry_points": attr.string_list_dict(
+        doc = """\
+entry_points, e.g. {'console_scripts': ['main = examples.wheel.main:main']}.
+""",
+    ),
+}
+
+_other_attrs = {
+    "author": attr.string(default = ""),
+    "author_email": attr.string(default = ""),
+    "classifiers": attr.string_list(),
+    "description_file": attr.label(allow_single_file = True),
+    "homepage": attr.string(default = ""),
+    "license": attr.string(default = ""),
+    "python_requires": attr.string(default = ""),
+    "strip_path_prefixes": attr.string_list(
+        default = [],
+        doc = "path prefixes to strip from files added to the generated package",
+    ),
+}
+
+py_wheel = rule(
+    implementation = _py_wheel_impl,
+    doc = """
+A rule for building Python Wheels.
+
+Wheels are Python distribution format defined in https://www.python.org/dev/peps/pep-0427/.
+
+This rule packages a set of targets into a single wheel.
+
+Currently only pure-python wheels are supported.
+
+Examples:
+
+<code>
+# Package just a specific py_libraries, without their dependencies
+py_wheel(
+    name = "minimal_with_py_library",
+    # Package data. We're building "example_minimal_library-0.0.1-py3-none-any.whl"
+    distribution = "example_minimal_library",
+    python_tag = "py3",
+    version = "0.0.1",
+    deps = [
+        "//examples/wheel/lib:module_with_data",
+        "//examples/wheel/lib:simple_module",
+    ],
+)
+
+# Use py_package to collect all transitive dependencies of a target,
+# selecting just the files within a specific python package.
+py_package(
+    name = "example_pkg",
+    # Only include these Python packages.
+    packages = ["examples.wheel"],
+    deps = [":main"],
+)
+
+py_wheel(
+    name = "minimal_with_py_package",
+    # Package data. We're building "example_minimal_package-0.0.1-py3-none-any.whl"
+    distribution = "example_minimal_package",
+    python_tag = "py3",
+    version = "0.0.1",
+    deps = [":example_pkg"],
+)
+</code>
+""",
+    attrs = _concat_dicts(
+        {
+            "deps": attr.label_list(
+                doc = """\
+Targets to be included in the distribution.
+
+The targets to package are usually `py_library` rules or filesets (for packaging data files).
+
+Note it's usually better to package `py_library` targets and use
+`entry_points` attribute to specify `console_scripts` than to package
+`py_binary` rules. `py_binary` targets would wrap a executable script that
+tries to locate `.runfiles` directory which is not packaged in the wheel.
+""",
+            ),
+            "_wheelmaker": attr.label(
+                executable = True,
+                cfg = "host",
+                default = "//tools:wheelmaker",
+            ),
+        },
+        _distribution_attrs,
+        _requirement_attrs,
+        _entrypoint_attrs,
+        _other_attrs,
+    ),
+)
diff --git a/python/pip_install/extract_wheels/lib/BUILD b/python/pip_install/extract_wheels/lib/BUILD
index de67b29..2a26985 100644
--- a/python/pip_install/extract_wheels/lib/BUILD
+++ b/python/pip_install/extract_wheels/lib/BUILD
@@ -51,7 +51,7 @@
     deps = [
         ":lib",
     ],
-    data = ["//experimental/examples/wheel:minimal_with_py_package"]
+    data = ["//examples/wheel:minimal_with_py_package"]
 )
 
 py_test(
diff --git a/python/pip_install/extract_wheels/lib/whl_filegroup_test.py b/python/pip_install/extract_wheels/lib/whl_filegroup_test.py
index 39589c1..a338a14 100644
--- a/python/pip_install/extract_wheels/lib/whl_filegroup_test.py
+++ b/python/pip_install/extract_wheels/lib/whl_filegroup_test.py
@@ -7,7 +7,7 @@
 class TestExtractWheel(unittest.TestCase):
     def test_generated_build_file_has_filegroup_target(self) -> None:
         wheel_name = "example_minimal_package-0.0.1-py3-none-any.whl"
-        wheel_dir = "experimental/examples/wheel/"
+        wheel_dir = "examples/wheel/"
         wheel_path = wheel_dir + wheel_name
         generated_bazel_dir = bazel.extract_wheel(
             wheel_path,