Move dynamic library symlink actions to starlark.

Should we implement https://github.com/bazelbuild/bazel/issues/26529 and just make sha256 available in starlark?

PiperOrigin-RevId: 949247328
Change-Id: I005b0408ffdea7acb59bf75c66d9443940422d1f
diff --git a/cc/common/cc_helper_internal.bzl b/cc/common/cc_helper_internal.bzl
index 6ced37f..6d0cb95 100644
--- a/cc/common/cc_helper_internal.bzl
+++ b/cc/common/cc_helper_internal.bzl
@@ -287,6 +287,7 @@
     ALWAYSLINK_LIBRARY = _ALWAYSLINK_LIBRARY,
     ALWAYSLINK_PIC_LIBRARY = _ALWAYSLINK_PIC_LIBRARY,
     SHARED_LIBRARY = _SHARED_LIBRARY,
+    INTERFACE_SHARED_LIBRARY = _INTERFACE_SHARED_LIBRARY,
     OBJECT_FILE = _OBJECT_FILE,
     PIC_OBJECT_FILE = _PIC_OBJECT_FILE,
     CC_AND_OBJC = _CC_AND_OBJC,
diff --git a/cc/private/cc_common.bzl b/cc/private/cc_common.bzl
index 466f311..d78312c 100644
--- a/cc/private/cc_common.bzl
+++ b/cc/private/cc_common.bzl
@@ -32,6 +32,7 @@
 load("//cc/private/link:create_linker_input.bzl", "create_linker_input")
 load("//cc/private/link:create_linking_context_from_compilation_outputs.bzl", "create_linking_context_from_compilation_outputs")
 load("//cc/private/link:create_linkstamp.bzl", "create_linkstamp")
+load("//cc/private/link:dynamic_library_symlink.bzl", "solib_symlink_action")
 load("//cc/private/link:link.bzl", "link")
 load("//cc/private/link:link_build_variables.bzl", "create_link_variables")
 load("//cc/private/link:lto_backends.bzl", "create_lto_backend_artifacts", "setup_common_lto_variables")
@@ -717,12 +718,7 @@
 
 def _solib_symlink_action(*, ctx, artifact, solib_directory, runtime_solib_dir_base):
     _cc_internal.check_private_api(allowlist = _PRIVATE_STARLARKIFICATION_ALLOWLIST)
-    return _cc_internal.solib_symlink_action(
-        ctx = ctx,
-        artifact = artifact,
-        solib_directory = solib_directory,
-        runtime_solib_dir_base = runtime_solib_dir_base,
-    )
+    return solib_symlink_action(ctx, artifact, solib_directory, runtime_solib_dir_base)
 
 def _cc_toolchain_variables(*, vars):
     _cc_internal.check_private_api(allowlist = _PRIVATE_STARLARKIFICATION_ALLOWLIST)
diff --git a/cc/private/link/cc_linking_helper.bzl b/cc/private/link/cc_linking_helper.bzl
index b668e00..1af4169 100644
--- a/cc/private/link/cc_linking_helper.bzl
+++ b/cc/private/link/cc_linking_helper.bzl
@@ -28,6 +28,11 @@
 load("//cc/private/compile:cc_compilation_outputs.bzl", "EMPTY_COMPILATION_OUTPUTS")
 load("//cc/private/link:cpp_link_action.bzl", "link_action")
 load("//cc/private/link:create_library_to_link.bzl", "make_library_to_link")
+load(
+    "//cc/private/link:dynamic_library_symlink.bzl",
+    "dynamic_library_soname",
+    "dynamic_library_symlink",
+)
 load("//cc/private/link:lto_indexing_action.bzl", "create_lto_artifacts_and_lto_indexing_action")
 load("//cc/private/link:target_types.bzl", "LINKING_MODE", "LINK_TARGET_TYPE", "USE_ARCHIVER", "is_dynamic_library")
 
@@ -289,7 +294,7 @@
 
         # TODO(b/28946988): Remove this hard-coded flag.
         if not feature_configuration.is_enabled("targets_windows") and not feature_configuration.is_enabled("set_soname"):
