feat: expose runtime's shared libraries through toolchain (#1717)
This exposes the runtime's C libraries through the py cc toolchain. This
allows tools to embed the Python runtime or otherwise link against it.
It follows the same pattern as with the headers: the toolchain consumes
the cc_library,
exports CcInfo, and a `:current_py_cc_libs` target makes it easily
accessible to users.
Work towards https://github.com/bazelbuild/rules_python/issues/824
* Also upgrades to rules_testing 0.5.0 to make use of rules_testing's
DefaultInfoSubject
diff --git a/internal_deps.bzl b/internal_deps.bzl
index 3835cd6..9931933 100644
--- a/internal_deps.bzl
+++ b/internal_deps.bzl
@@ -57,9 +57,9 @@
http_archive(
name = "rules_testing",
- sha256 = "8df0a8eb21739ea4b0a03f5dc79e68e245a45c076cfab404b940cc205cb62162",
- strip_prefix = "rules_testing-0.4.0",
- url = "https://github.com/bazelbuild/rules_testing/releases/download/v0.4.0/rules_testing-v0.4.0.tar.gz",
+ sha256 = "b84ed8546f1969d700ead4546de9f7637e0f058d835e47e865dcbb13c4210aed",
+ strip_prefix = "rules_testing-0.5.0",
+ url = "https://github.com/bazelbuild/rules_testing/releases/download/v0.5.0/rules_testing-v0.5.0.tar.gz",
)
http_archive(
diff --git a/python/cc/BUILD.bazel b/python/cc/BUILD.bazel
index 0d90e15..d384d05 100644
--- a/python/cc/BUILD.bazel
+++ b/python/cc/BUILD.bazel
@@ -3,6 +3,7 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
load("//python/private:current_py_cc_headers.bzl", "current_py_cc_headers")
+load("//python/private:current_py_cc_libs.bzl", "current_py_cc_libs")
package(
default_visibility = ["//:__subpackages__"],
@@ -19,6 +20,17 @@
visibility = ["//visibility:public"],
)
+# This target provides the C libraries 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_libs(
+ name = "current_py_cc_libs",
+ # 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"],
diff --git a/python/private/current_py_cc_libs.bzl b/python/private/current_py_cc_libs.bzl
new file mode 100644
index 0000000..863e59a
--- /dev/null
+++ b/python/private/current_py_cc_libs.bzl
@@ -0,0 +1,41 @@
+# Copyright 2024 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_libs rule."""
+
+def _current_py_cc_libs_impl(ctx):
+ py_cc_toolchain = ctx.toolchains["//python/cc:toolchain_type"].py_cc_toolchain
+ return py_cc_toolchain.libs.providers_map.values()
+
+current_py_cc_libs = rule(
+ implementation = _current_py_cc_libs_impl,
+ toolchains = ["//python/cc:toolchain_type"],
+ provides = [CcInfo],
+ doc = """\
+Provides the currently active Python toolchain's C libraries.
+
+This is a wrapper around the underlying `cc_library()` for the
+C libraries 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 `:libpython` target:
+
+```starlark
+cc_library(
+ name = "foo",
+ deps = ["@rules_python//python/cc:current_py_cc_libs"]
+)
+```
+""",
+)
diff --git a/python/private/py_cc_toolchain_info.bzl b/python/private/py_cc_toolchain_info.bzl
index a2e62a8..a47a6a5 100644
--- a/python/private/py_cc_toolchain_info.bzl
+++ b/python/private/py_cc_toolchain_info.bzl
@@ -38,6 +38,27 @@
e.g. `:current_py_cc_headers` to act as the underlying headers target it
represents).
""",
+ "libs": """\
+(struct) Information about C libraries, 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 libraries.
+ * DefaultInfo: the DefaultInfo provider instance for the headers.
+
+ A map is used to allow additional providers from the originating libraries
+ 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 beginning 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_rule.bzl b/python/private/py_cc_toolchain_rule.bzl
index c80f845..abb3fb6 100644
--- a/python/private/py_cc_toolchain_rule.bzl
+++ b/python/private/py_cc_toolchain_rule.bzl
@@ -28,6 +28,12 @@
"DefaultInfo": ctx.attr.headers[DefaultInfo],
},
),
+ libs = struct(
+ providers_map = {
+ "CcInfo": ctx.attr.libs[CcInfo],
+ "DefaultInfo": ctx.attr.libs[DefaultInfo],
+ },
+ ),
python_version = ctx.attr.python_version,
)
return [platform_common.ToolchainInfo(
@@ -43,6 +49,12 @@
providers = [CcInfo],
mandatory = True,
),
+ "libs": attr.label(
+ doc = ("Target that provides the Python runtime libraries for linking. " +
+ "Typically this is a cc_library target of `.so` files."),
+ providers = [CcInfo],
+ mandatory = True,
+ ),
"python_version": attr.string(
doc = "The Major.minor Python version, e.g. 3.11",
mandatory = True,
diff --git a/python/repositories.bzl b/python/repositories.bzl
index e991511..1a6c0e5 100644
--- a/python/repositories.bzl
+++ b/python/repositories.bzl
@@ -353,6 +353,7 @@
py_cc_toolchain(
name = "py_cc_toolchain",
headers = ":python_headers",
+ libs = ":libpython",
python_version = "{python_version}",
)
""".format(
diff --git a/tests/cc/BUILD.bazel b/tests/cc/BUILD.bazel
index ef64d6d..889f9e0 100644
--- a/tests/cc/BUILD.bazel
+++ b/tests/cc/BUILD.bazel
@@ -21,6 +21,12 @@
exports_files(["fake_header.h"])
+filegroup(
+ name = "libpython",
+ srcs = ["libpython-fake.so"],
+ tags = PREVENT_IMPLICIT_BUILDING_TAGS,
+)
+
toolchain(
name = "fake_py_cc_toolchain",
tags = PREVENT_IMPLICIT_BUILDING_TAGS,
@@ -31,6 +37,7 @@
py_cc_toolchain(
name = "fake_py_cc_toolchain_impl",
headers = ":fake_headers",
+ libs = ":fake_libs",
python_version = "3.999",
tags = PREVENT_IMPLICIT_BUILDING_TAGS,
)
@@ -44,6 +51,14 @@
tags = PREVENT_IMPLICIT_BUILDING_TAGS,
)
+# buildifier: disable=native-cc
+cc_library(
+ name = "fake_libs",
+ srcs = ["libpython3.so"],
+ data = ["libdata.txt"],
+ tags = PREVENT_IMPLICIT_BUILDING_TAGS,
+)
+
cc_toolchain_suite(
name = "cc_toolchain_suite",
tags = ["manual"],
diff --git a/tests/cc/current_py_cc_libs/BUILD.bazel b/tests/cc/current_py_cc_libs/BUILD.bazel
new file mode 100644
index 0000000..1e108c3
--- /dev/null
+++ b/tests/cc/current_py_cc_libs/BUILD.bazel
@@ -0,0 +1,17 @@
+# Copyright 2024 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_libs_tests.bzl", "current_py_cc_libs_test_suite")
+
+current_py_cc_libs_test_suite(name = "current_py_cc_libs_tests")
diff --git a/tests/cc/current_py_cc_libs/current_py_cc_libs_tests.bzl b/tests/cc/current_py_cc_libs/current_py_cc_libs_tests.bzl
new file mode 100644
index 0000000..5699b75
--- /dev/null
+++ b/tests/cc/current_py_cc_libs/current_py_cc_libs_tests.bzl
@@ -0,0 +1,77 @@
+# Copyright 2024 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_libs."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("//tests:cc_info_subject.bzl", "cc_info_subject")
+
+_tests = []
+
+def _test_current_toolchain_libs(name):
+ analysis_test(
+ name = name,
+ impl = _test_current_toolchain_libs_impl,
+ target = "//python/cc:current_py_cc_libs",
+ config_settings = {
+ "//command_line_option:extra_toolchains": [str(Label("//tests/cc:all"))],
+ },
+ attrs = {
+ "lib": attr.label(
+ default = "//tests/cc:libpython",
+ allow_single_file = True,
+ ),
+ },
+ )
+
+def _test_current_toolchain_libs_impl(env, target):
+ # Check that the forwarded CcInfo looks vaguely correct.
+ cc_info = env.expect.that_target(target).provider(
+ CcInfo,
+ factory = cc_info_subject,
+ )
+ cc_info.linking_context().linker_inputs().has_size(2)
+
+ # Check that the forward DefaultInfo looks correct
+ env.expect.that_target(target).runfiles().contains_predicate(
+ matching.str_matches("*/libdata.txt"),
+ )
+
+ # The shared library should also end up in runfiles
+ # The `_solib` directory is a special directory CC rules put
+ # libraries into.
+ env.expect.that_target(target).runfiles().contains_predicate(
+ matching.str_matches("*_solib*/libpython3.so"),
+ )
+
+_tests.append(_test_current_toolchain_libs)
+
+def _test_toolchain_is_registered_by_default(name):
+ analysis_test(
+ name = name,
+ impl = _test_toolchain_is_registered_by_default_impl,
+ target = "//python/cc:current_py_cc_libs",
+ )
+
+def _test_toolchain_is_registered_by_default_impl(env, target):
+ env.expect.that_target(target).has_provider(CcInfo)
+
+_tests.append(_test_toolchain_is_registered_by_default)
+
+def current_py_cc_libs_test_suite(name):
+ test_suite(
+ name = name,
+ tests = _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
index 609518d..fe83bf2 100644
--- a/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl
+++ b/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl
@@ -15,7 +15,7 @@
"""Tests for py_cc_toolchain."""
load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
-load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:truth.bzl", "matching", "subjects")
load("//tests:cc_info_subject.bzl", "cc_info_subject")
load("//tests:default_info_subject.bzl", "default_info_subject")
load("//tests:py_cc_toolchain_info_subject.bzl", "PyCcToolchainInfoSubject")
@@ -74,6 +74,19 @@
matching.str_matches("*/cc/data.txt"),
)
+ libs_providers = toolchain.libs().providers_map()
+ libs_providers.keys().contains_exactly(["CcInfo", "DefaultInfo"])
+
+ cc_info = libs_providers.get("CcInfo", factory = cc_info_subject)
+
+ cc_info.linking_context().linker_inputs().has_size(2)
+
+ default_info = libs_providers.get("DefaultInfo", factory = subjects.default_info)
+ default_info.runfiles().contains("{workspace}/tests/cc/libdata.txt")
+ default_info.runfiles().contains_predicate(
+ matching.str_matches("/libpython3."),
+ )
+
_tests.append(_py_cc_toolchain_test)
def py_cc_toolchain_test_suite(name):
diff --git a/tests/cc_info_subject.bzl b/tests/cc_info_subject.bzl
index 31ac03a..e33ccb8 100644
--- a/tests/cc_info_subject.bzl
+++ b/tests/cc_info_subject.bzl
@@ -29,7 +29,9 @@
# buildifier: disable=uninitialized
public = struct(
# go/keep-sorted start
+ actual = info,
compilation_context = lambda *a, **k: _cc_info_subject_compilation_context(self, *a, **k),
+ linking_context = lambda *a, **k: _cc_info_subject_linking_context(self, *a, **k),
# go/keep-sorted end
)
self = struct(
@@ -52,6 +54,20 @@
meta = self.meta.derive("compilation_context()"),
)
+def _cc_info_subject_linking_context(self):
+ """Returns the CcInfo.linking_context as a subject.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`LinkingContextSubject`] instance.
+ """
+ return _linking_context_subject_new(
+ self.actual.linking_context,
+ meta = self.meta.derive("linking_context()"),
+ )
+
def _compilation_context_subject_new(info, *, meta):
"""Creates a CompilationContextSubject.
@@ -126,3 +142,42 @@
container_name = "includes",
element_plural_name = "include paths",
)
+
+def _linking_context_subject_new(info, meta):
+ """Creates a LinkingContextSubject.
+
+ Args:
+ info: ([`LinkingContext`]) object instance.
+ meta: rules_testing `ExpectMeta` instance.
+
+ Returns:
+ [`LinkingContextSubject`] object.
+ """
+
+ # buildifier: disable=uninitialized
+ public = struct(
+ # go/keep-sorted start
+ linker_inputs = lambda *a, **k: _linking_context_subject_linker_inputs(self, *a, **k),
+ # go/keep-sorted end
+ )
+ self = struct(
+ actual = info,
+ meta = meta,
+ )
+ return public
+
+def _linking_context_subject_linker_inputs(self):
+ """Returns the linker inputs.
+
+ Args:
+ self: implicitly added
+
+ Returns:
+ [`CollectionSubject`] of the linker inputs.
+ """
+ return subjects.collection(
+ self.actual.linker_inputs.to_list(),
+ meta = self.meta.derive("linker_inputs()"),
+ container_name = "linker_inputs",
+ element_plural_name = "linker input values",
+ )
diff --git a/tests/py_cc_toolchain_info_subject.bzl b/tests/py_cc_toolchain_info_subject.bzl
index ab9d1b8..4d3647c 100644
--- a/tests/py_cc_toolchain_info_subject.bzl
+++ b/tests/py_cc_toolchain_info_subject.bzl
@@ -19,6 +19,7 @@
# buildifier: disable=uninitialized
public = struct(
headers = lambda *a, **k: _py_cc_toolchain_info_subject_headers(self, *a, **k),
+ libs = lambda *a, **k: _py_cc_toolchain_info_subject_libs(self, *a, **k),
python_version = lambda *a, **k: _py_cc_toolchain_info_subject_python_version(self, *a, **k),
actual = info,
)
@@ -34,6 +35,15 @@
),
)
+def _py_cc_toolchain_info_subject_libs(self):
+ return subjects.struct(
+ self.actual.libs,
+ meta = self.meta.derive("libs()"),
+ attrs = dict(
+ providers_map = subjects.dict,
+ ),
+ )
+
def _py_cc_toolchain_info_subject_python_version(self):
return subjects.str(
self.actual.python_version,