Add subpackages module to skylib to support new bazel native.subpackages (#348)

diff --git a/README.md b/README.md
index 3c4ec2b..52733eb 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@
 * [new_sets](docs/new_sets_doc.md)
 * [shell](docs/shell_doc.md)
 * [structs](docs/structs_doc.md)
+* [subpackages](docs/subpackages_doc.md)
 * [types](docs/types_doc.md)
 * [unittest](docs/unittest_doc.md)
 * [versions](docs/versions_doc.md)
diff --git a/docs/BUILD b/docs/BUILD
index 284db3c..988b53a 100644
--- a/docs/BUILD
+++ b/docs/BUILD
@@ -83,6 +83,11 @@
 )
 
 stardoc_with_diff_test(
+    bzl_library_target = "//lib:subpackages",
+    out_label = "//docs:subpackages_doc.md",
+)
+
+stardoc_with_diff_test(
     bzl_library_target = "//lib:types",
     out_label = "//docs:types_doc.md",
 )
diff --git a/docs/subpackages_doc.md b/docs/subpackages_doc.md
new file mode 100755
index 0000000..b2363cd
--- /dev/null
+++ b/docs/subpackages_doc.md
@@ -0,0 +1,96 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+Skylib module containing common functions for working with native.subpackages()
+
+
+<a id="#subpackages.all"></a>
+
+## subpackages.all
+
+<pre>
+subpackages.all(<a href="#subpackages.all-exclude">exclude</a>, <a href="#subpackages.all-allow_empty">allow_empty</a>, <a href="#subpackages.all-fully_qualified">fully_qualified</a>)
+</pre>
+
+List all direct subpackages of the current package regardless of directory depth.
+
+The returned list contains all subpackages, but not subpackages of subpackages.
+
+Example:
+Assuming the following BUILD files exist:
+
+    BUILD
+    foo/BUILD
+    foo/sub/BUILD
+    bar/BUILD
+    baz/deep/dir/BUILD
+
+If the current package is '//' all() will return ['//foo', '//bar',
+'//baz/deep/dir'].  //foo/sub is not included because it is a direct
+subpackage of '//foo' not '//'
+
+NOTE: fail()s if native.subpackages() is not supported.
+
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="subpackages.all-exclude"></a>exclude |  see native.subpackages(exclude)   |  <code>[]</code> |
+| <a id="subpackages.all-allow_empty"></a>allow_empty |  see native.subpackages(allow_empty)   |  <code>False</code> |
+| <a id="subpackages.all-fully_qualified"></a>fully_qualified |  It true return fully qualified Labels for subpackages, otherwise returns subpackage path relative to current package.   |  <code>True</code> |
+
+**RETURNS**
+
+A mutable sorted list containing all sub-packages of the current Bazel
+package.
+
+
+<a id="#subpackages.exists"></a>
+
+## subpackages.exists
+
+<pre>
+subpackages.exists(<a href="#subpackages.exists-relative_path">relative_path</a>)
+</pre>
+
+Checks to see if relative_path is a direct subpackage of the current package.
+
+Example:
+
+    BUILD
+    foo/BUILD
+    foo/sub/BUILD
+
+If the current package is '//' (the top-level BUILD file):
+    subpackages.exists("foo") == True
+    subpackages.exists("foo/sub") == False
+    subpackages.exists("bar") == False
+
+NOTE: fail()s if native.subpackages() is not supported in the current Bazel version.
+
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="subpackages.exists-relative_path"></a>relative_path |  a path to a subpackage to test, must not be an absolute Label.   |  none |
+
+**RETURNS**
+
+True if 'relative_path' is a subpackage of the current package.
+
+
+<a id="#subpackages.supported"></a>
+
+## subpackages.supported
+
+<pre>
+subpackages.supported()
+</pre>
+
+
+
+
+
diff --git a/lib/BUILD b/lib/BUILD
index 7e7a9a4..2328081 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -62,6 +62,11 @@
 )
 
 bzl_library(
+    name = "subpackages",
+    srcs = ["subpackages.bzl"],
+)
+
+bzl_library(
     name = "types",
     srcs = ["types.bzl"],
 )