-            link_action_kwargs["linkopts"].append("-Wl,-soname=" + _cc_internal.dynamic_library_soname(
+            link_action_kwargs["linkopts"].append("-Wl,-soname=" + dynamic_library_soname(
                 actions,
                 # Must match https://github.com/bazelbuild/bazel/blob/795af54db5c348af5ca8b2961a982b399206ea20/src/main/java/com/google/devtools/build/lib/rules/cpp/SolibSymlinkAction.java#L169.
                 root_relative_path(linker_output),
@@ -335,7 +340,7 @@
     impl_library_link_artifact = None
     if dynamic_link_type == LINK_TARGET_TYPE.DYNAMIC_LIBRARY and not neverlink and \
        not feature_configuration.is_enabled("copy_dynamic_libraries_to_binary"):
-        impl_library_link_artifact = _cc_internal.dynamic_library_symlink(
+        impl_library_link_artifact = dynamic_library_symlink(
             actions,
             linker_output,
             cc_toolchain._solib_dir,
diff --git a/cc/private/link/cpp_link_action.bzl b/cc/private/link/cpp_link_action.bzl
index f60376c..8842e78 100644
--- a/cc/private/link/cpp_link_action.bzl
+++ b/cc/private/link/cpp_link_action.bzl
@@ -15,7 +15,7 @@
 
 load("@bazel_skylib//lib:paths.bzl", "paths")
 load("//cc/common:cc_helper_internal.bzl", "root_relative_path", artifact_category = "artifact_category_names")
-load("//cc/private:cc_internal.bzl", _cc_internal = "cc_internal")
+load("//cc/private/link:dynamic_library_symlink.bzl", "dynamic_library_soname")
 load("//cc/private/link:finalize_link_action.bzl", "finalize_link_action")
 load("//cc/private/link:link_build_variables.bzl", "setup_linking_variables")
 load("//cc/private/link:lto_backends.bzl", "create_shared_non_lto_artifacts")
@@ -190,7 +190,7 @@
         cc_toolchain,
         feature_configuration,
         output,
-        _cc_internal.dynamic_library_soname(
+        dynamic_library_soname(
             actions,
             # Must match https://github.com/bazelbuild/bazel/blob/795af54db5c348af5ca8b2961a982b399206ea20/src/main/java/com/google/devtools/build/lib/rules/cpp/SolibSymlinkAction.java#L169.
             root_relative_path(output),
diff --git a/cc/private/link/create_library_to_link.bzl b/cc/private/link/create_library_to_link.bzl
index 79e3566..3f61fb6 100644
--- a/cc/private/link/create_library_to_link.bzl
+++ b/cc/private/link/create_library_to_link.bzl
@@ -19,6 +19,7 @@
 load("//cc/common:cc_helper_internal.bzl", "is_versioned_shared_library", "path_contains_up_level_references")
 load("//cc/private:cc_internal.bzl", _cc_internal = "cc_internal")
 load("//cc/private/compile:lto_compilation_context.bzl", _EMPTY_LTO = "EMPTY_LTO_COMPILATION_CONTEXT")
+load("//cc/private/link:dynamic_library_symlink.bzl", "dynamic_library_symlink", "dynamic_library_symlink2")
 load("//cc/private/link:lto_backends.bzl", "create_shared_non_lto_artifacts")
 
 _warning = """ Don't use this field. It's intended for internal use and will be changed or removed
@@ -211,9 +212,9 @@
             if dynamic_library_symlink_path:
                 if dynamic_library.short_path.startswith("_solib_"):
                     fail("dynamic_library must not be a symbolic link in the solib directory. Got '%s'" % dynamic_library.short_path)
-                dynamic_library = _cc_internal.dynamic_library_symlink2(actions, dynamic_library, cc_toolchain._solib_dir, dynamic_library_symlink_path)
+                dynamic_library = dynamic_library_symlink2(actions, dynamic_library, cc_toolchain._solib_dir, dynamic_library_symlink_path)
             else:
-                dynamic_library = _cc_internal.dynamic_library_symlink(actions, dynamic_library, cc_toolchain._solib_dir, True, True)
+                dynamic_library = dynamic_library_symlink(actions, dynamic_library, cc_toolchain._solib_dir, True, True)
 
     resolved_symlink_interface_library = None
     if interface_library:
@@ -226,9 +227,9 @@
             if interface_library_symlink_path:
                 if interface_library.short_path.startswith("_solib_"):
                     fail("dynamic_library must not be a symbolic link in the solib directory. Got '%s'" % dynamic_library.short_path)
-                interface_library = _cc_internal.dynamic_library_symlink2(actions, interface_library, cc_toolchain._solib_dir, interface_library_symlink_path)
+                interface_library = dynamic_library_symlink2(actions, interface_library, cc_toolchain._solib_dir, interface_library_symlink_path)
             else:
-                interface_library = _cc_internal.dynamic_library_symlink(actions, interface_library, cc_toolchain._solib_dir, True, True)
+                interface_library = dynamic_library_symlink(actions, interface_library, cc_toolchain._solib_dir, True, True)
 
     identifier = static_library or pic_static_library or dynamic_library or interface_library
     if not identifier:
diff --git a/cc/private/link/dynamic_library_symlink.bzl b/cc/private/link/dynamic_library_symlink.bzl
new file mode 100644
index 0000000..22882a8
--- /dev/null
+++ b/cc/private/link/dynamic_library_symlink.bzl
@@ -0,0 +1,192 @@
+# Copyright 2026 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.
+"""Helper functions for creating solib and dynamic library symbolic links."""
+
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("//cc/common:cc_helper_internal.bzl", "extensions", "is_versioned_shared_library_extension_valid", "root_relative_path")
+load("//cc/private:cc_internal.bzl", _cc_internal = "cc_internal")
+
+# TODO(bgorshenev): Remove this variable once we're sure these functions won't
+# won't interfere with path mapping.
+USE_STARLARK_SOLIB_SYMLINK = True
+
+def _escaped_path(path):
+    return path.replace("_", "_U").replace("/", "_S").replace("\\", "_B").replace(":", "_C").replace("@", "_A")
+
+def _label_to_string(label):
+    """Java and Starlark convert label to string slightly differently. Match Java's behavior."""
+    s = str(label)
+    if s.startswith("@@//"):  # buildifier: disable=canonical-repository
+        return s[2:]
+    if s.startswith("@//"):
+        return s[1:]
+    return s
+
+def _is_shared_library_filetype(basename):
+    if "." not in basename:
+        return True
+    if is_versioned_shared_library_extension_valid(basename):
+        return True
+    ext = basename[basename.rfind("."):]
+    if ext in extensions.SHARED_LIBRARY:
+        return True
+    if ext in extensions.INTERFACE_SHARED_LIBRARY:
+        return True
+    return False
+
+def _dynamic_library_soname(library_path, preserve_name, mnemonic):
+    if preserve_name:
+        return paths.basename(library_path)
+    else:
+        mnemonic_mangling = ""
+        if "ST-" in mnemonic:
+            st_index = mnemonic.find("ST-")
+            mnemonic_mangling = mnemonic[st_index:] + "_"
+        return "lib" + mnemonic_mangling + _escaped_path(library_path)
+
+def dynamic_library_soname(actions, library_path, preserve_name):
+    """Compute the SONAME to use for a dynamic library.
+
+    This name is basically the name of the shared
+    library in its final symlinked location.
+
+    Args:
+        actions: action construction context of rule requesting symlink
+        library_path: name of the shared library that needs to be mangled
+        preserve_name: whether to preserve the name of the library
+
+    Returns:
+        soname to embed in the dynamic library
+    """
+    if USE_STARLARK_SOLIB_SYMLINK and hasattr(_cc_internal, "maybe_hash_preserve_extension"):
+        ctx = _cc_internal.actions2ctx_cheat(actions)
+        mnemonic = ctx.bin_dir.path.split("/")[1]
+        return _dynamic_library_soname(library_path, preserve_name, mnemonic)
+    else:
+        return _cc_internal.dynamic_library_soname(actions, library_path, preserve_name)
+
+def _get_mangled_name(label, solib_dir, mnemonic, library_path, preserve_name, prefix_consumer):
+    escaped_rule_path = _escaped_path("_" + _label_to_string(label))
+    soname = _dynamic_library_soname(library_path, preserve_name, mnemonic)
+
+    if preserve_name:
+        parent_dir = paths.dirname(library_path)
+        escaped_library_path = _escaped_path("_" + parent_dir)
+        escaped_full_path = escaped_rule_path + "__" + escaped_library_path if prefix_consumer else escaped_library_path
+        mangled_dir = solib_dir + "/" + _cc_internal.maybe_hash_preserve_extension(escaped_full_path)
+        return mangled_dir + "/" + soname
+    else:
+        filename = escaped_rule_path + "__" + soname if prefix_consumer else soname
+        return solib_dir + "/" + _cc_internal.maybe_hash_preserve_extension(filename)
+
+def dynamic_library_symlink(actions, library, solib_directory, preserve_name, prefix_consumer):
+    """ Create dynamic library symlink.
+
+    Replaces shared library artifact with mangled symlink and creates related symlink action. For
+    artifacts that should retain filename (e.g. libraries with SONAME tag), link is created to the
+    parent directory instead.
+
+    This action is performed to minimize number of -rpath entries used during linking process
+    (by essentially "collecting" as many shared libraries as possible in the single directory),
+    since we will be paying quadratic price for each additional entry on the -rpath.
+
+    Args:
+        actions: action construction context of rule requesting symlink
+        library: Shared library artifact that needs to be mangled.
+        solib_directory: String giving the solib directory
+        preserve_name: whether to preserve the name of the library
+        prefix_consumer: whether to prefix the output artifact name with the label of the consumer
+    Returns:
+        mangled symlink artifact.
+   """
+    if USE_STARLARK_SOLIB_SYMLINK and hasattr(_cc_internal, "maybe_hash_preserve_extension"):
+        if not _is_shared_library_filetype(library.basename):
+            fail("Library '%s' does not match expected filetype" % library.basename)
+        if root_relative_path(library).startswith("_solib_"):
+            fail("Library '%s' is already in _solib_" % library.path)
+        ctx = _cc_internal.actions2ctx_cheat(actions)
+        label = ctx.label
+        mnemonic = ctx.bin_dir.path.split("/")[1]
+        mangled_name = _get_mangled_name(
+            label,
+            solib_directory,
+            mnemonic,
+            root_relative_path(library),
+            preserve_name,
+            prefix_consumer,
+        )
+        symlink = ctx.actions.declare_shareable_artifact(mangled_name)
+        ctx.actions.symlink(
+            output = symlink,
+            target_file = library,
+        )
+        return symlink
+    else:
+        return _cc_internal.dynamic_library_symlink(actions, library, solib_directory, preserve_name, prefix_consumer)
+
+def dynamic_library_symlink2(actions, library, solib_directory, path):
+    """Creates a symlink for a dynamic library with a rule-specified path.
+
+    Args:
+        actions: action construction context of rule requesting symlink
+        library: Shared library artifact
+        solib_directory: String giving the solib directory
+        path: Symlink path underneath the solib directory.
+    Returns:
+        symlink artifact.
+    """
+    if USE_STARLARK_SOLIB_SYMLINK and hasattr(_cc_internal, "maybe_hash_preserve_extension"):
+        if not _is_shared_library_filetype(library.basename):
+            fail("Library '%s' does not match expected filetype" % library.basename)
+        if not _is_shared_library_filetype(path.split("/")[-1]):
+            fail("Path '%s' does not match expected filetype" % path)
+        if root_relative_path(library).startswith("_solib_"):
+            fail("Library '%s' is already in _solib_" % library.path)
+        ctx = _cc_internal.actions2ctx_cheat(actions)
+        symlink_name = solib_directory + "/" + path
+        symlink = ctx.actions.declare_shareable_artifact(symlink_name)
+        ctx.actions.symlink(
+            output = symlink,
+            target_file = library,
+        )
+        return symlink
+    else:
+        return _cc_internal.dynamic_library_symlink2(actions, library, solib_directory, path)
+
+def solib_symlink_action(ctx, artifact, solib_directory, runtime_solib_dir_base):
+    """Create a symlink for C++ runtime libraries without name or directory mangling.
+
+    Args:
+        ctx: rule context of rule requesting symlink
+        artifact: Shared library artifact
+        solib_directory: String giving the solib directory, as defined by the toolchain.
+        runtime_solib_dir_base: Base directory for runtime symlinks, if the toolchain's needs to be overridden.
+    Returns:
+        symlink artifact.
+    """
+    if USE_STARLARK_SOLIB_SYMLINK:
+        if not _is_shared_library_filetype(artifact.basename):
+            fail("Library '%s' does not match expected filetype" % artifact.basename)
+        if root_relative_path(artifact).startswith("_solib_"):
+            fail("Library '%s' is already in _solib_" % artifact.path)
+        solib_dir = runtime_solib_dir_base if runtime_solib_dir_base != None else solib_directory
+        symlink_name = solib_dir + "/" + artifact.basename
+        symlink = ctx.actions.declare_shareable_artifact(symlink_name)
+        ctx.actions.symlink(
+            output = symlink,
+            target_file = artifact,
+        )
+        return symlink
+    else:
+        return _cc_internal.solib_symlink_action(ctx, artifact, solib_directory, runtime_solib_dir_base)