feat: Expose Python C headers through the toolchain. (#1287)

This allows getting a build's `cc_library` of Python headers through
toolchain resolution instead of having to use the underlying toolchain's
repository `:python_headers` target directly.

Without this feature, it's not possible to reliably and correctly get
the C information about the runtime a build is going to use. Existing
solutions require carefully setting up repo names, external state,
and/or using specific build rules. In comparison, with this feature,
consumers are able to simply ask for the current headers via a helper
target or manually lookup the toolchain and pull the relevant
information; toolchain resolution handles finding the correct headers.

The basic way this works is by registering a second toolchain to carry
C/C++ related information; as such, it is named `py_cc_toolchain`. The
py cc toolchain has the same constraint settings as the regular py
toolchain; an expected invariant is that there is a 1:1 correspondence
between the two. This base functionality allows a consuming rule
implementation to use toolchain resolution to find the Python C
toolchain information.

Usually what downstream consumers need are the headers to feed into
another `cc_library` (or equivalent) target, so, rather than have every
project re-implement the same "lookup and forward cc_library info"
logic,
this is provided by the `//python/cc:current_py_cc_headers` target.
Targets that need the headers can then depend on that target as if it
was a `cc_library` target.

Work towards https://github.com/bazelbuild/rules_python/issues/824
diff --git a/python/private/util.bzl b/python/private/util.bzl
index f0d4373..4c4b8fc 100644
--- a/python/private/util.bzl
+++ b/python/private/util.bzl
@@ -1,7 +1,25 @@
+# Copyright 2023 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.
+
 """Functionality shared by multiple pieces of code."""
 
 load("@bazel_skylib//lib:types.bzl", "types")
 
+# When bzlmod is enabled, canonical repos names have @@ in them, while under
+# workspace builds, there is never a @@ in labels.
+BZLMOD_ENABLED = "@@" in str(Label("//:unused"))
+
 def copy_propagating_kwargs(from_kwargs, into_kwargs = None):
     """Copies args that must be compatible between two targets with a dependency relationship.
 
@@ -46,15 +64,26 @@
     Returns:
         The same `attrs` object, but modified.
     """
+    add_tag(attrs, _MIGRATION_TAG)
+    return attrs
+
+def add_tag(attrs, tag):
+    """Adds `tag` to `attrs["tags"]`.
+
+    Args:
+        attrs: dict of keyword args. It is modified in place.
+        tag: str, the tag to add.
+    """
     if "tags" in attrs and attrs["tags"] != None:
         tags = attrs["tags"]
 
         # Preserve the input type: this allows a test verifying the underlying
         # rule can accept the tuple for the tags argument.
         if types.is_tuple(tags):
-            attrs["tags"] = tags + (_MIGRATION_TAG,)
+            attrs["tags"] = tags + (tag,)
         else:
-            attrs["tags"] = tags + [_MIGRATION_TAG]
+            # List concatenation is necessary because the original value
+            # may be a frozen list.
+            attrs["tags"] = tags + [tag]
     else:
-        attrs["tags"] = [_MIGRATION_TAG]
-    return attrs
+        attrs["tags"] = [tag]