Skip to content

Commit bb546a7

Browse files
committed
feat: Expose Python C headers through the toolchain.
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 special 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 reimplement 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 bazel-contrib#824
1 parent 5b8fa22 commit bb546a7

19 files changed

+718
-4
lines changed

python/cc/BUILD.bazel

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Package for C/C++ specific functionality of the Python rules.
2+
3+
load("//python/private:current_py_cc_headers.bzl", "current_py_cc_headers")
4+
5+
# This target provides the C headers for whatever the current toolchain is
6+
# for the consuming rule. It basically acts like a cc_library by forwarding
7+
# on the providers for the underlying cc_library that the toolchain is using.
8+
current_py_cc_headers(
9+
name = "current_py_cc_headers",
10+
visibility = ["//visibility:public"],
11+
)
12+
13+
toolchain_type(
14+
name = "toolchain_type",
15+
visibility = ["//visibility:public"],
16+
)

python/cc/py_cc_toolchain.bzl

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Public entry point for py_cc_toolchain rule."""
16+
17+
load("//python/private:py_cc_toolchain_macro.bzl", _py_cc_toolchain = "py_cc_toolchain")
18+
19+
py_cc_toolchain = _py_cc_toolchain

python/cc/py_cc_toolchain_info.bzl

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Public entry point for PyCcToolchainInfo."""
16+
17+
load("//python/private:py_cc_toolchain_info.bzl", _PyCcToolchainInfo = "PyCcToolchainInfo")
18+
19+
PyCcToolchainInfo = _PyCcToolchainInfo
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Implementation of current_py_cc_headers rule."""
16+
17+
def _current_py_cc_headers_impl(ctx):
18+
py_cc_toolchain = ctx.toolchains["//python/cc:toolchain_type"].py_cc_toolchain
19+
return py_cc_toolchain.headers.providers_map.values()
20+
21+
current_py_cc_headers = rule(
22+
implementation = _current_py_cc_headers_impl,
23+
toolchains = ["//python/cc:toolchain_type"],
24+
provides = [CcInfo],
25+
)
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Implementation of PyCcToolchainInfo."""
16+
17+
PyCcToolchainInfo = provider(
18+
doc = "C/C++ information about the Python runtime.",
19+
fields = {
20+
"headers": """\
21+
(struct) Information about the header files, with fields:
22+
* providers_map: a dict of string to provider instances. The key should be
23+
a fully qualified name (e.g. `@rules_foo//bar:baz.bzl#MyInfo`) of the
24+
provider to uniquely identify its type.
25+
26+
The following keys are always present:
27+
* CcInfo: the CcInfo provider instance for the headers.
28+
* DefaultInfo: the DefaultInfo provider instance for the headers.
29+
30+
A map is used to allow additional providers from the originating headers
31+
target (typically a `cc_library`) to be propagated to consumers (directly
32+
exposing a Target object can cause memory issues and is an anti-pattern).
33+
34+
When consuming this map, it's suggested to use `providers_map.values()` to
35+
return all providers; or copy the map and filter out or replace keys as
36+
appropriate. Note that any keys begining with `_` (underscore) are
37+
considered private and should be forward along as-is (this better allows
38+
e.g. `:current_py_cc_headers` to act as the underlying headers target it
39+
represents).
40+
""",
41+
"python_version": "(str) The Python Major.Minor version.",
42+
},
43+
)
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Fronting macro for the py_cc_toolchain rule."""
16+
17+
load(":py_cc_toolchain_rule.bzl", _py_cc_toolchain = "py_cc_toolchain")
18+
load(":util.bzl", "add_tag")
19+
20+
# A fronting macro is used because macros have user-observable behavior;
21+
# using one from the onset avoids introducing those changes in the future.
22+
def py_cc_toolchain(**kwargs):
23+
# This tag is added to easily identify usages through other macros.
24+
add_tag(kwargs, "@rules_python//python:py_cc_toolchain")
25+
_py_cc_toolchain(**kwargs)
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Implementation of py_cc_toolchain rule."""
16+
17+
load(":py_cc_toolchain_info.bzl", "PyCcToolchainInfo")
18+
19+
def _py_cc_toolchain_impl(ctx):
20+
py_cc_toolchain = PyCcToolchainInfo(
21+
headers = struct(
22+
providers_map = {
23+
"CcInfo": ctx.attr.headers[CcInfo],
24+
"DefaultInfo": ctx.attr.headers[DefaultInfo],
25+
},
26+
),
27+
python_version = ctx.attr.python_version,
28+
)
29+
return [platform_common.ToolchainInfo(
30+
py_cc_toolchain = py_cc_toolchain,
31+
)]
32+
33+
py_cc_toolchain = rule(
34+
implementation = _py_cc_toolchain_impl,
35+
attrs = {
36+
"headers": attr.label(
37+
doc = ("Target that provides the Python headers. Typically this " +
38+
"is a cc_library target."),
39+
providers = [CcInfo],
40+
mandatory = True,
41+
),
42+
"python_version": attr.string(
43+
doc = "The Major.minor Python version, e.g. 3.11",
44+
mandatory = True,
45+
),
46+
},
47+
)

