blob: 30faa67695a8f58818cedb71d4a4a691fec76442 [file]
# Copyright 2023 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.
"""Compilation helper for C++ rules."""
load("@bazel_skylib//lib:paths.bzl", "paths")
load(
"//cc/common:cc_helper_internal.bzl",
"get_relative_path",
"package_source_root",
"repository_exec_path",
)
load("//cc/common:semantics.bzl", "STRIP_INCLUDE_PREFIX_APPLIES_TO_TEXTUAL_HEADERS", "USE_EXEC_ROOT_FOR_VIRTUAL_INCLUDES_SYMLINKS")
load("//cc/private:cc_info.bzl", "create_compilation_context", "create_module_map")
load("//cc/private:cc_internal.bzl", _cc_internal = "cc_internal")
_VIRTUAL_INCLUDES_DIR = "_virtual_includes"
def _include_dir(directory, repo_path, sibling_repo_layout):
if sibling_repo_layout:
return directory
else:
return get_relative_path(directory, repo_path)
def _repo_relative_path(artifact):
relative_path = artifact.path
if artifact.is_source:
if artifact.owner.workspace_root:
relative_path = "/".join(relative_path.split("/")[2:])
else:
relative_path = paths.relativize(relative_path, artifact.root.path)
if (artifact.owner.workspace_root.startswith("external/") or artifact.owner.workspace_root.startswith("../")) and \
relative_path.startswith("external"):
relative_path = "/".join(relative_path.split("/")[2:])
return relative_path
def _enabled(feature_configuration, feature_name):
return feature_configuration.is_enabled(feature_name)
def _compute_public_headers(
actions,
config,
public_headers_artifacts,
include_prefix,
strip_include_prefix,
label,
binfiles_dir,
non_module_map_headers,
is_sibling_repository_layout,
shorten_virtual_includes,
*,
must_use_strip_prefix = True):
if include_prefix:
if not paths.is_normalized(include_prefix, False):
fail("include prefix should not contain uplevel references: " + include_prefix)
if paths.is_absolute(include_prefix):
fail("include prefix should be a relative path: " + include_prefix)
if strip_include_prefix:
if not paths.is_normalized(strip_include_prefix, False):
fail("strip include prefix should not contain uplevel references: " + strip_include_prefix)
strip_prefix = strip_include_prefix
if strip_prefix.startswith("//"):
strip_prefix = strip_prefix[1:]
if paths.is_absolute(strip_prefix):
# Very crude way of determining driver length, but the best one at the same time.
strip_driver_length = 0
if strip_prefix.startswith("/"): # Unix
strip_driver_length = 1
elif len(strip_prefix) > 1 and strip_prefix[1] == ":": # Windows
strip_driver_length = 3
strip_prefix = strip_prefix[strip_driver_length:]
else:
# paths.normalize differs from Java normalize call in a way
# that if string to be normalized is "." Starlark version returns ".",
# while Java version returns an empty string "".
# Because of this if label.package is an empty string and strip_prefix
# is "." get_relative_path("", ".") returns "." instead of "".
strip_prefix = get_relative_path(label.package, strip_prefix)
if strip_prefix == ".":
strip_prefix = ""
elif include_prefix:
strip_prefix = label.package
else:
strip_prefix = None
if strip_prefix and strip_prefix.startswith("/"):
strip_prefix = strip_prefix[1:]
if include_prefix and include_prefix.startswith("/"):
include_prefix = include_prefix[1:]
if strip_prefix == None and not include_prefix:
# If CppOptions.experimentalStarlarkCompiling is enabled, then
# strip_include_prefix and include_prefix are not None.
# If the option is disabled, their default values (from CcCompilationHelper) are None.
#
# Special case: when "strip_include_prefix = '.'" is set in a target located in
# workspace/BUILD.bazel, strip_prefix will be "", not None.
# Thus, we check differently for empty values.
#
# If neither strip_include_prefix nor include_prefix is specified,
# we don't need to create virtual headers, so we return the public headers as-is.
return struct(
headers = public_headers_artifacts + non_module_map_headers,
module_map_headers = public_headers_artifacts,
virtual_include_path = None,
virtual_to_original_headers = [],
)
module_map_headers = []
virtual_to_original_headers_list = []
source_package_path = package_source_root(label.workspace_name, label.package, is_sibling_repository_layout)
if shorten_virtual_includes:
virtual_include_dir = paths.join(_VIRTUAL_INCLUDES_DIR, "%x" % hash(paths.join(source_package_path, label.name)))
else:
virtual_include_dir = paths.join(source_package_path, _VIRTUAL_INCLUDES_DIR, label.name)
for original_header in public_headers_artifacts:
repo_relative_path = _repo_relative_path(original_header)
if not repo_relative_path.startswith(strip_prefix):
if not must_use_strip_prefix:
module_map_headers.append(original_header)
continue
fail("header '{}' is not under the specified strip prefix '{}'".format(repo_relative_path, strip_prefix))
include_path = paths.relativize(repo_relative_path, strip_prefix)
if include_prefix != None:
include_path = get_relative_path(include_prefix, include_path)
virtual_header = actions.declare_shareable_artifact(paths.join(virtual_include_dir, include_path))
actions.symlink(
output = virtual_header,
target_file = original_header,
progress_message = "Symlinking virtual headers for %{label}",
use_exec_root_for_source = USE_EXEC_ROOT_FOR_VIRTUAL_INCLUDES_SYMLINKS,
)
module_map_headers.append(virtual_header)
if config.coverage_enabled:
virtual_to_original_headers_list.append((virtual_header.path, original_header.path))
module_map_headers.append(original_header)
virtual_headers = module_map_headers + non_module_map_headers
return struct(
headers = virtual_headers,
module_map_headers = module_map_headers,
virtual_include_path = paths.join(binfiles_dir, virtual_include_dir),
virtual_to_original_headers = virtual_to_original_headers_list,
)
def _generates_header_module(feature_configuration, public_headers, private_headers, generate_action):
return _enabled(feature_configuration, "header_modules") and \
(public_headers or private_headers) and \
generate_action
def _header_module_artifact(actions, label, is_sibling_repository_layout, suffix, extension):
object_dir = paths.join(paths.join(package_source_root(label.workspace_name, label.package, is_sibling_repository_layout), "_objs"), label.name)
base_name = label.name.split("/")[-1]
output_path = paths.join(object_dir, base_name + suffix + extension)
return actions.declare_shareable_artifact(output_path)
def _collect_module_maps(deps, cc_toolchain_compilation_context, additional_cpp_module_maps):
# TODO(bazel-team): Here we use the implementationDeps to build the dependents of this rule's
# module map. This is technically incorrect for the following reasons:
# - Clang will not issue a layering_check warning if headers from implementation deps are
# included from headers of this library.
# - If we were to ever build with modules, Clang might store this dependency inside the .pcm
# It should be evaluated whether this is ok. If this turned into a problem at some
# point, we could probably just declare two different modules with different use-declarations
# in the module map file.
direct_module_maps = []
transitive_module_maps = []
for cc_context in deps:
if cc_context._module_map != None:
direct_module_maps.append(cc_context._module_map)
transitive_module_maps.append(cc_context._exporting_module_maps)
if cc_toolchain_compilation_context != None and cc_toolchain_compilation_context._module_map != None:
direct_module_maps.append(cc_toolchain_compilation_context._module_map)
direct_module_maps.extend(additional_cpp_module_maps)
return depset(direct = direct_module_maps, transitive = transitive_module_maps)
_ModuleMapInfo = provider(
doc = "An internal provider for create_module_map_action().",
fields = [
"module_map",
"public_headers",
"textual_headers",
"private_headers",
"dependency_module_maps",
"additional_exported_headers",
"separate_module_headers",
"compiled_module",
"generate_submodules",
"extern_dependencies",
"leading_periods",
],
)
def _module_map_struct_to_module_map_content(parameters, tree_expander):
lines = []
module_map = parameters.module_map
lines.append("module \"%s\" {" % module_map.name)
lines.append(" export *")
def expanded(artifacts):
expanded = []
for artifact in artifacts:
if artifact.is_directory:
expanded.extend(tree_expander.expand(artifact))
else:
expanded.append(artifact)
return expanded
def add_header(path, visibility, can_compile):
header_line = []
if parameters.generate_submodules:
lines.append(" module \"" + path + "\" {")
lines.append(" export *")
header_line.append(" ")
header_line.append(" ")
if visibility:
header_line.append(visibility)
header_line.append(" ")
should_compile = parameters.compiled_module and not path.endswith(".inc")
if not can_compile or not should_compile:
header_line.append("textual ")
header_line.append("header \"")
header_line.append(parameters.leading_periods)
header_line.append(path)
header_line.append("\"")
lines.append("".join(header_line))
if parameters.generate_submodules:
lines.append(" }")
added_paths = set()
for header in expanded(parameters.public_headers):
if header.path in added_paths:
continue
add_header(path = header.path, visibility = "", can_compile = True)
added_paths.add(header.path)
for header in expanded(parameters.textual_headers):
if header.path in added_paths:
continue
add_header(path = header.path, visibility = "", can_compile = False)
added_paths.add(header.path)
for header in expanded(parameters.private_headers):
if header.path in added_paths:
continue
add_header(path = header.path, visibility = "private", can_compile = True)
added_paths.add(header.path)
for header in parameters.separate_module_headers:
if header.path in added_paths:
continue
add_header(path = header.path, visibility = "", can_compile = False)
added_paths.add(header.path)
for path in parameters.additional_exported_headers:
if path in added_paths:
continue
add_header(path = path, visibility = "", can_compile = False)
added_paths.add(path)
dependency_module_maps = parameters.dependency_module_maps.to_list()
for dep in dependency_module_maps:
lines.append(" use \"" + dep.name + "\"")
if parameters.separate_module_headers:
separate_name = module_map.name + ".sep"
lines.append(" use \"" + separate_name + "\"")
lines.append("}")
lines.append("module \"" + separate_name + "\" {")
lines.append(" export *")
added_paths = set()
for header in parameters.separate_module_headers:
if header.path in added_paths:
continue
add_header(path = header.path, visibility = "", can_compile = True)
added_paths.add(header.path)
for dep in dependency_module_maps:
lines.append(" use \"" + dep.name + "\"")
lines.append("}")
if parameters.extern_dependencies:
for dep in dependency_module_maps:
lines.append(
"extern module \"" + dep.name + "\" \"" +
parameters.leading_periods + dep.file.path + "\"",
)
return lines
def _create_module_map_action(
actions,
module_map,
private_headers,
public_headers,
textual_headers,
dependency_module_maps,
additional_exported_headers,
separate_module_headers,
compiled_module,
module_map_home_is_cwd,
generate_submodules,
extern_dependencies):
content = actions.args()
content.set_param_file_format("multiline")
segments_to_exec_path = module_map.file.path.count("/")
leading_periods = "" if module_map_home_is_cwd else "../" * segments_to_exec_path
public_headers = _cc_internal.freeze(public_headers)
private_headers = _cc_internal.freeze(private_headers)
additional_exported_headers = _cc_internal.freeze(additional_exported_headers)
separate_module_headers = _cc_internal.freeze(separate_module_headers)
data_struct = _ModuleMapInfo(
module_map = module_map,
public_headers = public_headers,
textual_headers = textual_headers,
private_headers = private_headers,
dependency_module_maps = dependency_module_maps,
additional_exported_headers = additional_exported_headers,
separate_module_headers = separate_module_headers,
compiled_module = compiled_module,
generate_submodules = generate_submodules,
extern_dependencies = extern_dependencies,
leading_periods = leading_periods,
)
content.add_all([data_struct], map_each = _module_map_struct_to_module_map_content)
# We need to add all tree artifacts to the args object directly so we they can be
# expanded in the _module_map_struct_to_module_map_content callback function.
# We don't want to do anything with them at this point, so the map_each callback should be a
# simple null function.
tree_artifacts = [h for h in private_headers if h.is_directory]
tree_artifacts += [h for h in public_headers if h.is_directory]
tree_artifacts += [h for h in textual_headers if h.is_directory]
content.add_all(tree_artifacts, map_each = lambda x: None, allow_closure = True)
actions.write(module_map.file, content = content, is_executable = True, mnemonic = "CppModuleMap")
def _init_cc_compilation_context(
# DO NOT use ctx, this is a temporary placeholder
# to avoid adding a new field to CcCompilationHelper.
# Once compile is in Starlark we can directly pass in actions here.
ctx,
binfiles_dir,
genfiles_dir,
label,
config,
quote_include_dirs,
framework_include_dirs,
system_include_dirs,
include_dirs,
feature_configuration,
public_headers_artifacts,
include_prefix,
strip_include_prefix,
non_module_map_headers,
cc_toolchain_compilation_context,
defines,
local_defines,
public_textual_headers,
private_headers_artifacts,
additional_inputs,
separate_module_headers,
generate_module_map,
generate_pic_action,
generate_no_pic_action,
module_map,
additional_exported_headers,
deps,
implementation_deps,
additional_cpp_module_maps):
# Single usage of ctx.
actions = ctx.actions
# Setup the include path; local include directories come before those inherited from deps or
# from the toolchain; in case of aliasing (same include file found on different entries),
# prefer the local include rather than the inherited one.
# Add in the roots for well-formed include names for source files and
# generated files. It is important that the execRoot (EMPTY_FRAGMENT) comes
# before the genfilesFragment to preferably pick up source files. Otherwise
# we might pick up stale generated files.
sibling_repo_layout = config.is_sibling_repository_layout()
repo_name = label.workspace_name
repo_path = repository_exec_path(repo_name, sibling_repo_layout)
gen_include_dir = _include_dir(genfiles_dir, repo_path, sibling_repo_layout)
bin_include_dir = _include_dir(binfiles_dir, repo_path, sibling_repo_layout)
quote_include_dirs_for_context = [repo_path, gen_include_dir, bin_include_dir] + quote_include_dirs
external = repo_name != "" and _enabled(feature_configuration, "external_include_paths")
shorten_virtual_includes = _enabled(feature_configuration, "shorten_virtual_includes")
external_include_dirs = []
declared_include_srcs = []
if not external and feature_configuration.is_requested("system_include_paths"):
system_include_dirs_for_context = system_include_dirs + include_dirs
include_dirs_for_context = []
elif not external:
system_include_dirs_for_context = list(system_include_dirs)
include_dirs_for_context = list(include_dirs)
else:
# Do not add system_include_dirs and include_dirs directly to compilation context.
system_include_dirs_for_context = []
include_dirs_for_context = []
external_include_dirs.append(repo_path)
external_include_dirs.append(gen_include_dir)
external_include_dirs.append(bin_include_dir)
external_include_dirs.extend(quote_include_dirs_for_context)
external_include_dirs.extend(system_include_dirs)
external_include_dirs.extend(include_dirs)
public_headers = _compute_public_headers(
actions,
config,
public_headers_artifacts,
include_prefix,
strip_include_prefix,
label,
binfiles_dir,
non_module_map_headers,
sibling_repo_layout,
shorten_virtual_includes,
)
if public_headers.virtual_include_path:
if external:
external_include_dirs.append(public_headers.virtual_include_path)
else:
include_dirs_for_context.append(public_headers.virtual_include_path)
textual_headers = _compute_public_headers(
actions,
config,
public_textual_headers,
include_prefix,
strip_include_prefix if STRIP_INCLUDE_PREFIX_APPLIES_TO_TEXTUAL_HEADERS else None,
label,
binfiles_dir,
non_module_map_headers,
sibling_repo_layout,
shorten_virtual_includes,
must_use_strip_prefix = False,
)
if textual_headers.virtual_include_path:
if external or feature_configuration.is_requested("system_include_paths"):
external_include_dirs.append(textual_headers.virtual_include_path)
else:
include_dirs_for_context.append(textual_headers.virtual_include_path)
if config.coverage_enabled:
# Populate the map only when code coverage collection is enabled, to report the actual
# source file name in the coverage output file.
virtual_to_original_headers = public_headers.virtual_to_original_headers + textual_headers.virtual_to_original_headers
else:
virtual_to_original_headers = []
declared_include_srcs.extend(public_headers.headers)
declared_include_srcs.extend(textual_headers.headers)
declared_include_srcs.extend(private_headers_artifacts)
declared_include_srcs.extend(additional_inputs)
generates_pic_header_module = _generates_header_module(feature_configuration, public_headers_artifacts, private_headers_artifacts, generate_pic_action)
generates_no_pic_header_module = _generates_header_module(feature_configuration, public_headers_artifacts, private_headers_artifacts, generate_no_pic_action)
if separate_module_headers:
if not (_enabled(feature_configuration, "module_maps") and
generate_module_map and
(generates_pic_header_module or generates_no_pic_header_module)):
fail("Should use separate headers only when building modules: " + label.name)
separate_public_headers = _compute_public_headers(
actions,
config,
separate_module_headers,
include_prefix,
strip_include_prefix,
label,
binfiles_dir,
non_module_map_headers,
sibling_repo_layout,
shorten_virtual_includes,
)
separate_module = None
separate_pic_module = None
pic_header_module = None
header_module = None
if _enabled(feature_configuration, "module_maps"):
if not module_map:
module_map = create_module_map(
file = actions.declare_file(label.name + ".cppmap"),
name = label.workspace_name + "//" + label.package + ":" + label.name,
)
# There are different modes for module compilation:
# 1. We create the module map and compile the module so that libraries depending on us can
# use the resulting module artifacts in their compilation (compiled is true).
# 2. We create the module map so that libraries depending on us will include the headers
# textually (compiled is false).
if generate_module_map:
compiled = _enabled(feature_configuration, "header_modules") or \
_enabled(feature_configuration, "compile_all_modules")
if _enabled(feature_configuration, "only_doth_headers_in_module_maps"):
public_headers_for_module_map_action = [header for header in public_headers.module_map_headers if (header.is_directory or header.extension == "h")]
textual_headers_for_module_map_action = [header for header in textual_headers.module_map_headers if (header.is_directory or header.extension == "h")]
else:
public_headers_for_module_map_action = public_headers.module_map_headers
textual_headers_for_module_map_action = textual_headers.module_map_headers
private_headers_for_module_map_action = private_headers_artifacts
if _enabled(feature_configuration, "exclude_private_headers_in_module_maps"):
private_headers_for_module_map_action = []
dependency_module_maps = _collect_module_maps(deps + implementation_deps, cc_toolchain_compilation_context, additional_cpp_module_maps)
_create_module_map_action(
actions = actions,
module_map = module_map,
public_headers = public_headers_for_module_map_action,
textual_headers = textual_headers_for_module_map_action,
separate_module_headers = separate_public_headers.module_map_headers,
dependency_module_maps = dependency_module_maps,
private_headers = private_headers_for_module_map_action,
additional_exported_headers = additional_exported_headers,
compiled_module = compiled,
module_map_home_is_cwd = _enabled(feature_configuration, "module_map_home_cwd"),
generate_submodules = _enabled(feature_configuration, "generate_submodules"),
extern_dependencies = not _enabled(feature_configuration, "module_map_without_extern_module"),
)
if generates_pic_header_module:
pic_header_module = _header_module_artifact(
actions,
label,
sibling_repo_layout,
"",
".pic.pcm",
)
if generates_no_pic_header_module:
header_module = _header_module_artifact(
actions,
label,
sibling_repo_layout,
"",
".pcm",
)
if separate_module_headers:
declared_include_srcs.extend(separate_public_headers.headers)
if generates_no_pic_header_module:
separate_module = _header_module_artifact(
actions,
label,
sibling_repo_layout,
".sep",
".pcm",
)
if generates_pic_header_module:
separate_pic_module = _header_module_artifact(
actions,
label,
sibling_repo_layout,
".sep",
".pic.pcm",
)
else:
# Do not set module map related attributes.
module_map = None
dependent_cc_compilation_contexts = []
if cc_toolchain_compilation_context != None:
dependent_cc_compilation_contexts.append(cc_toolchain_compilation_context)
dependent_cc_compilation_contexts.extend(deps)
main_context = create_compilation_context(
quote_includes = depset(quote_include_dirs_for_context),
framework_includes = depset(framework_include_dirs),
external_includes = depset(external_include_dirs),
system_includes = depset(system_include_dirs_for_context),
includes = depset(include_dirs_for_context),
virtual_to_original_headers = depset(virtual_to_original_headers),
dependent_cc_compilation_contexts = dependent_cc_compilation_contexts,
non_code_inputs = additional_inputs,
defines = depset(defines),
local_defines = depset(local_defines),
headers = depset(declared_include_srcs),
direct_public_headers = public_headers.headers,
direct_private_headers = private_headers_artifacts,
direct_textual_headers = textual_headers.headers,
module_map = module_map,
pic_header_module = pic_header_module,
header_module = header_module,
separate_module_headers = separate_public_headers.headers,
separate_module = separate_module,
separate_pic_module = separate_pic_module,
add_public_headers_to_modular_headers = False,
exported_dependent_cc_compilation_contexts = [],
)
implementation_deps_context = None
if implementation_deps:
implementation_deps_context = create_compilation_context(
quote_includes = depset(quote_include_dirs_for_context),
framework_includes = depset(framework_include_dirs),
external_includes = depset(external_include_dirs),
system_includes = depset(system_include_dirs_for_context),
includes = depset(include_dirs_for_context),
virtual_to_original_headers = depset(virtual_to_original_headers),
dependent_cc_compilation_contexts = dependent_cc_compilation_contexts + implementation_deps,
non_code_inputs = additional_inputs,
defines = depset(defines),
local_defines = depset(local_defines),
headers = depset(declared_include_srcs),
direct_public_headers = public_headers.headers,
direct_private_headers = private_headers_artifacts,
direct_textual_headers = textual_headers.headers,
module_map = module_map,
pic_header_module = pic_header_module,
header_module = header_module,
separate_module_headers = separate_public_headers.headers,
separate_module = separate_module,
separate_pic_module = separate_pic_module,
add_public_headers_to_modular_headers = False,
exported_dependent_cc_compilation_contexts = [],
)
return main_context, implementation_deps_context
# buildifier: disable=function-docstring
def dotd_files_enabled(language, cpp_configuration, feature_configuration):
# TODO: b/396122076 - migrate callers of cc_common.compile() to request the
# right fragment(s) and drop this API from cpp_semantics
if language == None or language == "cpp":
enabled_in_config = cpp_configuration.should_generate_dotd_files()
else: # objc
enabled_in_config = cpp_configuration.objc_should_generate_dotd_files()
return (
enabled_in_config and
not feature_configuration.is_enabled("parse_showincludes") and
not feature_configuration.is_enabled("no_dotd_file")
)
# buildifier: disable=function-docstring
def serialized_diagnostics_file_enabled(feature_configuration):
return feature_configuration.is_enabled("serialized_diagnostics_file")
cc_compilation_helper = struct(
init_cc_compilation_context = _init_cc_compilation_context,
)