diff --git a/lib/subpackages.bzl b/lib/subpackages.bzl
new file mode 100644
index 0000000..5b674fd
--- /dev/null
+++ b/lib/subpackages.bzl
@@ -0,0 +1,96 @@
+# Copyright 2022 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.
+
+"""Skylib module containing common functions for working with native.subpackages()
+"""
+_SUBPACKAGES_SUPPORTED = hasattr(native, "subpackages")
+
+def _supported():
+    return _SUBPACKAGES_SUPPORTED
+
+def _check_supported():
+    if not _SUBPACKAGES_SUPPORTED:
+        fail("native.subpackages not supported in this version of Bazel.")
+
+def _all(exclude = [], allow_empty = False, fully_qualified = True):
+    """List all direct subpackages of the current package regardless of directory depth.
+
+    The returned list contains all subpackages, but not subpackages of subpackages.
+
+    Example:
+    Assuming the following BUILD files exist:
+
+        BUILD
+        foo/BUILD
+        foo/sub/BUILD
+        bar/BUILD
+        baz/deep/dir/BUILD
+
+    If the current package is '//' all() will return ['//foo', '//bar',
+    '//baz/deep/dir'].  //foo/sub is not included because it is a direct
+    subpackage of '//foo' not '//'
+
+    NOTE: fail()s if native.subpackages() is not supported.
+
+    Args:
+      exclude:          see native.subpackages(exclude)
+      allow_empty:      see native.subpackages(allow_empty)
+      fully_qualified:  It true return fully qualified Labels for subpackages,
+          otherwise returns subpackage path relative to current package.
+
+    Returns:
+      A mutable sorted list containing all sub-packages of the current Bazel
+      package.
+    """
+    _check_supported()
+
+    subs = native.subpackages(include = ["**"], exclude = exclude, allow_empty = allow_empty)
+    if fully_qualified:
+        return [_fully_qualified(s) for s in subs]
+
+    return subs
+
+def _fully_qualified(relative_path):
+    return "//%s/%s" % (native.package_name(), relative_path)
+
+def _exists(relative_path):
+    """Checks to see if relative_path is a direct subpackage of the current package.
+
+    Example:
+
+        BUILD
+        foo/BUILD
+        foo/sub/BUILD
+
+    If the current package is '//' (the top-level BUILD file):
+        subpackages.exists("foo") == True
+        subpackages.exists("foo/sub") == False
+        subpackages.exists("bar") == False
+
+    NOTE: fail()s if native.subpackages() is not supported in the current Bazel version.
+
+    Args:
+      relative_path: a path to a subpackage to test, must not be an absolute Label.
+
+    Returns:
+      True if 'relative_path' is a subpackage of the current package.
+    """
+    _check_supported()
+    return relative_path in native.subpackages(include = [relative_path], allow_empty = True)
+
+subpackages = struct(
+    all = _all,
+    exists = _exists,
+    supported = _supported,
+)
diff --git a/tests/BUILD b/tests/BUILD
index c2a4223..bbab077 100644
--- a/tests/BUILD
+++ b/tests/BUILD
@@ -8,6 +8,7 @@
 load(":selects_tests.bzl", "selects_test_suite")
 load(":shell_tests.bzl", "shell_args_test_gen", "shell_test_suite")
 load(":structs_tests.bzl", "structs_test_suite")
+load(":subpackages_tests.bzl", "subpackages_test_suite")
 load(":types_tests.bzl", "types_test_suite")
 load(":unittest_tests.bzl", "unittest_passing_tests_suite")
 load(":versions_tests.bzl", "versions_test_suite")
@@ -37,6 +38,8 @@
 
 structs_test_suite()
 
+subpackages_test_suite()
+
 types_test_suite()
 
 unittest_passing_tests_suite()
diff --git a/tests/subpackages_tests.bzl b/tests/subpackages_tests.bzl
new file mode 100644
index 0000000..623a446
--- /dev/null
+++ b/tests/subpackages_tests.bzl
@@ -0,0 +1,84 @@
+# Copyright 2022 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.
+
+"""Unit tests for subpackages.bzl."""
+
+load("//lib:subpackages.bzl", "subpackages")
+load("//lib:unittest.bzl", "loadingtest")
+
+def _all_test(env):
+    """Unit tests for subpackages.all."""
+
+    all_pkgs = [
+        "copy_file",
+        "diff_test",
+        "expand_template",
+        "select_file",
+        "write_file",
+    ]
+
+    # Not all pkgs exist in all test environments.
+    if subpackages.exists("run_binary"):
+        all_pkgs.append("run_binary")
+
+    if subpackages.exists("native_binary"):
+        all_pkgs.append("native_binary")
+
+    # These exist in all cases
+    filtered_pkgs = [
+        "copy_file",
+        "expand_template",
+        "select_file",
+        "write_file",
+    ]
+
+    # subpackages is always in sorted order:
+    all_pkgs = sorted(all_pkgs)
+
+    # test defaults
+    loadingtest.equals(
+        env,
+        "all",
+        ["//tests/" + pkg for pkg in all_pkgs],
+        subpackages.all(),
+    )
+
+    # test non-fully-qualified output
+    loadingtest.equals(
+        env,
+        "all_not_fully_qualified",
+        all_pkgs,
+        subpackages.all(fully_qualified = False),
+    )
+
+    # test exclusion
+    loadingtest.equals(
+        env,
+        "all_w_exclude",
+        filtered_pkgs,
+        subpackages.all(exclude = ["diff_test", "run_binary", "native_binary"], fully_qualified = False),
+    )
+
+def _exists_test(env):
+    """Unit tests for subpackages.exists."""
+    loadingtest.equals(env, "exists_yes", True, subpackages.exists("copy_file"))
+    loadingtest.equals(env, "exists_no", False, subpackages.exists("never_existed"))
+
+def subpackages_test_suite():
+    """Creates the test targets and test suite for subpackages.bzl tests."""
+
+    if subpackages.supported():
+        env = loadingtest.make("subpackages")
+        _all_test(env)
+        _exists_test(env)