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/BUILD.bazel b/python/BUILD.bazel
index d75889d..c5f2580 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -33,6 +33,7 @@
filegroup(
name = "distribution",
srcs = glob(["**"]) + [
+ "//python/cc:distribution",
"//python/config_settings:distribution",
"//python/constraints:distribution",
"//python/private:distribution",
diff --git a/python/cc/BUILD.bazel b/python/cc/BUILD.bazel
new file mode 100644
index 0000000..d4a6bb8
--- /dev/null
+++ b/python/cc/BUILD.bazel
@@ -0,0 +1,44 @@
+# Package for C/C++ specific functionality of the Python rules.
+
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:current_py_cc_headers.bzl", "current_py_cc_headers")
+load("//python/private:util.bzl", "BZLMOD_ENABLED")
+
+package(
+ default_visibility = ["//:__subpackages__"],
+)
+
+# This target provides the C headers for whatever the current toolchain is
+# for the consuming rule. It basically acts like a cc_library by forwarding
+# on the providers for the underlying cc_library that the toolchain is using.
+current_py_cc_headers(
+ name = "current_py_cc_headers",
+ # Building this directly will fail unless a py cc toolchain is registered,
+ # and it's only under bzlmod that one is registered by default.
+ tags = [] if BZLMOD_ENABLED else ["manual"],
+ visibility = ["//visibility:public"],
+)
+
+toolchain_type(
+ name = "toolchain_type",
+ visibility = ["//visibility:public"],
+)
+
+bzl_library(
+ name = "py_cc_toolchain_bzl",
+ srcs = ["py_cc_toolchain.bzl"],
+ visibility = ["//visibility:public"],
+ deps = ["//python/private:py_cc_toolchain_bzl"],
+)
+
+bzl_library(
+ name = "py_cc_toolchain_info_bzl",
+ srcs = ["py_cc_toolchain_info.bzl"],
+ visibility = ["//visibility:public"],
+ deps = ["//python/private:py_cc_toolchain_info_bzl"],
+)
+
+filegroup(
+ name = "distribution",
+ srcs = glob(["**"]),
+)
diff --git a/python/cc/py_cc_toolchain.bzl b/python/cc/py_cc_toolchain.bzl
new file mode 100644
index 0000000..2e782ef
--- /dev/null
+++ b/python/cc/py_cc_toolchain.bzl
@@ -0,0 +1,19 @@
+# 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.
+
+"""Public entry point for py_cc_toolchain rule."""
+
+load("//python/private:py_cc_toolchain_macro.bzl", _py_cc_toolchain = "py_cc_toolchain")
+
+py_cc_toolchain = _py_cc_toolchain
diff --git a/python/cc/py_cc_toolchain_info.bzl b/python/cc/py_cc_toolchain_info.bzl
new file mode 100644
index 0000000..9ea394a
--- /dev/null
+++ b/python/cc/py_cc_toolchain_info.bzl
@@ -0,0 +1,23 @@
+# 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.
+
+"""Provider for C/C++ information about the Python runtime.
+
+NOTE: This is a beta-quality feature. APIs subject to change until
+https://github.com/bazelbuild/rules_python/issues/824 is considered done.
+"""
+
+load("//python/private:py_cc_toolchain_info.bzl", _PyCcToolchainInfo = "PyCcToolchainInfo")
+
+PyCcToolchainInfo = _PyCcToolchainInfo
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index f454f42..10af17e 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -51,6 +51,28 @@
deps = ["@bazel_skylib//lib:types"],
)
+bzl_library(
+ name = "py_cc_toolchain_bzl",
+ srcs = [
+ "py_cc_toolchain_macro.bzl",
+ "py_cc_toolchain_rule.bzl",
+ ],
+ visibility = [
+ "//docs:__subpackages__",
+ "//python/cc:__pkg__",
+ ],
+ deps = [
+ ":py_cc_toolchain_info_bzl",
+ ":util_bzl",
+ ],
+)
+
+bzl_library(
+ name = "py_cc_toolchain_info_bzl",
+ srcs = ["py_cc_toolchain_info.bzl"],
+ visibility = ["//python/cc:__pkg__"],
+)
+
# @bazel_tools can't define bzl_library itself, so we just put a wrapper around it.
bzl_library(
name = "bazel_tools_bzl",
@@ -73,6 +95,7 @@
"reexports.bzl",
"stamp.bzl",
"util.bzl",
+ "py_cc_toolchain_rule.bzl",
],
visibility = ["//docs:__pkg__"],
)
diff --git a/python/private/current_py_cc_headers.bzl b/python/private/current_py_cc_headers.bzl
new file mode 100644
index 0000000..be7f8f8
--- /dev/null
+++ b/python/private/current_py_cc_headers.bzl
@@ -0,0 +1,41 @@
+# 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.
+
+"""Implementation of current_py_cc_headers rule."""
+
+def _current_py_cc_headers_impl(ctx):
+ py_cc_toolchain = ctx.toolchains["//python/cc:toolchain_type"].py_cc_toolchain
+ return py_cc_toolchain.headers.providers_map.values()
+
+current_py_cc_headers = rule(
+ implementation = _current_py_cc_headers_impl,
+ toolchains = ["//python/cc:toolchain_type"],
+ provides = [CcInfo],
+ doc = """\
+Provides the currently active Python toolchain's C headers.
+
+This is a wrapper around the underlying `cc_library()` for the
+C headers for the consuming target's currently active Python toolchain.
+
+To use, simply depend on this target where you would have wanted the
+toolchain's underlying `:python_headers` target:
+
+```starlark
+cc_library(
+ name = "foo",
+ deps = ["@rules_python//python/cc:current_py_cc_headers"]
+)
+```
+""",
+)
diff --git a/python/private/py_cc_toolchain_info.bzl b/python/private/py_cc_toolchain_info.bzl
new file mode 100644
index 0000000..e7afc10
--- /dev/null
+++ b/python/private/py_cc_toolchain_info.bzl
@@ -0,0 +1,43 @@
+# 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.
+
+"""Implementation of PyCcToolchainInfo."""
+
+PyCcToolchainInfo = provider(
+ doc = "C/C++ information about the Python runtime.",
+ fields = {
+ "headers": """\
+(struct) Information about the header files, with fields:
+ * providers_map: a dict of string to provider instances. The key should be
+ a fully qualified name (e.g. `@rules_foo//bar:baz.bzl#MyInfo`) of the
+ provider to uniquely identify its type.
+
+ The following keys are always present:
+ * CcInfo: the CcInfo provider instance for the headers.
+ * DefaultInfo: the DefaultInfo provider instance for the headers.
+
+ A map is used to allow additional providers from the originating headers
+ target (typically a `cc_library`) to be propagated to consumers (directly
+ exposing a Target object can cause memory issues and is an anti-pattern).
+
+ When consuming this map, it's suggested to use `providers_map.values()` to
+ return all providers; or copy the map and filter out or replace keys as
+ appropriate. Note that any keys begining with `_` (underscore) are
+ considered private and should be forward along as-is (this better allows
+ e.g. `:current_py_cc_headers` to act as the underlying headers target it
+ represents).
+""",
+ "python_version": "(str) The Python Major.Minor version.",
+ },
+)
diff --git a/python/private/py_cc_toolchain_macro.bzl b/python/private/py_cc_toolchain_macro.bzl
new file mode 100644
index 0000000..35276f7
--- /dev/null
+++ b/python/private/py_cc_toolchain_macro.bzl
@@ -0,0 +1,31 @@
+# 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.
+
+"""Fronting macro for the py_cc_toolchain rule."""
+
+load(":py_cc_toolchain_rule.bzl", _py_cc_toolchain = "py_cc_toolchain")
+load(":util.bzl", "add_tag")
+
+# A fronting macro is used because macros have user-observable behavior;
+# using one from the onset avoids introducing those changes in the future.
+def py_cc_toolchain(**kwargs):
+ """Creates a py_cc_toolchain target.
+
+ Args:
+ **kwargs: Keyword args to pass onto underlying rule.
+ """
+
+ # This tag is added to easily identify usages through other macros.
+ add_tag(kwargs, "@rules_python//python:py_cc_toolchain")
+ _py_cc_toolchain(**kwargs)
diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl
new file mode 100644
index 0000000..c80f845
--- /dev/null
+++ b/python/private/py_cc_toolchain_rule.bzl
@@ -0,0 +1,57 @@
+# 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.
+
+"""Implementation of py_cc_toolchain rule.
+
+NOTE: This is a beta-quality feature. APIs subject to change until
+https://github.com/bazelbuild/rules_python/issues/824 is considered done.
+"""
+
+load(":py_cc_toolchain_info.bzl", "PyCcToolchainInfo")
+
+def _py_cc_toolchain_impl(ctx):
+ py_cc_toolchain = PyCcToolchainInfo(
+ headers = struct(
+ providers_map = {
+ "CcInfo": ctx.attr.headers[CcInfo],
+ "DefaultInfo": ctx.attr.headers[DefaultInfo],
+ },
+ ),
+ python_version = ctx.attr.python_version,
+ )
+ return [platform_common.ToolchainInfo(
+ py_cc_toolchain = py_cc_toolchain,
+ )]
+
+py_cc_toolchain = rule(
+ implementation = _py_cc_toolchain_impl,
+ attrs = {
+ "headers": attr.label(
+ doc = ("Target that provides the Python headers. Typically this " +
+ "is a cc_library target."),
+ providers = [CcInfo],
+ mandatory = True,
+ ),
+ "python_version": attr.string(
+ doc = "The Major.minor Python version, e.g. 3.11",
+ mandatory = True,
+ ),
+ },
+ doc = """\
+A toolchain for a Python runtime's C/C++ information (e.g. headers)
+
+This rule carries information about the C/C++ side of a Python runtime, e.g.
+headers, shared libraries, etc.
+""",
+)
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index f47ea8f..5923787 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -83,6 +83,15 @@
toolchain = "@{user_repository_name}_{platform}//:python_runtimes",
toolchain_type = "@bazel_tools//tools/python:toolchain_type",
)
+
+toolchain(
+ name = "{prefix}{platform}_py_cc_toolchain",
+ target_compatible_with = {compatible_with},
+ target_settings = {target_settings},
+ toolchain = "@{user_repository_name}_{platform}//:py_cc_toolchain",
+ toolchain_type = "@rules_python//python/cc:toolchain_type",
+
+)
""".format(
compatible_with = meta.compatible_with,
platform = platform,
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]
diff --git a/python/repositories.bzl b/python/repositories.bzl
index 04de657..38a580e 100644
--- a/python/repositories.bzl
+++ b/python/repositories.bzl
@@ -265,6 +265,7 @@
# Generated by python/repositories.bzl
load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair")
+load("@rules_python//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")
package(default_visibility = ["//visibility:public"])
@@ -336,6 +337,12 @@
py2_runtime = None,
py3_runtime = ":py3_runtime",
)
+
+py_cc_toolchain(
+ name = "py_cc_toolchain",
+ headers = ":python_headers",
+ python_version = "{python_version}",
+)
""".format(
glob_exclude = repr(glob_exclude),
glob_include = repr(glob_include),