blob: 6073344d124447781e12b3f566c43f13da85da9b [file]
# 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.
"""Common functions that create C++ link and LTO indexing action."""
load("//cc/common:semantics.bzl", "semantics")
load("//cc/private:cc_internal.bzl", _cc_internal = "cc_internal")
load("//cc/private/compile:linkstamp_compile.bzl", "register_linkstamp_compile_action")
load("//cc/private/link:collect_solib_dirs.bzl", "collect_solib_dirs")
load("//cc/private/link:create_libraries_to_link_values.bzl", "add_libraries_to_link", "add_object_files_to_link", "process_objects_for_lto")
load("//cc/private/link:link_build_variables.bzl", "setup_common_linking_variables")
load("//cc/private/link:target_types.bzl", "LINKING_MODE", "LINK_TARGET_TYPE", "USE_ARCHIVER", "USE_LINKER", "is_dynamic_library")
load("//cc/private/rules_impl:native_cc_common.bzl", _cc_common_internal = "native_cc_common")
def finalize_link_action(
actions,
mnemonic,
action_name,
link_type,
linking_mode,
stamping,
feature_configuration,
cc_toolchain,
progress_message,
# Inputs:
object_file_inputs,
non_code_inputs,
libraries_to_link,
linkstamp_map,
linkstamp_object_artifacts,
linkstamp_object_file_inputs,
user_link_flags,
# Custom user input files and variables:
additional_linker_inputs,
additional_build_variables,
# Outputs:
output,
interface_output,
dynamic_library_solib_symlink_output,
action_outputs,
# Originating from private APIs:
use_test_only_flags,
whole_archive,
native_deps,
additional_linkstamp_defines,
# LTO:
lto_mapping,
allow_lto_indexing):
"""Creates C++ linking or LTO indexing, and linkstamp compile actions.
Runs all the libraries through `libraries_to_link_collector`.
Sets up common link build variables.
Picks the right tool for the action.
Compiles the linkstamps.
Creates the action, producing the `output` and maybe `interface_output`.
Args:
actions: (Actions) `actions` object.
mnemonic: (str) The mnemonic used in the action.
action_name: (str) action name.
link_type: (LINK_TARGET_TYPE) Type of libraries to create.
linking_mode: (LINKING_MODE) Linking mode used for dynamic libraries.
stamping: (bool) Whether to stamp the output.
feature_configuration: (FeatureConfiguration) `feature_configuration` to be queried.
cc_toolchain: (CcToolchainInfo) CcToolchainInfo provider to be used.
progress_message: (str) The progress message of the action.
object_file_inputs: (list[File]) Object files
non_code_inputs: (list[File]) Additional inputs to the linker.
libraries_to_link: (list[LibraryToLink]) The libraries to link in.
linkstamp_map: (dict[Linkstamp, File]) Map from linkstamps to their object files.
linkstamp_object_artifacts: (list[File]) Linkstamp object files.
linkstamp_object_file_inputs: (list[File]) Linkstamp object files wrapped into LinkerInputs.
user_link_flags: (list[str]) Additional list of linker options.
additional_linker_inputs: (list[File]|depset[File]) For additional inputs to the linking action,
e.g.: linking scripts.
additional_build_variables: (dict[str,?]) linking variables.
output: (File) The main output.
interface_output: (None|File) The interface library. The second output.
dynamic_library_solib_symlink_output: (None|File) The symlink to the main output in _solib_ dir.
action_outputs: (depset[File])
use_test_only_flags: (bool) undocumented.
whole_archive: (bool) undocumented.
native_deps: (bool) undocumented.
additional_linkstamp_defines: (list[str]) undocumented.
lto_mapping: (dict[File, File]) Map from bitcode files to object files. Used to replace all linker inputs.
allow_lto_indexing: (bool) Was LTO indexing done.
Returns:
None
"""
need_whole_archive = whole_archive or _need_whole_archive(
feature_configuration,
linking_mode,
link_type,
user_link_flags,
cc_toolchain._cpp_configuration,
)
must_keep_debug = any([lib._must_keep_debug for lib in libraries_to_link])
toolchain_libraries_solib_dir = ""
if feature_configuration.is_enabled("static_link_cpp_runtimes"):
toolchain_libraries_solib_dir = cc_toolchain.dynamic_runtime_solib_dir
solib_dir = output.root.path + "/" + cc_toolchain._solib_dir
libraries_to_link_values = []
expanded_linker_artifacts = []
lto_map = dict(lto_mapping) # copy map, because following functions pop from it
if feature_configuration.is_enabled("use_lto_native_object_directory"):
shared_non_lto_obj_root_prefix = "shared.nonlto-obj"
else:
shared_non_lto_obj_root_prefix = "shared.nonlto"
object_file_inputs = process_objects_for_lto(
object_file_inputs,
lto_map,
allow_lto_indexing,
shared_non_lto_obj_root_prefix,
expanded_linker_artifacts,
)
add_object_files_to_link(object_file_inputs, libraries_to_link_values)
add_object_files_to_link(linkstamp_object_file_inputs, libraries_to_link_values)
# TODO(b/331164666): Remove CppHelper.getArchiveType
use_start_end_lib = (cc_toolchain._cpp_configuration.start_end_lib() and
feature_configuration.is_enabled("supports_start_end_lib"))
# TODO(b/338618120): deduplicate prefer_static_lib, prefer_pic_libs
prefer_static_libs = linking_mode == LINKING_MODE.STATIC or \
not feature_configuration.is_enabled("supports_dynamic_linker")
# TODO(b/412540147): We select PIC libraries iff creating a dynamic library. Match PIC flags.
prefer_pic_libs = is_dynamic_library(link_type)
add_libraries_to_link(
libraries_to_link,
prefer_static_libs,
prefer_pic_libs,
use_start_end_lib,
need_whole_archive,
lto_map,
allow_lto_indexing,
shared_non_lto_obj_root_prefix,
feature_configuration,
expanded_linker_artifacts,
libraries_to_link_values,
)
# Interning is necessary because the values are repeated so often.
# Without it, this causes a very large regression.
libraries_to_link_values = _cc_internal.intern_seq(libraries_to_link_values)
if lto_map:
fail("Still have LTO objects left: %s" % lto_map)
expanded_linker_artifacts = depset([
lto_mapping.get(li, li)
for li in expanded_linker_artifacts
])
library_search_directories, all_runtime_library_search_directories = collect_solib_dirs(
libraries_to_link,
cc_toolchain,
feature_configuration,
prefer_static_libs,
output,
dynamic_library_solib_symlink_output,
link_type,
linking_mode,
native_deps,
solib_dir,
toolchain_libraries_solib_dir,
# TODO(b/338618120): remove cheat using semantic or simplifying collect_libraries_to_link
_cc_internal.actions2ctx_cheat(actions).workspace_name,
)
# Add build variables necessary to template link args into the crosstool.
build_variables = setup_common_linking_variables(
cc_toolchain = cc_toolchain,
feature_configuration = feature_configuration,
is_using_linker = link_type.linker_or_archiver == USE_LINKER,
is_linking_dynamic_library = link_type == LINK_TARGET_TYPE.DYNAMIC_LIBRARY,
param_file = "LINKER_PARAM_FILE_PLACEHOLDER",
must_keep_debug = must_keep_debug,
use_test_only_flags = use_test_only_flags,
user_link_flags = user_link_flags,
runtime_library_search_directories = all_runtime_library_search_directories,
libraries_to_link = libraries_to_link_values,
library_search_directories = library_search_directories,
)
build_variables = build_variables | additional_build_variables
if link_type == LINK_TARGET_TYPE.INTERFACE_DYNAMIC_LIBRARY:
fail("you can't link an interface dynamic library directly")
if not is_dynamic_library(link_type):
if interface_output:
fail("interface output may only be non-null for dynamic library links")
if link_type.linker_or_archiver == USE_ARCHIVER:
# solib dir must be None for static links
toolchain_libraries_solib_dir = None
if linking_mode != LINKING_MODE.STATIC:
fail("static library link must be static")
if native_deps:
fail("the native deps flag must be false for static links")
if need_whole_archive:
fail("the need whole archive flag must be false for static links")
# TODO(b/62693279): Cleanup once internal crosstools specify ifso building correctly.
should_use_link_dynamic_library_tool = (
is_dynamic_library(link_type) and
feature_configuration.is_enabled("supports_interface_shared_libraries") and
not feature_configuration.is_enabled("has_configured_linker_path")
)
if should_use_link_dynamic_library_tool:
tool_path = cc_toolchain._link_dynamic_library_tool.path
else:
tool_path = _cc_common_internal.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = link_type.action_name,
)
if cc_toolchain._cpp_configuration.incompatible_use_specific_tool_files() and \
link_type.linker_or_archiver == USE_ARCHIVER:
linker_files = cc_toolchain._ar_files
else:
linker_files = cc_toolchain._linker_files
# Compute the set of inputs - we only need stable order here.
dependency_inputs = depset(
# TODO(b/338618120): This should be set as a tool, not an input
direct = [cc_toolchain._link_dynamic_library_tool] if should_use_link_dynamic_library_tool else [],
transitive = [additional_linker_inputs, linker_files],
)
non_code_inputs = depset(non_code_inputs)
# actions display their first input in progress message, and that is a public interface - therefore the
# order here is important.
inputs = depset(transitive = [expanded_linker_artifacts, non_code_inputs, dependency_inputs])
if linkstamp_map:
# A different value from use_pic
needs_pic = (cc_toolchain._cpp_configuration.force_pic() or
(is_dynamic_library(link_type) and feature_configuration.is_enabled("supports_pic")))
for linkstamp, artifact in linkstamp_map.items():
register_linkstamp_compile_action(
actions = actions,
cc_toolchain = cc_toolchain,
feature_configuration = feature_configuration,
source_file = linkstamp.file(),
output_file = artifact,
compilation_inputs = linkstamp.hdrs(),
inputs_for_validation = inputs,
label_replacement = _quote_replacement(output.path if native_deps and cc_toolchain._cpp_configuration.share_native_deps() else str(output.owner)),
output_replacement = _quote_replacement(output.path),
needs_pic = needs_pic,
stamping = stamping,
additional_linkstamp_defines = additional_linkstamp_defines,
)
# Add linkstamps to the inputs (adding them sooner would create a cycle)
inputs = depset(
direct = linkstamp_map.values(),
transitive = [
expanded_linker_artifacts,
non_code_inputs,
dependency_inputs,
depset(linkstamp_object_artifacts),
],
)
_create_action(
actions,
action_name,
feature_configuration,
cc_toolchain,
build_variables,
mnemonic,
tool_path,
inputs,
action_outputs,
progress_message,
link_type,
interface_output,
)
def _create_action(
actions,
action_name,
feature_configuration,
cc_toolchain,
build_variables,
mnemonic,
tool_path,
inputs,
outputs,
progress_message,
link_type,
interface_output):
"""
Creates C++ linking or LTO indexing action.
Args:
actions: (StarlarkActions) `actions` object
action_name: (str) action name
feature_configuration: (FeatureConfiguration) `feature_configuration` to be queried.
cc_toolchain: (CcToolchainInfo) CcToolchainInfo provider to be used.
build_variables: (dict[str,?]) variables to substitute into command line
mnemonic: (str) action mnemonic
tool_path: (str) tool to execute
inputs: (depset[File]) all inputs to the action
outputs: (depset(File)) all outputs of the action
progress_message: (str) progress message
link_type: (LINK_TARGET_TYPE) link type, used to determine parameter file type
interface_output: (File) Interface output file, if any.
"""
parameter_file_type = None
if _can_split_command_line(
link_type,
cc_toolchain,
feature_configuration,
interface_output,
):
if feature_configuration.is_enabled("gcc_quoting_for_param_files"):
parameter_file_type = "GCC_QUOTED"
elif feature_configuration.is_enabled("windows_quoting_for_param_files"):
parameter_file_type = "WINDOWS"
else:
parameter_file_type = "UNQUOTED"
# If the crosstool uses action_configs to configure cc compilation, collect execution info
# from there, otherwise, use no execution info.
# TODO(b/27903698): Assert that the crosstool has an action_config for this action.
execution_info = {}
for req in _cc_common_internal.get_execution_requirements(feature_configuration = feature_configuration, action_name = action_name):
execution_info[req] = ""
build_variables = _cc_internal.cc_toolchain_variables(vars = build_variables)
link_args = _cc_internal.get_link_args(
feature_configuration = feature_configuration,
action_name = action_name,
build_variables = build_variables,
parameter_file_type = parameter_file_type,
)
env = _cc_common_internal.get_environment_variables(
feature_configuration = feature_configuration,
action_name = action_name,
variables = build_variables,
)
if "requires_darwin" not in execution_info:
# This prevents gcc from writing the unpredictable (and often irrelevant)
# value of getcwd() into the debug info.
env = env | {"PWD": "/proc/self/cwd"}
exec_group = None
toolchain = None
if "cpp_link" in _cc_internal.actions2ctx_cheat(actions).exec_groups:
# TODO(b/338618120): ^ remove cheat, no idea how though, maybe always use cpp_link exec group?
exec_group = "cpp_link"
elif "@@bazel_tools//tools/cpp:toolchain_type" in _cc_internal.actions2ctx_cheat(actions).toolchains: # buildifier: disable=canonical-repository
# TODO(b/338618120): ^ remove cheat, needs depot cleanup, always use a toolchain
toolchain = semantics.toolchain
execution_info = semantics.get_cc_link_memlimit(
cc_toolchain._cpp_configuration.compilation_mode(),
execution_info,
)
actions.run(
mnemonic = mnemonic,
executable = tool_path,
arguments = [link_args],
inputs = inputs,
outputs = outputs,
progress_message = progress_message,
resource_set = _resource_set,
env = env,
use_default_shell_env = True,
execution_requirements = execution_info,
toolchain = toolchain,
exec_group = exec_group,
)
def _can_split_command_line(
link_type,
cc_toolchain,
feature_configuration,
interface_output):
if not cc_toolchain._supports_param_files:
return False
elif is_dynamic_library(link_type):
# On Unix, we currently can't split dynamic library links if they have interface outputs.
# That was probably an unintended side effect of the change that introduced interface
# outputs.
if interface_output:
# On Windows, We can always split the command line when building DLL.
return feature_configuration.is_enabled("targets_windows")
else:
return True
elif link_type.linker_or_archiver == USE_LINKER:
return True
elif link_type.linker_or_archiver == USE_ARCHIVER:
# A feature to control whether to use param files for archiving commands.
return feature_configuration.is_enabled("archive_param_file")
# This should be unreachable:
return False
def _need_whole_archive(feature_configuration, linking_mode, link_type, linkopts, cpp_config):
"""The default heuristic on whether we need to use whole-archive for the link."""
shared_linkopts = is_dynamic_library(link_type) or "-shared" in linkopts or "-shared" in cpp_config.linkopts
# Fasten your seat belt, the logic below doesn't make perfect sense and it's full of obviously
# missed corner cases. The world still stands and depends on this behavior, so ¯\_(ツ)_/¯.
if not shared_linkopts:
# We are not producing shared library, there is no reason to use --whole-archive with
# executable (if the executable doesn't use the symbols, nobody else will, so --whole-archive
# is not needed).
return False
if feature_configuration.is_requested("force_no_whole_archive"):
return False
if cpp_config.incompatible_remove_legacy_whole_archive():
# --incompatible_remove_legacy_whole_archive has been flipped, no --whole-archive for the
# entire build.
return False
if linking_mode != LINKING_MODE.STATIC:
# legacy whole archive only applies to static linking mode.
return False
if feature_configuration.is_requested("legacy_whole_archive"):
# --incompatible_remove_legacy_whole_archive has not been flipped, and this target requested
# --whole-archive using features.
return True
if cpp_config.legacy_whole_archive():
# --incompatible_remove_legacy_whole_archive has not been flipped, so whether to
# use --whole-archive depends on --legacy_whole_archive.
return True
# Hopefully future default.
return False
def _quote_replacement(s):
if "\\" not in s and "$" not in s:
return s
return s.replace("\\", "\\\\").replace("$", "\\$")
def _resource_set(os, inputs):
if os == "osx":
return {"memory": 15 + 0.05 * inputs, "cpu": 1}
elif os == "linux":
return {"memory": max(50, -100 + 0.1 * inputs), "cpu": 1}
else:
return {"memory": 1500 + inputs, "cpu": 1}