From 8a3cc22610b4de6cd19de89f083aff3093eb7ee9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 21 Jun 2023 06:59:56 +0000 Subject: [PATCH] feat: Expose Python C headers through the toolchain. Tihs 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. This makes it easier and more reliable to build C extensions with the correct Python runtime's headers (e.g. the same ones for the runtime a binary uses). Work towards #824 --- python/cc/BUILD.bazel | 16 +++ python/cc/py_cc_toolchain.bzl | 19 +++ python/cc/py_cc_toolchain_info.bzl | 19 +++ python/private/current_py_cc_headers.bzl | 25 ++++ python/private/py_cc_toolchain_info.bzl | 43 ++++++ python/private/py_cc_toolchain_macro.bzl | 25 ++++ python/private/py_cc_toolchain_rule.bzl | 47 +++++++ python/private/toolchains_repo.bzl | 9 ++ python/private/util.bzl | 27 +++- python/repositories.bzl | 7 + tests/cc/BUILD.bazel | 44 ++++++ tests/cc/current_py_cc_headers/BUILD.bazel | 17 +++ .../current_py_cc_headers_tests.bzl | 68 ++++++++++ tests/cc/py_cc_toolchain/BUILD.bazel | 3 + .../py_cc_toolchain/py_cc_toolchain_tests.bzl | 87 ++++++++++++ tests/cc_info_subject.bzl | 128 ++++++++++++++++++ tests/default_info_subject.bzl | 47 +++++++ tests/py_cc_toolchain_info_subject.bzl | 49 +++++++ tests/struct_subject.bzl | 38 ++++++ 19 files changed, 714 insertions(+), 4 deletions(-) create mode 100644 python/cc/BUILD.bazel create mode 100644 python/cc/py_cc_toolchain.bzl create mode 100644 python/cc/py_cc_toolchain_info.bzl create mode 100644 python/private/current_py_cc_headers.bzl create mode 100644 python/private/py_cc_toolchain_info.bzl create mode 100644 python/private/py_cc_toolchain_macro.bzl create mode 100644 python/private/py_cc_toolchain_rule.bzl create mode 100644 tests/cc/BUILD.bazel create mode 100644 tests/cc/current_py_cc_headers/BUILD.bazel create mode 100644 tests/cc/current_py_cc_headers/current_py_cc_headers_tests.bzl create mode 100644 tests/cc/py_cc_toolchain/BUILD.bazel create mode 100644 tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl create mode 100644 tests/cc_info_subject.bzl create mode 100644 tests/default_info_subject.bzl create mode 100644 tests/py_cc_toolchain_info_subject.bzl create mode 100644 tests/struct_subject.bzl diff --git a/python/cc/BUILD.bazel b/python/cc/BUILD.bazel new file mode 100644 index 0000000000..a6cdd89533 --- /dev/null +++ b/python/cc/BUILD.bazel @@ -0,0 +1,16 @@ +# Package for C/C++ specific functionality of the Python rules. + +load("//python/private:current_py_cc_headers.bzl", "current_py_cc_headers") + +# 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", + visibility = ["//visibility:public"], +) + +toolchain_type( + name = "toolchain_type", + visibility = ["//visibility:public"], +) diff --git a/python/cc/py_cc_toolchain.bzl b/python/cc/py_cc_toolchain.bzl new file mode 100644 index 0000000000..2e782ef9f0 --- /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 0000000000..f11d8dd2cb --- /dev/null +++ b/python/cc/py_cc_toolchain_info.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 PyCcToolchainInfo.""" + +load("//python/private:py_cc_toolchain_info.bzl", _PyCcToolchainInfo = "PyCcToolchainInfo") + +PyCcToolchainInfo = _PyCcToolchainInfo diff --git a/python/private/current_py_cc_headers.bzl b/python/private/current_py_cc_headers.bzl new file mode 100644 index 0000000000..1620c88660 --- /dev/null +++ b/python/private/current_py_cc_headers.bzl @@ -0,0 +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. + +"""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], +) diff --git a/python/private/py_cc_toolchain_info.bzl b/python/private/py_cc_toolchain_info.bzl new file mode 100644 index 0000000000..e7afc10599 --- /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 0000000000..9c1438df83 --- /dev/null +++ b/python/private/py_cc_toolchain_macro.bzl @@ -0,0 +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. + +"""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): + # 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 0000000000..929720c44c --- /dev/null +++ b/python/private/py_cc_toolchain_rule.bzl @@ -0,0 +1,47 @@ +# 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.""" + +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, + ), + }, +) diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index f47ea8f064..592378739e 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -83,6 +83,15 @@ toolchain( 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 f0d43737a0..db8ed46eef 100644 --- a/python/private/util.bzl +++ b/python/private/util.bzl @@ -1,3 +1,17 @@ +# 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") @@ -46,15 +60,20 @@ def add_migration_tag(attrs): Returns: The same `attrs` object, but modified. """ + add_tag(attrs, _MIGRATION_TAG) + return attrs + +def add_tag(attrs, tag): 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 39182af88a..6ec0089c27 100644 --- a/python/repositories.bzl +++ b/python/repositories.bzl @@ -265,6 +265,7 @@ def _python_repository_impl(rctx): # 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 @@ py_runtime_pair( 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), diff --git a/tests/cc/BUILD.bazel b/tests/cc/BUILD.bazel new file mode 100644 index 0000000000..a4ccf3cd15 --- /dev/null +++ b/tests/cc/BUILD.bazel @@ -0,0 +1,44 @@ +# 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. + +# Tests for current_py_cc_headers rule + +load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") +load("@rules_testing//lib:util.bzl", "PREVENT_IMPLICIT_BUILDING_TAGS") + +package(default_visibility = ["//:__subpackages__"]) + +toolchain( + name = "fake_py_cc_toolchain", + tags = PREVENT_IMPLICIT_BUILDING_TAGS, + toolchain = ":fake_py_cc_toolchain_impl", + toolchain_type = "@rules_python//python/cc:toolchain_type", +) + +py_cc_toolchain( + name = "fake_py_cc_toolchain_impl", + testonly = True, + headers = ":fake_headers", + python_version = "3.999", + tags = PREVENT_IMPLICIT_BUILDING_TAGS, +) + +cc_library( + name = "fake_headers", + testonly = True, + hdrs = ["fake_header.h"], + data = ["data.txt"], + includes = ["fake_include"], + tags = PREVENT_IMPLICIT_BUILDING_TAGS, +) diff --git a/tests/cc/current_py_cc_headers/BUILD.bazel b/tests/cc/current_py_cc_headers/BUILD.bazel new file mode 100644 index 0000000000..e2d6a1b521 --- /dev/null +++ b/tests/cc/current_py_cc_headers/BUILD.bazel @@ -0,0 +1,17 @@ +# 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. + +load(":current_py_cc_headers_tests.bzl", "current_py_cc_headers_test_suite") + +current_py_cc_headers_test_suite(name = "current_py_cc_headers_tests") diff --git a/tests/cc/current_py_cc_headers/current_py_cc_headers_tests.bzl b/tests/cc/current_py_cc_headers/current_py_cc_headers_tests.bzl new file mode 100644 index 0000000000..7b74f259b4 --- /dev/null +++ b/tests/cc/current_py_cc_headers/current_py_cc_headers_tests.bzl @@ -0,0 +1,68 @@ +# 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. + +"""Tests for current_py_cc_headers.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:util.bzl", "TestingAspectInfo", rt_util = "util") +load("@rules_testing//lib:truth.bzl", "matching") +load("//tests:cc_info_subject.bzl", "cc_info_subject") + +_tests = [] + +def _test_current_toolchain_headers(name): + analysis_test( + name = name, + impl = _test_current_toolchain_headers_impl, + target = "//python/cc:current_py_cc_headers", + config_settings = { + "//command_line_option:extra_toolchains": ["//tests/cc:fake_py_cc_toolchain"], + }, + attrs = { + "header": attr.label( + default = "//tests/cc:fake_header.h", + allow_single_file = True, + ), + }, + ) + +def _test_current_toolchain_headers_impl(env, target): + compilation_context = env.expect.that_target(target).provider( + CcInfo, + factory = cc_info_subject, + ).compilation_context() + compilation_context.direct_headers().contains_exactly([ + env.ctx.file.header, + ]) + compilation_context.direct_public_headers().contains_exactly([ + env.ctx.file.header, + ]) + + # NOTE: The include dir gets added twice, once for the source path, + # and once for the config-specific path. + compilation_context.system_includes().contains_at_least_predicates([ + matching.str_matches("*/fake_include"), + ]) + + env.expect.that_target(target).runfiles().contains_predicate( + matching.str_matches("*/cc/data.txt"), + ) + +_tests.append(_test_current_toolchain_headers) + +def current_py_cc_headers_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/cc/py_cc_toolchain/BUILD.bazel b/tests/cc/py_cc_toolchain/BUILD.bazel new file mode 100644 index 0000000000..57d030c750 --- /dev/null +++ b/tests/cc/py_cc_toolchain/BUILD.bazel @@ -0,0 +1,3 @@ +load(":py_cc_toolchain_tests.bzl", "py_cc_toolchain_test_suite") + +py_cc_toolchain_test_suite(name = "py_cc_toolchain_tests") diff --git a/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl b/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl new file mode 100644 index 0000000000..cbbfd28b57 --- /dev/null +++ b/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl @@ -0,0 +1,87 @@ +# 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. + +"""Tests for py_cc_toolchain.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:util.bzl", "TestingAspectInfo", rt_util = "util") +load("@rules_testing//lib:truth.bzl", "matching") +load("//tests:cc_info_subject.bzl", "cc_info_subject") +load("//tests:default_info_subject.bzl", "DefaultInfoSubject") +load("//tests:py_cc_toolchain_info_subject.bzl", "PyCcToolchainInfoSubject") +load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") + +_tests = [] + +def _py_cc_toolchain_test(name): + analysis_test( + name = name, + impl = _py_cc_toolchain_test_impl, + target = "//tests/cc:fake_py_cc_toolchain_impl", + attrs = { + "header": attr.label( + default = "//tests/cc:fake_header.h", + allow_single_file = True, + ), + }, + ) + +def _py_cc_toolchain_test_impl(env, target): + env.expect.that_target(target).has_provider(platform_common.ToolchainInfo) + + toolchain = PyCcToolchainInfoSubject.new( + target[platform_common.ToolchainInfo].py_cc_toolchain, + meta = env.expect.meta.derive(expr = "py_cc_toolchain_info"), + ) + toolchain.python_version().equals("3.999") + + toolchain_headers = toolchain.headers() + toolchain_headers.providers_map().keys().contains_exactly(["CcInfo", "DefaultInfo"]) + + cc_info = cc_info_subject( + # TODO: Use DictSubject.get once available, + # https://github.com/bazelbuild/rules_testing/issues/51 + toolchain_headers.actual.providers_map["CcInfo"], + meta = env.expect.meta.derive(expr = "cc_info"), + ) + + compilation_context = cc_info.compilation_context() + compilation_context.direct_headers().contains_exactly([ + env.ctx.file.header, + ]) + compilation_context.direct_public_headers().contains_exactly([ + env.ctx.file.header, + ]) + + # NOTE: The include dir gets added twice, once for the source path, + # and once for the config-specific path, but we don't care about that. + compilation_context.system_includes().contains_at_least_predicates([ + matching.str_matches("*/fake_include"), + ]) + + default_info = DefaultInfoSubject.new( + toolchain_headers.actual.providers_map["DefaultInfo"], + meta = env.expect.meta.derive(expr = "default_info"), + ) + default_info.runfiles().contains_predicate( + matching.str_matches("*/cc/data.txt"), + ) + +_tests.append(_py_cc_toolchain_test) + +def py_cc_toolchain_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/cc_info_subject.bzl b/tests/cc_info_subject.bzl new file mode 100644 index 0000000000..31ac03a035 --- /dev/null +++ b/tests/cc_info_subject.bzl @@ -0,0 +1,128 @@ +# 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. +"""CcInfo testing subject.""" + +load("@rules_testing//lib:truth.bzl", "subjects") + +def cc_info_subject(info, *, meta): + """Creates a new `CcInfoSubject` for a CcInfo provider instance. + + Args: + info: The CcInfo object. + meta: ExpectMeta object. + + Returns: + A `CcInfoSubject` struct. + """ + + # buildifier: disable=uninitialized + public = struct( + # go/keep-sorted start + compilation_context = lambda *a, **k: _cc_info_subject_compilation_context(self, *a, **k), + # go/keep-sorted end + ) + self = struct( + actual = info, + meta = meta, + ) + return public + +def _cc_info_subject_compilation_context(self): + """Returns the CcInfo.compilation_context as a subject. + + Args: + self: implicitly added. + + Returns: + [`CompilationContext`] instance. + """ + return _compilation_context_subject_new( + self.actual.compilation_context, + meta = self.meta.derive("compilation_context()"), + ) + +def _compilation_context_subject_new(info, *, meta): + """Creates a CompilationContextSubject. + + Args: + info: ([`CompilationContext`]) object instance. + meta: rules_testing `ExpectMeta` instance. + + Returns: + [`CompilationContextSubject`] object. + """ + + # buildifier: disable=uninitialized + public = struct( + # go/keep-sorted start + direct_headers = lambda *a, **k: _compilation_context_subject_direct_headers(self, *a, **k), + direct_public_headers = lambda *a, **k: _compilation_context_subject_direct_public_headers(self, *a, **k), + system_includes = lambda *a, **k: _compilation_context_subject_system_includes(self, *a, **k), + # go/keep-sorted end + ) + self = struct( + actual = info, + meta = meta, + ) + return public + +def _compilation_context_subject_direct_headers(self): + """Returns the direct headers as a subjecct. + + Args: + self: implicitly added + + Returns: + [`CollectionSubject`] of `File` objects of the direct headers. + """ + return subjects.collection( + self.actual.direct_headers, + meta = self.meta.derive("direct_headers()"), + container_name = "direct_headers", + element_plural_name = "header files", + ) + +def _compilation_context_subject_direct_public_headers(self): + """Returns the direct public headers as a subjecct. + + Args: + self: implicitly added + + Returns: + [`CollectionSubject`] of `File` objects of the direct headers. + """ + return subjects.collection( + self.actual.direct_public_headers, + meta = self.meta.derive("direct_public_headers()"), + container_name = "direct_public_headers", + element_plural_name = "public header files", + ) + +def _compilation_context_subject_system_includes(self): + """Returns the system include directories as a subject. + + NOTE: The system includes are the `cc_library.includes` attribute. + + Args: + self: implicitly added + + Returns: + [`CollectionSubject`] of [`str`] + """ + return subjects.collection( + self.actual.system_includes.to_list(), + meta = self.meta.derive("includes()"), + container_name = "includes", + element_plural_name = "include paths", + ) diff --git a/tests/default_info_subject.bzl b/tests/default_info_subject.bzl new file mode 100644 index 0000000000..7ab8546c38 --- /dev/null +++ b/tests/default_info_subject.bzl @@ -0,0 +1,47 @@ +# 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. +"""DefaultInfo testing subject.""" + +load("@rules_testing//lib:truth.bzl", "subjects") + +# TODO: Load this through truth.bzl#subjects when made available +# https://github.com/bazelbuild/rules_testing/issues/54 +load("@rules_testing//lib/private:str_subject.bzl", "StrSubject") + +# TODO: Load this through truth.bzl#subjects when made available +# https://github.com/bazelbuild/rules_testing/issues/54 +load("@rules_testing//lib/private:dict_subject.bzl", "DictSubject") + +# TODO: Load this through truth.bzl#subjects when made available +# https://github.com/bazelbuild/rules_testing/issues/54 +load("@rules_testing//lib/private:runfiles_subject.bzl", "RunfilesSubject") + +# TODO: Use rules_testing's DefaultInfoSubject once it's available +# https://github.com/bazelbuild/rules_testing/issues/52 +def _default_info_subject_new(info, *, meta): + public = struct( + runfiles = lambda *a, **k: _default_info_subject_runfiles(self, *a, **k), + ) + self = struct(actual = info, meta = meta) + return public + +def _default_info_subject_runfiles(self): + return RunfilesSubject.new( + self.actual.default_runfiles, + meta = self.meta.derive("runfiles()"), + ) + +DefaultInfoSubject = struct( + new = _default_info_subject_new, +) diff --git a/tests/py_cc_toolchain_info_subject.bzl b/tests/py_cc_toolchain_info_subject.bzl new file mode 100644 index 0000000000..1eed503d03 --- /dev/null +++ b/tests/py_cc_toolchain_info_subject.bzl @@ -0,0 +1,49 @@ +# 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. +"""PyCcToolchainInfo testing subject.""" + +load("@rules_testing//lib:truth.bzl", "subjects") + +# TODO: Load this through truth.bzl#subjects when made available +load("@rules_testing//lib/private:str_subject.bzl", "StrSubject") + +# TODO: Load this through truth.bzl#subjects when made available +load("@rules_testing//lib/private:dict_subject.bzl", "DictSubject") +load(":struct_subject.bzl", "struct_subject") + +def _py_cc_toolchain_info_subject_new(info, *, meta): + public = struct( + headers = lambda *a, **k: _py_cc_toolchain_info_subject_headers(self, *a, **k), + python_version = lambda *a, **k: _py_cc_toolchain_info_subject_python_version(self, *a, **k), + actual = info, + ) + self = struct(actual = info, meta = meta) + return public + +def _py_cc_toolchain_info_subject_headers(self): + return struct_subject( + self.actual.headers, + meta = self.meta.derive("headers()"), + providers_map = DictSubject.new, + ) + +def _py_cc_toolchain_info_subject_python_version(self): + return StrSubject.new( + self.actual.python_version, + meta = self.meta.derive("python_version()"), + ) + +PyCcToolchainInfoSubject = struct( + new = _py_cc_toolchain_info_subject_new, +) diff --git a/tests/struct_subject.bzl b/tests/struct_subject.bzl new file mode 100644 index 0000000000..09e7b75b7e --- /dev/null +++ b/tests/struct_subject.bzl @@ -0,0 +1,38 @@ +# 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. + +# TODO: Replace this with rules_testing StructSubject +# https://github.com/bazelbuild/rules_testing/issues/53 +def struct_subject(actual, *, meta, **attr_factories): + public_attrs = {} + for name, factory in attr_factories.items(): + if not hasattr(actual, name): + fail("Struct missing attribute: '{}'".format(name)) + + def attr_handler(*, __name = name, __factory = factory): + return __factory( + getattr(actual, __name), + meta = self.meta.derive(__name + "()"), + ) + + public_attrs[name] = attr_handler + public = struct( + actual = actual, + **public_attrs + ) + self = struct( + actual = actual, + meta = meta, + ) + return public