python/private/toolchains_repo.bzl

+9
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ toolchain(
8383
toolchain = "@{user_repository_name}_{platform}//:python_runtimes",
8484
toolchain_type = "@bazel_tools//tools/python:toolchain_type",
8585
)
86+
87+
toolchain(
88+
name = "{prefix}{platform}_py_cc_toolchain",
89+
target_compatible_with = {compatible_with},
90+
target_settings = {target_settings},
91+
toolchain = "@{user_repository_name}_{platform}//:py_cc_toolchain",
92+
toolchain_type = "@rules_python//python/cc:toolchain_type",
93+
94+
)
8695
""".format(
8796
compatible_with = meta.compatible_with,
8897
platform = platform,

python/private/util.bzl

+29-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
115
"""Functionality shared by multiple pieces of code."""
216

317
load("@bazel_skylib//lib:types.bzl", "types")
@@ -46,15 +60,26 @@ def add_migration_tag(attrs):
4660
Returns:
4761
The same `attrs` object, but modified.
4862
"""
63+
add_tag(attrs, _MIGRATION_TAG)
64+
return attrs
65+
66+
def add_tag(attrs, tag):
67+
"""Adds `tag` to `attrs["tags"]`.
68+
69+
Args:
70+
attrs: dict of keyword args. It is modified in place.
71+
tag: str, the tag to add.
72+
"""
4973
if "tags" in attrs and attrs["tags"] != None:
5074
tags = attrs["tags"]
5175

5276
# Preserve the input type: this allows a test verifying the underlying
5377
# rule can accept the tuple for the tags argument.
5478
if types.is_tuple(tags):
55-
attrs["tags"] = tags + (_MIGRATION_TAG,)
79+
attrs["tags"] = tags + (tag,)
5680
else:
57-
attrs["tags"] = tags + [_MIGRATION_TAG]
81+
# List concatenation is necessary because the original value
82+
# may be a frozen list.
83+
attrs["tags"] = tags + [tag]
5884
else:
59-
attrs["tags"] = [_MIGRATION_TAG]
60-
return attrs
85+
attrs["tags"] = [tag]

python/repositories.bzl

+7
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ def _python_repository_impl(rctx):
265265
# Generated by python/repositories.bzl
266266
267267
load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair")
268+
load("@rules_python//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")
268269
269270
package(default_visibility = ["//visibility:public"])
270271
@@ -336,6 +337,12 @@ py_runtime_pair(
336337
py2_runtime = None,
337338
py3_runtime = ":py3_runtime",
338339
)
340+
341+
py_cc_toolchain(
342+
name = "py_cc_toolchain",
343+
headers = ":python_headers",
344+
python_version = "{python_version}",
345+
)
339346
""".format(
340347
glob_exclude = repr(glob_exclude),
341348
glob_include = repr(glob_include),

tests/cc/BUILD.bazel

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Tests for current_py_cc_headers rule
16+
17+
load("@rules_cc//cc:defs.bzl", "cc_library")
18+
load("@rules_testing//lib:util.bzl", "PREVENT_IMPLICIT_BUILDING_TAGS")
19+
load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")
20+
21+
package(default_visibility = ["//:__subpackages__"])
22+
23+
toolchain(
24+
name = "fake_py_cc_toolchain",
25+
tags = PREVENT_IMPLICIT_BUILDING_TAGS,
26+
toolchain = ":fake_py_cc_toolchain_impl",
27+
toolchain_type = "@rules_python//python/cc:toolchain_type",
28+
)
29+
30+
py_cc_toolchain(
31+
name = "fake_py_cc_toolchain_impl",
32+
testonly = True,
33+
headers = ":fake_headers",
34+
python_version = "3.999",
35+
tags = PREVENT_IMPLICIT_BUILDING_TAGS,
36+
)
37+
38+
cc_library(
39+
name = "fake_headers",
40+
testonly = True,
41+
hdrs = ["fake_header.h"],
42+
data = ["data.txt"],
43+
includes = ["fake_include"],
44+
tags = PREVENT_IMPLICIT_BUILDING_TAGS,
45+
)
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load(":current_py_cc_headers_tests.bzl", "current_py_cc_headers_test_suite")
16+
17+
current_py_cc_headers_test_suite(name = "current_py_cc_headers_tests")

0 commit comments

Comments
 (0)