chore: support removal of builtin providers (#2274)

The builtin providers PyInfo, PyRuntimeInfo, and PyCcLinkParamsProvider
are being removed,
which means Bazel throws an error while compiling bzl files if there is
a reference to a
top-level symbol that doesn't exist anymore. For backwards
compatibility, rules_python
consumes/produces these providers, so the symbols are used in various
places.

To fix, use `native.legacy_globals` and Bazel version detection to
conditionally emit
the symbols into `@rules_python_internal`. If they aren't present, they
are reported
as None.

This mimics equivalent functionality in bazel_features; bazel_features
isn't used because
it would require users to update their WORKSPACE to initialize some
dependencies before
rules_python can perform its initialization.

Removal of the builtin symbols is controlled by
`--incompatible_autoload_externally`
(which is in Bazel 8 and has been cherry-picked into earlier version).
If the flag is
enabled with "@rules_python" or "-@rules_python" the providers are
removed from Bazel.

---------

Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/python/config_settings/transition.bzl b/python/config_settings/transition.bzl
index 7ac41f8..a7646dc 100644
--- a/python/config_settings/transition.bzl
+++ b/python/config_settings/transition.bzl
@@ -101,12 +101,12 @@
     ]
     if PyInfo in target:
         providers.append(target[PyInfo])
-    if BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
+    if BuiltinPyInfo != None and BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
         providers.append(target[BuiltinPyInfo])
 
     if PyRuntimeInfo in target:
         providers.append(target[PyRuntimeInfo])
-    if BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
+    if BuiltinPyRuntimeInfo != None and BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
         providers.append(target[BuiltinPyRuntimeInfo])
     return providers
 
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index cb0fd5c..b4084fb 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -344,7 +344,10 @@
     visibility = [
         "//:__subpackages__",
     ],
-    deps = [":bazel_tools_bzl"],
+    deps = [
+        ":bazel_tools_bzl",
+        "@rules_python_internal//:rules_python_config_bzl",
+    ],
 )
 
 bzl_library(
diff --git a/python/private/common/attributes.bzl b/python/private/common/attributes.bzl
index 5e81f46..0299e85 100644
--- a/python/private/common/attributes.bzl
+++ b/python/private/common/attributes.bzl
@@ -253,6 +253,8 @@
     allow_none = True,
 )
 
