blob: a7edc0fd400af8d13c9da4afe944cadc2372edac [file] [edit]
# Copyright 2025 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.
"""Goes over LegacyLinkerInputs and produces LibraryToLinkValue-s."""
load("@bazel_skylib//lib:paths.bzl", "paths")
load("//cc/common:cc_helper_internal.bzl", "is_shared_library")
load("//cc/private/link:target_types.bzl", "LINKING_MODE", "LINK_TARGET_TYPE", "is_dynamic_library")
# TODO(b/338618120): Refine the signature of collect_solib_dirs. Large objects are passed in
# just to determine a single property, for example link_type and linking_mode are passed in, just to
# determine need_toolchain_libraries_rpath. Refining the signature will increase readability.
def collect_solib_dirs(
libraries,
cc_toolchain,
feature_configuration,
prefer_static_libs,
output,
dynamic_library_solib_symlink_output,
link_type,
linking_mode,
is_native_deps,
solib_dir,
toolchain_libraries_solib_dir,
workspace_name):
"""Goes over LegacyLinkerInputs and produces LibraryToLinkValue-s and rpaths.
LibraryToLinkValues are consumed by link_build_variables.bzl.
When linking a shared library fully or mostly static then we need to link in *all* dependent
files, not just what the shared library needs for its own code. This is done by wrapping all
objects/libraries with -Wl,-whole-archive and -Wl,-no-whole-archive. For this case the
globalNeedWholeArchive parameter must be set to true. Otherwise only library objects (.lo) need
to be wrapped with -Wl,-whole-archive and -Wl,-no-whole-archive.
Args:
libraries: (list[LibraryToLink]) Libraries to link in.
cc_toolchain: cc_toolchain providing some extra information in the conversion.
feature_configuration: Feature configuration to be queried.
prefer_static_libs: (bool) Prefer static libraries.
Used to select dynamic libraries from the whole set.
output: (File) The linker's output.
dynamic_library_solib_symlink_output: (None|File) Symlink to the dynamic library being created.
link_type: (LINK_TARGET_TYPE) The type of ELF file to be created (.a, .so, .lo, executable).
linking_mode: ("static"|"dynamic") Linking mode.
is_native_deps: (bool) Is this link action is used for a native dependency library.
solib_dir: (str) solib directory.
toolchain_libraries_solib_dir: (str) Directory where toolchain stores language-runtime libraries (libstdc++, libc++ ...).
workspace_name: (str) Workspace name. To support legacy code.
Returns:
({libraries_to_link: list[LibraryToLinkValue],
expanded_linker_inputs: list[File],
library_search_directories: depset[str],
all_runtime_library_search_directories: depset[str]})
- Returned libraries_to_link are passed in this form to link_build_variables.
- expanded_linker_inputs are all the files that will be consumed by the linker (if
start-end library is used, then it contains object files, otherwise an archive)
- library_search_directories are absolute directories of all dynamic libraries
- all_runtime_library_search_directories are directories of all dynamic libraries
Both depsets of directories are exposed to the link_build_variables.
"""
need_toolchain_libraries_rpath = (
toolchain_libraries_solib_dir and
(is_dynamic_library(link_type) or
(link_type == LINK_TARGET_TYPE.EXECUTABLE and linking_mode == LINKING_MODE.DYNAMIC))
)
# Collect LibrariesToLink
library_search_directories = []
rpath_roots_for_explicit_so_deps = {}
# Calculate the correct relative value for the "-rpath" link option (which sets
# the search path for finding shared libraries).
solib_dir_path_string = cc_toolchain._solib_dir
if is_native_deps and cc_toolchain._cpp_configuration.share_native_deps():
# For shared native libraries, special symlinking is applied to ensure C++
# toolchain libraries are available under $ORIGIN/_solib_[arch]. So we set the RPATH to find
# them.
# Note that we have to do this because $ORIGIN points to different paths for
# different targets. In other words, blaze-bin/d1/d2/d3/a_shareddeps.so and
# blaze-bin/d4/b_shareddeps.so have different path depths. The first could
# reference a standard blaze-bin/_solib_[arch] via $ORIGIN/../../../_solib[arch],
# and the second could use $ORIGIN/../_solib_[arch]. But since this is a shared
# artifact, both are symlinks to the same place, so
# there's no *one* RPATH setting that fits all targets involved in the sharing.
potential_solib_parents = []
rpath_roots = [solib_dir_path_string + "/"]
else:
potential_solib_parents = _find_potential_solib_parents(output, dynamic_library_solib_symlink_output, workspace_name)
rpath_roots = [exec_root + solib_dir_path_string + "/" for exec_root in potential_solib_parents]
# include_solib_dir: bool, include_toolchain_libraries_solib_dir: bool
# TODO(b/338618120): instead of returning include_solib_dir, and the paths inside _add_linker_inputs
include_solib_dir, include_toolchain_libraries_solib_dir = _collect_solib_dirs_from_libraries(
libraries,
prefer_static_libs,
feature_configuration,
solib_dir,
toolchain_libraries_solib_dir,
rpath_roots,
# Outputs:
library_search_directories,
rpath_roots_for_explicit_so_deps,
)
# Remove repetitions
rpath_roots_for_explicit_so_deps = rpath_roots_for_explicit_so_deps.keys()
# rpath ordering matters for performance; first add the one where most libraries are found.
direct_runtime_library_search_directories = []
if include_solib_dir:
direct_runtime_library_search_directories.extend(rpath_roots)
direct_runtime_library_search_directories.extend(rpath_roots_for_explicit_so_deps)
transitive_runtime_library_search_directories = []
if include_toolchain_libraries_solib_dir:
transitive_runtime_library_search_directories = [
_collect_toolchain_runtime_library_search_directories(
cc_toolchain,
output,
potential_solib_parents,
need_toolchain_libraries_rpath,
toolchain_libraries_solib_dir,
is_native_deps,
workspace_name,
),
]
all_runtime_library_search_directories = depset(
order = "topological",
direct = direct_runtime_library_search_directories,
transitive = transitive_runtime_library_search_directories,
)
return depset(library_search_directories), all_runtime_library_search_directories
def _collect_solib_dirs_from_libraries(
libraries,
prefer_static_libs,
feature_configuration,
solib_dir,
toolchain_libraries_solib_dir,
rpath_roots,
# Outputs:
library_search_directories,
rpath_roots_for_explicit_so_deps):
"""
Goes over all linker_inputs transforming them and collecting rpath_roots.
Args:
libraries: (list[LegacyLinkerInput]) Linker inputs
prefer_static_libs: (bool) Prefer static libraries.
feature_configuration: Feature configuration to be queried.
solib_dir: (str) solib directory.
toolchain_libraries_solib_dir: (str) Directory where toolchain stores language-runtime libraries (libstdc++, libc++ ...).
rpath_roots: (list[str]) rpath roots (for example solib_dir)
library_search_directories: (list[str]) Output collecting library search directories.
rpath_roots_for_explicit_so_deps: (dict[str, None]) Output collecting rpaths.
Returns:
(include_solib_dir: bool, include_toolchain_libraries_solib_dir: bool)
"""
include_solib_dir, include_toolchain_libraries_solib_dir = False, False
linked_libraries_paths = {} # :dict[str, str]
dont_copy_dynamic_libraries_to_binary = not feature_configuration.is_enabled("copy_dynamic_libraries_to_binary")
# On Windows, dynamic library (dll) cannot be linked directly when using toolchains that
# support interface library (eg. MSVC). If the user is doing so, it is only to be referenced
# in other places (such as copy_dynamic_libraries_to_binary); skip adding it.
windows_shared_libraries = (feature_configuration.is_enabled("targets_windows") and
feature_configuration.is_enabled("supports_interface_shared_libraries"))
solib_dir_split = reversed(solib_dir.split("/"))
for library in libraries:
static_lib = (prefer_static_libs and
(library.static_library != None or library.pic_static_library != None) or
(library.interface_library == None and library.dynamic_library == None))
if static_lib:
continue
if library.interface_library:
input_file = library.interface_library
original_file = library.resolved_symlink_interface_library or input_file
else:
input_file = library.dynamic_library
original_file = library.resolved_symlink_dynamic_library or input_file
original_lib_dir = original_file.dirname
library_identifier = library._library_identifier
previous_lib_dir = linked_libraries_paths.setdefault(library_identifier, original_lib_dir)
if previous_lib_dir != original_lib_dir:
fail(("You are trying to link the same dynamic library %s built in a different" +
" configuration. Previously registered instance had path %s, current one" +
" has path %s") %
(library_identifier, previous_lib_dir, original_lib_dir))
lib_dir = input_file.dirname
# When COPY_DYNAMIC_LIBRARIES_TO_BINARY is enabled, dynamic libraries are not symlinked
# under solib_dir, so don't check it and don't include solib_dir.
if dont_copy_dynamic_libraries_to_binary:
# The first fragment is bazel-out, and the second may contain a configuration mnemonic.
# We should always add the default solib dir because that's where libraries will be found
# e.g., in remote execution, so we ignore the first two fragments.
if not include_solib_dir and lib_dir.split("/")[2:] == solib_dir.split("/")[2:]:
include_solib_dir = True
if lib_dir == toolchain_libraries_solib_dir:
include_toolchain_libraries_solib_dir = True
if windows_shared_libraries:
if is_shared_library(input_file):
continue
lib_dir = input_file.dirname
if lib_dir != solib_dir and (not toolchain_libraries_solib_dir or toolchain_libraries_solib_dir != lib_dir):
# TODO(b/338618120): the code should be optimized to first get unique library_search_directories and
# then compute relative paths, i.e. rpath_roots_for_explicit_so_deps
# TODO(b/331164666): this is a duplication of _get_relative function implemented below
dotdots = ""
common_parent = solib_dir
for seg in solib_dir_split:
if lib_dir.startswith(common_parent + "/"):
break
dotdots += "../"
common_parent = common_parent[:-len(seg) - 1]
# When all dynamic deps are built in transitioned configurations, the default solib dir is
# not created. While resolving paths, the dynamic linker stops at the first directory that
# does not exist, even when followed by "../". We thus have to normalize the relative path.
for rpath_root in rpath_roots:
normalized_path_to_root = paths.normalize(rpath_root + dotdots + paths.relativize(lib_dir, common_parent))
rpath_roots_for_explicit_so_deps[normalized_path_to_root] = None
# Unless running locally, libraries will be available under the root relative path, so we
# should add that to the rpath as well.
if input_file.short_path.startswith("_solib_"):
artifact_path_under_solib = "/".join(input_file.short_path.split("/")[1:-1])
for rpath_root in rpath_roots:
rpath_roots_for_explicit_so_deps[rpath_root + artifact_path_under_solib] = None
library_search_directories.append(lib_dir)
return include_solib_dir, include_toolchain_libraries_solib_dir
def _collect_toolchain_runtime_library_search_directories(
cc_toolchain,
output,
potential_solib_parents,
need_toolchain_libraries_rpath,
toolchain_libraries_solib_dir,
is_native_deps,
workspace_name):
if not need_toolchain_libraries_rpath:
return depset()
runtime_library_search_directories = []
toolchain_libraries_solib_name = paths.basename(toolchain_libraries_solib_dir)
if not (is_native_deps and cc_toolchain.cc_configuration.share_native_deps()):
for potential_exec_root in _find_toolchain_solib_parents(cc_toolchain, output, potential_solib_parents, toolchain_libraries_solib_dir, workspace_name):
runtime_library_search_directories.append(potential_exec_root + toolchain_libraries_solib_name + "/")
if is_native_deps:
runtime_library_search_directories.append("../" + toolchain_libraries_solib_name + "/")
runtime_library_search_directories.append(".")
runtime_library_search_directories.append(toolchain_libraries_solib_name + "/")
return depset(runtime_library_search_directories, order = "topological")
# TODO(b/338618120): converge back together _find_toolchain_solib_parents and _find_potential_solib_parents
def _find_potential_solib_parents(output, dynamic_library_solib_symlink_output, workspace_name):
solib_parents = []
outputs = [output]
if dynamic_library_solib_symlink_output:
outputs.append(dynamic_library_solib_symlink_output)
for output in outputs:
# The runtime location of the solib directory relative to the binary depends on four factors:
#
# * whether the binary is contained in the main repository or an external repository;
# * whether the binary is executed directly or from a runfiles tree;
# * whether the binary is staged as a symlink (sandboxed execution; local execution if the
# binary is in the runfiles of another target) or a regular file (remote execution) - the
# dynamic linker follows sandbox and runfiles symlinks into its location under the
# unsandboxed execroot, which thus becomes the effective $ORIGIN;
# * whether --experimental_sibling_repository_layout is enabled or not.
#
# The rpaths emitted into the binary thus have to cover the following cases (assuming that
# the binary target is located in the pkg `pkg` and has name `file`) for the directory used
# as $ORIGIN by the dynamic linker and the directory containing the solib directories:
#
# 1. main, direct, symlink:
# $ORIGIN: $EXECROOT/pkg
# solib root: $EXECROOT
# 2. main, direct, regular file:
# $ORIGIN: $EXECROOT/pkg
# solib root: $EXECROOT/pkg/file.runfiles/main_repo
# 3. main, runfiles, symlink:
# $ORIGIN: $EXECROOT/pkg
# solib root: $EXECROOT
# 4. main, runfiles, regular file:
# $ORIGIN: other_target.runfiles/main_repo/pkg
# solib root: other_target.runfiles/main_repo
# 5a. external, direct, symlink:
# $ORIGIN: $EXECROOT/external/other_repo/pkg
# solib root: $EXECROOT
# 5b. external, direct, symlink, with --experimental_sibling_repository_layout:
# $ORIGIN: $EXECROOT/../other_repo/pkg
# solib root: $EXECROOT/../other_repo
# 6a. external, direct, regular file:
# $ORIGIN: $EXECROOT/external/other_repo/pkg
# solib root: $EXECROOT/external/other_repo/pkg/file.runfiles/main_repo
# 6b. external, direct, regular file, with --experimental_sibling_repository_layout:
# $ORIGIN: $EXECROOT/../other_repo/pkg
# solib root: $EXECROOT/../other_repo/pkg/file.runfiles/other_repo
# 7a. external, runfiles, symlink:
# $ORIGIN: $EXECROOT/external/other_repo/pkg
# solib root: $EXECROOT
# 7b. external, runfiles, symlink, with --experimental_sibling_repository_layout:
# $ORIGIN: $EXECROOT/../other_repo/pkg
# solib root: $EXECROOT/../other_repo
# 8a. external, runfiles, regular file:
# $ORIGIN: other_target.runfiles/some_repo/pkg
# solib root: other_target.runfiles/main_repo
# 8b. external, runfiles, regular file, with --experimental_sibling_repository_layout:
# $ORIGIN: other_target.runfiles/some_repo/pkg
# solib root: other_target.runfiles/some_repo
#
# Cases 1, 3, 4, 5, 7, and 8b are covered by an rpath that walks up the root relative path.
# Cases 2 and 6 covered by walking into file.runfiles/main_repo.
# Case 8a is covered by walking up some_repo/pkg and then into main_repo.
path = output.short_path
is_external = path.startswith("../")
uses_legacy_repository_layout = is_external and output.path.split("/")[3] == "external"
# Handles cases 1, 3, 4, 5, and 7.
solib_parents.append("../" * (len(path.split("/")) - 1 - (2 if is_external and not uses_legacy_repository_layout else 0)))
# Handle cases 2 and 6.
if is_external and not uses_legacy_repository_layout:
# Case 6b
solib_repository_name = path.split("/")[1]
else:
# Cases 2 and 6a
solib_repository_name = workspace_name
solib_parents.append(output.basename + ".runfiles/" + solib_repository_name + "/")
if is_external and uses_legacy_repository_layout:
# Handles case 8a. The runfiles path is of the form ../some_repo/pkg/file and we need to
# walk up some_repo/pkg and then down into main_repo.
solib_parents.append(
"../" * (len(output.short_path.split("/")) - 2) + workspace_name + "/",
)
return solib_parents
def _find_toolchain_solib_parents(cc_toolchain, output, potential_solib_parents, toolchain_libraries_solib_dir, workspace_name):
uses_legacy_repository_layout = not cc_toolchain._is_sibling_repository_layout
# When -experimental_sibling_repository_layout is not enabled, the toolchain solib sits next to
# the solib_<cpu> directory - so that it shares the same parents.
if uses_legacy_repository_layout:
return potential_solib_parents
# When -experimental_sibling_repository_layout is enabled, the toolchain solib is located in
# these 2 places:
# 1. The `bin` directory of the repository where the toolchain target is declared (this is the
# parent directory of `toolchainLibrariesSolibDir`).
# 2. In `target.runfiles/<toolchain repo>`
#
# And the following factors affect what $ORIGIN is resolved to:
# * whether the binary is contained in the main repository or an external repository;
# * whether the binary is executed directly or from a runfiles tree;
# * whether the binary is staged as a symlink (sandboxed execution; local execution if the
# binary is in the runfiles of another target) or a regular file (remote execution) - the
# dynamic linker follows sandbox and runfiles symlinks into its location under the
# unsandboxed execroot, which thus becomes the effective $ORIGIN;
#
# The rpaths emitted into the binary thus have to cover the following cases (assuming that
# the binary target is located in the pkg `pkg` and has name `file`) for the directory used
# as $ORIGIN by the dynamic linker and the directory containing the solib directories:
# 1. main, direct, symlink:
# $ORIGIN: $EXECROOT/pkg
# solib root: <toolchain repo bin>
# 2. main, direct, regular file:
# $ORIGIN: $EXECROOT/pkg
# solib root: $EXECROOT/pkg/file.runfiles/<toolchain repo>
# 3. main, runfiles, symlink:
# $ORIGIN: $EXECROOT/pkg
# solib root: <toolchain repo bin>
# 4. main, runfiles, regular file:
# $ORIGIN: other_target.runfiles/main_repo/pkg
# solib root: other_target.runfiles/<toolchain repo>
# 5. external, direct, symlink:
# $ORIGIN: $EXECROOT/../other_repo/pkg
# solib root: <toolchain repo bin>
# 6. external, direct, regular file:
# $ORIGIN: $EXECROOT/../other_repo/pkg
# solib root: $EXECROOT/../other_repo/pkg/file.runfiles/<toolchain repo>
# 7. external, runfiles, symlink:
# $ORIGIN: $EXECROOT/../other_repo/pkg
# solib root: <toolchain repo bin>
# 8. external, runfiles, regular file:
# $ORIGIN: other_target.runfiles/some_repo/pkg
# solib root: other_target.runfiles/<toolchain repo>
#
# For cases 1, 3, 5, 7, we need to compute the relative path from the output artifact to
# toolchain repo's bin directory. For 2 and 6, we walk down into `file.runfiles/<toolchain
# repo>`. For 4 and 8, we need to compute the relative path from the output runfile to
# <toolchain repo> under runfiles.
solib_parents = []
# Cases 1, 3, 5, 7
toolchain_bin_exec_path = paths.dirname(toolchain_libraries_solib_dir)
binary_origin_exec_path = output.dirname
solib_parents.append(
_get_relative(binary_origin_exec_path, toolchain_bin_exec_path) + "/",
)
# Cases 2 and 6
toolchain_runfiles_repo_name = _get_runfiles_repo_name(cc_toolchain._toolchain_label.repo_name, workspace_name)
solib_parents.append(
output.basename + ".runfiles/" + toolchain_runfiles_repo_name + "/",
)
# Cases 4 and 8
binary_repo_name = _get_runfiles_repo_name(output.owner.repo_name, workspace_name)
toolchain_bin_runfiles_path = toolchain_runfiles_repo_name
binary_origin_runfiles_path = binary_repo_name + "/" + output.dirname[len(output.root.path):].removeprefix("/")
solib_parents.append(
_get_relative(binary_origin_runfiles_path, toolchain_bin_runfiles_path) + "/",
)
return solib_parents
def _get_runfiles_repo_name(repo_name, workspace_name):
# TODO(b/331164666): inline
return repo_name or workspace_name
def _get_relative(start, to):
"""
Returns the relative path from "from" to "to".
Example 1:
`_get_relative("foo", "foo/bar/wiz") -> returns "bar/wiz"`
Example 2:
`_get_relative("foo/bar/wiz", "foo/wiz") -> returns "../../wiz"`
The following requirements / assumptions are made: 1) paths must be both relative; 2) they
are assumed to be relative to the same location; 3) when the `from` path starts with
`..` prefixes, the prefix length must not exceed `..` prefixes of the `to` path.
"""
common_parent = start
dotdots = ""
for seg in reversed(common_parent.split("/")):
if paths.starts_with(to, common_parent):
break
dotdots += "../"
common_parent = common_parent[:-len(seg) - 1]
return dotdots + paths.relativize(to, common_parent)