+_MaybeBuiltinPyInfo = [[BuiltinPyInfo]] if BuiltinPyInfo != None else []
+
 # Attributes common to rules accepting Python sources and deps.
 PY_SRCS_ATTRS = union_attrs(
     {
@@ -260,8 +262,7 @@
             providers = [
                 [PyInfo],
                 [CcInfo],
-                [BuiltinPyInfo],
-            ],
+            ] + _MaybeBuiltinPyInfo,
             # TODO(b/228692666): Google-specific; remove these allowances once
             # the depot is cleaned up.
             allow_rules = DEPS_ATTR_ALLOW_RULES,
diff --git a/python/private/common/common.bzl b/python/private/common/common.bzl
index e4cc254..99a6324 100644
--- a/python/private/common/common.bzl
+++ b/python/private/common/common.bzl
@@ -280,7 +280,7 @@
         dep[BuiltinPyInfo].imports
         for dep in ctx.attr.deps
         if BuiltinPyInfo in dep
-    ])
+    ] if BuiltinPyInfo != None else [])
 
 def collect_runfiles(ctx, files = depset()):
     """Collects the necessary files from the rule's context.
@@ -374,7 +374,7 @@
 
     for target in ctx.attr.deps:
         # PyInfo may not be present e.g. cc_library rules.
-        if PyInfo in target or BuiltinPyInfo in target:
+        if PyInfo in target or (BuiltinPyInfo != None and BuiltinPyInfo in target):
             py_info.merge(_get_py_info(target))
         else:
             # TODO(b/228692666): Remove this once non-PyInfo targets are no
@@ -395,7 +395,7 @@
         for target in ctx.attr.data:
             # TODO(b/234730058): Remove checking for PyInfo in data once depot
             # cleaned up.
-            if PyInfo in target or BuiltinPyInfo in target:
+            if PyInfo in target or (BuiltinPyInfo != None and BuiltinPyInfo in target):
                 info = _get_py_info(target)
                 py_info.merge_uses_shared_libraries(info.uses_shared_libraries)
             else:
@@ -410,7 +410,7 @@
     return py_info.build(), deps_transitive_sources, py_info.build_builtin_py_info()
 
 def _get_py_info(target):
-    return target[PyInfo] if PyInfo in target else target[BuiltinPyInfo]
+    return target[PyInfo] if PyInfo in target or BuiltinPyInfo == None else target[BuiltinPyInfo]
 
 def create_instrumented_files_info(ctx):
     return _coverage_common.instrumented_files_info(
diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl
index 1d14344..cfd9961 100644
--- a/python/private/common/py_executable.bzl
+++ b/python/private/common/py_executable.bzl
@@ -855,7 +855,7 @@
         # builtin py_runtime rule or defined their own. We can't directly detect
         # the type of the provider object, but the rules_python PyRuntimeInfo
         # object has an extra attribute that the builtin one doesn't.
-        if hasattr(py_runtime_info, "interpreter_version_info"):
+        if hasattr(py_runtime_info, "interpreter_version_info") and BuiltinPyRuntimeInfo != None:
             providers.append(BuiltinPyRuntimeInfo(
                 interpreter_path = py_runtime_info.interpreter_path,
                 interpreter = py_runtime_info.interpreter,
@@ -890,7 +890,8 @@
         )
 
     providers.append(py_info)
-    providers.append(builtin_py_info)
+    if builtin_py_info:
+        providers.append(builtin_py_info)
     providers.append(create_output_group_info(py_info.transitive_sources, output_groups))
 
     extra_providers = semantics.get_extra_providers(
diff --git a/python/private/common/py_library.bzl b/python/private/common/py_library.bzl
index 4423986..078626e 100644
--- a/python/private/common/py_library.bzl
+++ b/python/private/common/py_library.bzl
@@ -98,14 +98,16 @@
             dependency_transitive_python_sources = deps_transitive_sources,
         )
 
-    return [
+    providers = [
         DefaultInfo(files = default_outputs, runfiles = runfiles),
         py_info,
-        builtins_py_info,
         create_instrumented_files_info(ctx),
         PyCcLinkParamsProvider(cc_info = cc_info),
         create_output_group_info(py_info.transitive_sources, extra_groups = {}),
     ]
+    if builtins_py_info:
+        providers.append(builtins_py_info)
+    return providers
 
 _DEFAULT_PY_LIBRARY_DOC = """
 A library of Python code that can be depended upon.
diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl
index d944796..088b6ea 100644
--- a/python/private/common/py_runtime_rule.bzl
+++ b/python/private/common/py_runtime_rule.bzl
@@ -125,18 +125,20 @@
     if not IS_BAZEL_7_OR_HIGHER:
         builtin_py_runtime_info_kwargs.pop("bootstrap_template")
 
-    return [
+    providers = [
         PyRuntimeInfo(**py_runtime_info_kwargs),
-        # Return the builtin provider for better compatibility.
-        # 1. There is a legacy code path in py_binary that
-        #    checks for the provider when toolchains aren't used
-        # 2. It makes it easier to transition from builtins to rules_python
-        BuiltinPyRuntimeInfo(**builtin_py_runtime_info_kwargs),
         DefaultInfo(
             files = runtime_files,
             runfiles = runfiles,
         ),
     ]
+    if BuiltinPyRuntimeInfo != None and BuiltinPyRuntimeInfo != PyRuntimeInfo:
+        # Return the builtin provider for better compatibility.
+        # 1. There is a legacy code path in py_binary that
+        #    checks for the provider when toolchains aren't used
+        # 2. It makes it easier to transition from builtins to rules_python
+        providers.append(BuiltinPyRuntimeInfo(**builtin_py_runtime_info_kwargs))
+    return providers
 
 # Bind to the name "py_runtime" to preserve the kind/rule_class it shows up
 # as elsewhere.
diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl
index c37bc35..e2fa8f6 100644
--- a/python/private/internal_config_repo.bzl
+++ b/python/private/internal_config_repo.bzl
@@ -24,6 +24,9 @@
 _CONFIG_TEMPLATE = """\
 config = struct(
   enable_pystar = {enable_pystar},
+  BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}),
+  BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}),
+  BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}),
 )
 """
 
@@ -65,8 +68,20 @@
     else:
         enable_pystar = False
 
+    if native.bazel_version.startswith("8."):
+        builtin_py_info_symbol = "None"
+        builtin_py_runtime_info_symbol = "None"
+        builtin_py_cc_link_params_provider = "None"
+    else:
+        builtin_py_info_symbol = "PyInfo"
+        builtin_py_runtime_info_symbol = "PyRuntimeInfo"
+        builtin_py_cc_link_params_provider = "PyCcLinkParamsProvider"
+
     rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format(
         enable_pystar = enable_pystar,
+        builtin_py_info_symbol = builtin_py_info_symbol,
+        builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol,
+        builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider,
     ))
 
     if enable_pystar:
diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl
index 97cd50b..ce56e23 100644
--- a/python/private/py_info.bzl
+++ b/python/private/py_info.bzl
@@ -118,7 +118,7 @@
 )
 
 # The "effective" PyInfo is what the canonical //python:py_info.bzl%PyInfo symbol refers to
-_EffectivePyInfo = PyInfo if config.enable_pystar else BuiltinPyInfo
+_EffectivePyInfo = PyInfo if (config.enable_pystar or BuiltinPyInfo == None) else BuiltinPyInfo
 
 def PyInfoBuilder():
     # buildifier: disable=uninitialized
@@ -206,7 +206,7 @@
 def _PyInfoBuilder_merge_target(self, target):
     if PyInfo in target:
         self.merge(target[PyInfo])
-    elif BuiltinPyInfo in target:
+    elif BuiltinPyInfo != None and BuiltinPyInfo in target:
         self.merge(target[BuiltinPyInfo])
     return self
 
@@ -234,6 +234,9 @@
     )
 
 def _PyInfoBuilder_build_builtin_py_info(self):
+    if BuiltinPyInfo == None:
+        return None
+
     return BuiltinPyInfo(
         has_py2_only_sources = self._has_py2_only_sources[0],
         has_py3_only_sources = self._has_py3_only_sources[0],
diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl
index eb91413..39f15bf 100644
--- a/python/private/py_runtime_pair_rule.bzl
+++ b/python/private/py_runtime_pair_rule.bzl
@@ -56,7 +56,7 @@
     # py_binary (implemented in Java) performs a type check on the provider
     # value to verify it is an instance of the Java-implemented PyRuntimeInfo
     # class.
-    if IS_BAZEL_7_OR_HIGHER and PyRuntimeInfo in target:
+    if (IS_BAZEL_7_OR_HIGHER and PyRuntimeInfo in target) or BuiltinPyRuntimeInfo == None:
         return target[PyRuntimeInfo]
     else:
         return target[BuiltinPyRuntimeInfo]
@@ -70,13 +70,15 @@
         return False
     return ctx.fragments.py.disable_py2
 
+_MaybeBuiltinPyRuntimeInfo = [[BuiltinPyRuntimeInfo]] if BuiltinPyRuntimeInfo != None else []
+
 py_runtime_pair = rule(
     implementation = _py_runtime_pair_impl,
     attrs = {
         # The two runtimes are used by the py_binary at runtime, and so need to
         # be built for the target platform.
         "py2_runtime": attr.label(
-            providers = [[PyRuntimeInfo], [BuiltinPyRuntimeInfo]],
+            providers = [[PyRuntimeInfo]] + _MaybeBuiltinPyRuntimeInfo,
             cfg = "target",
             doc = """\
 The runtime to use for Python 2 targets. Must have `python_version` set to
@@ -84,7 +86,7 @@
 """,
         ),
         "py3_runtime": attr.label(
-            providers = [[PyRuntimeInfo], [BuiltinPyRuntimeInfo]],
+            providers = [[PyRuntimeInfo]] + _MaybeBuiltinPyRuntimeInfo,
             cfg = "target",
             doc = """\
 The runtime to use for Python 3 targets. Must have `python_version` set to
diff --git a/python/private/reexports.bzl b/python/private/reexports.bzl
index ea39ac9..e9d2ded 100644
--- a/python/private/reexports.bzl
+++ b/python/private/reexports.bzl
@@ -30,11 +30,12 @@
 different name. Then we can load it from elsewhere.
 """
 
-# Don't use underscore prefix, since that would make the symbol local to this
-# file only. Use a non-conventional name to emphasize that this is not a public
-# symbol.
-# buildifier: disable=name-conventions
-BuiltinPyInfo = PyInfo
+load("@rules_python_internal//:rules_python_config.bzl", "config")
 
+# NOTE: May be None (Bazel 8 autoloading rules_python)
 # buildifier: disable=name-conventions
-BuiltinPyRuntimeInfo = PyRuntimeInfo
+BuiltinPyInfo = config.BuiltinPyInfo
+
+# NOTE: May be None (Bazel 8 autoloading rules_python)
+# buildifier: disable=name-conventions
+BuiltinPyRuntimeInfo = config.BuiltinPyRuntimeInfo
diff --git a/python/py_cc_link_params_info.bzl b/python/py_cc_link_params_info.bzl
index 42d8daf..b0ad0a7 100644
--- a/python/py_cc_link_params_info.bzl
+++ b/python/py_cc_link_params_info.bzl
@@ -3,4 +3,8 @@
 load("@rules_python_internal//:rules_python_config.bzl", "config")
 load("//python/private/common:providers.bzl", _starlark_PyCcLinkParamsProvider = "PyCcLinkParamsProvider")
 
-PyCcLinkParamsInfo = _starlark_PyCcLinkParamsProvider if config.enable_pystar else PyCcLinkParamsProvider
+PyCcLinkParamsInfo = (
+    _starlark_PyCcLinkParamsProvider if (
+        config.enable_pystar or config.BuiltinPyCcLinkParamsProvider == None
+    ) else config.BuiltinPyCcLinkParamsProvider
+)
diff --git a/python/py_info.bzl b/python/py_info.bzl
index 52a66a8..5697f58 100644
--- a/python/py_info.bzl
+++ b/python/py_info.bzl
@@ -18,4 +18,4 @@
 load("//python/private:py_info.bzl", _starlark_PyInfo = "PyInfo")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")
 
-PyInfo = _starlark_PyInfo if config.enable_pystar else BuiltinPyInfo
+PyInfo = _starlark_PyInfo if config.enable_pystar or BuiltinPyInfo == None else BuiltinPyInfo
diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl
index 873349f..3cc6dfb 100644
--- a/tests/base_rules/py_executable_base_tests.bzl
+++ b/tests/base_rules/py_executable_base_tests.bzl
@@ -19,14 +19,13 @@
 load("@rules_testing//lib:truth.bzl", "matching")
 load("@rules_testing//lib:util.bzl", rt_util = "util")
 load("//python:py_executable_info.bzl", "PyExecutableInfo")
+load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")  # buildifier: disable=bzl-visibility
 load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")  # buildifier: disable=bzl-visibility
 load("//tests/base_rules:base_tests.bzl", "create_base_tests")
 load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util")
 load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject")
 load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64")
 
-_BuiltinPyRuntimeInfo = PyRuntimeInfo
-
 _tests = []
 
 def _test_basic_windows(name, config):
@@ -359,9 +358,10 @@
     # Make sure that the rules_python loaded symbol is provided.
     env.expect.that_target(target).has_provider(RulesPythonPyRuntimeInfo)
 
-    # For compatibility during the transition, the builtin PyRuntimeInfo should
-    # also be provided.
-    env.expect.that_target(target).has_provider(_BuiltinPyRuntimeInfo)
+    if BuiltinPyRuntimeInfo != None:
+        # For compatibility during the transition, the builtin PyRuntimeInfo should
+        # also be provided.
+        env.expect.that_target(target).has_provider(BuiltinPyRuntimeInfo)
 
 _tests.append(_test_py_runtime_info_provided)
 
diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl
index 97c8e26..0f46d12 100644
--- a/tests/base_rules/py_info/py_info_tests.bzl
+++ b/tests/base_rules/py_info/py_info_tests.bzl
@@ -191,7 +191,8 @@
             ])
 
     check(builder.build())
-    check(builder.build_builtin_py_info())
+    if BuiltinPyInfo != None:
+        check(builder.build_builtin_py_info())
 
     builder.set_has_py2_only_sources(False)
     builder.set_has_py3_only_sources(False)