Vendor dependencies

Let's see how I like this workflow.
This commit is contained in:
John Doty 2022-12-19 08:27:18 -08:00
parent 34d1830413
commit 9c435dc440
7500 changed files with 1665121 additions and 99 deletions

View file

@ -0,0 +1,106 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//linking:link_info.bzl", "Archive")
load("@prelude//utils:utils.bzl", "value_or")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
def _archive_flags(
archiver_type: str.type,
linker_type: str.type,
use_archiver_flags: bool.type,
thin: bool.type) -> [str.type]:
if not use_archiver_flags:
return []
if archiver_type == "windows":
if thin:
fail("'windows' archiver doesn't support thin archives")
return ["/Brepro", "/d2threads1"]
elif archiver_type == "windows_clang":
return ["/llvmlibthin"] if thin else []
flags = ""
# Operate in quick append mode, so that objects with identical basenames
# won't overwrite one another.
flags += "q"
# Suppress warning about creating a new archive.
flags += "c"
# Run ranlib to generate symbol index for faster linking.
flags += "s"
# Generate thin archives.
if thin:
flags += "T"
# GNU archivers support generating deterministic archives.
if linker_type == "gnu":
flags += "D"
return [flags]
# Create a static library from a list of object files.
def _archive(ctx: "context", name: str.type, args: "cmd_args", thin: bool.type, prefer_local: bool.type) -> "artifact":
archive_output = ctx.actions.declare_output(name)
toolchain = get_cxx_toolchain_info(ctx)
command = cmd_args(toolchain.linker_info.archiver)
archiver_type = toolchain.linker_info.archiver_type
command.add(_archive_flags(
archiver_type,
toolchain.linker_info.type,
toolchain.linker_info.use_archiver_flags,
thin,
))
if archiver_type == "windows" or archiver_type == "windows_clang":
command.add([cmd_args(archive_output.as_output(), format = "/OUT:{}")])
else:
command.add([archive_output.as_output()])
if toolchain.linker_info.archiver_supports_argfiles:
shell_quoted_args = cmd_args(args, quote = "shell")
argfile, _ = ctx.actions.write(name + ".argsfile", shell_quoted_args, allow_args = True)
command.hidden([shell_quoted_args])
command.add(cmd_args(["@", argfile], delimiter = ""))
else:
command.add(args)
category = "archive"
if thin:
category = "archive_thin"
ctx.actions.run(command, category = category, identifier = name, prefer_local = prefer_local)
return archive_output
def _archive_locally(ctx: "context", linker_info: "LinkerInfo") -> bool.type:
archive_locally = linker_info.archive_objects_locally
if hasattr(ctx.attrs, "_archive_objects_locally_override"):
return value_or(ctx.attrs._archive_objects_locally_override, archive_locally)
return archive_locally
# Creates a static library given a list of object files.
def make_archive(
ctx: "context",
name: str.type,
objects: ["artifact"],
args: ["cmd_args", None] = None) -> Archive.type:
if len(objects) == 0:
fail("no objects to archive")
if args == None:
args = cmd_args(objects)
linker_info = get_cxx_toolchain_info(ctx).linker_info
thin = linker_info.archive_contents == "thin"
archive = _archive(ctx, name, args, thin = thin, prefer_local = _archive_locally(ctx, linker_info))
# TODO(T110378125): use argsfiles for GNU archiver for long lists of objects.
# TODO(T110378123): for BSD archiver, split long args over multiple invocations.
# TODO(T110378100): We need to scrub the static library (timestamps, permissions, etc) as those are
# sources of non-determinisim. See `ObjectFileScrubbers.createDateUidGidScrubber()` in Buck v1.
return Archive(artifact = archive, external_objects = objects if thin else [])

View file

@ -0,0 +1,63 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
def cxx_by_language_ext(x: {"": ""}, ext: str.type) -> [""]:
# lang_preprocessor_flags is indexed by c/cxx
# lang_compiler_flags is indexed by c_cpp_output/cxx_cpp_output
# so write a function that can do either
#
# === Buck v1 Compatibility ===
#
# `lang_compiler_flags` keys are coerced to CxxSource.Type,
# so the allowable values are the lowercase versions of the enum values.
#
# The keys themselves should be the _output_ type of the language. For example,
# for Obj-C, that would be OBJC_CPP_OUTPUT.
#
# The actual lookup for `lang_compiler_flags` happens in
# CxxSourceRuleFactory::getRuleCompileFlags().
#
# `lang_preprocessor_flags` keys are also coerced to CxxSource.Type.
# The keys are the _input_ type of the language. For example, for Obj-C,
# that would be OBJC.
if ext == ".c":
key_pp = "c"
# TODO(gabrielrc): v1 docs have other keys
# https://buck.build/rule/cxx_library.html#lang_compiler_flags
# And you can see them in java code, but somehow it works with
# this one, which is seem across the repo. Find out what's happening.
key_compiler = "c_cpp_output"
elif ext in (".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp"):
key_pp = "cxx"
key_compiler = "cxx_cpp_output"
elif ext == ".m":
key_pp = "objc"
key_compiler = "objc_cpp_output"
elif ext == ".mm":
key_pp = "objcxx"
key_compiler = "objcxx_cpp_output"
elif ext in (".s", ".S"):
key_pp = "assembler_with_cpp"
key_compiler = "assembler"
elif ext == ".cu":
key_pp = "cuda"
key_compiler = "cuda_cpp_output"
elif ext == ".hip":
key_pp = "hip"
key_compiler = "hip_cpp_output"
elif ext in (".asm", ".asmpp"):
key_pp = "asm_with_cpp"
key_compiler = "asm"
else:
fail("Unexpected file extension: " + ext)
res = []
if key_pp in x:
res += x[key_pp]
if key_compiler in x:
res += x[key_compiler]
return res

View file

@ -0,0 +1,65 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:paths.bzl", "paths")
load(
":compile.bzl",
"CxxSrcCompileCommand", # @unused Used as a type
)
load(":cxx_context.bzl", "get_cxx_toolchain_info")
# Provider that exposes the compilation database information
CxxCompilationDbInfo = provider(fields = {
"info": "A map of the file (an \"artifact.type\") to its corresponding \"CxxSrcCompileCommand\"",
"platform": "platform for this compilation database",
"toolchain": "toolchain for this compilation database",
})
def make_compilation_db_info(src_compile_cmds: [CxxSrcCompileCommand.type], toolchainInfo: "CxxToolchainInfo", platformInfo: "CxxPlatformInfo") -> CxxCompilationDbInfo.type:
info = {}
for src_compile_cmd in src_compile_cmds:
info.update({src_compile_cmd.src: src_compile_cmd})
return CxxCompilationDbInfo(info = info, toolchain = toolchainInfo, platform = platformInfo)
def create_compilation_database(
ctx: "context",
src_compile_cmds: [CxxSrcCompileCommand.type]) -> DefaultInfo.type:
mk_comp_db = get_cxx_toolchain_info(ctx).mk_comp_db[RunInfo]
# Generate the per-source compilation DB entries.
entries = {}
other_outputs = []
for src_compile_cmd in src_compile_cmds:
cdb_path = paths.join("__comp_db__", src_compile_cmd.src.short_path + ".comp_db.json")
if cdb_path not in entries:
entry = ctx.actions.declare_output(cdb_path)
cmd = cmd_args(mk_comp_db)
cmd.add("gen")
cmd.add(cmd_args(entry.as_output(), format = "--output={}"))
cmd.add(src_compile_cmd.src.basename)
cmd.add(cmd_args(src_compile_cmd.src).parent())
cmd.add("--")
cmd.add(src_compile_cmd.cxx_compile_cmd.base_compile_cmd)
cmd.add(src_compile_cmd.cxx_compile_cmd.argsfile.cmd_form)
cmd.add(src_compile_cmd.args)
ctx.actions.run(cmd, category = "cxx_compilation_database", identifier = src_compile_cmd.src.short_path)
# Add all inputs the command uses to runtime files.
other_outputs.append(cmd)
entries[cdb_path] = entry
# Merge all entries into the actual compilation DB.
db = ctx.actions.declare_output("compile_commands.json")
cmd = cmd_args(mk_comp_db)
cmd.add("merge")
cmd.add(cmd_args(db.as_output(), format = "--output={}"))
cmd.add(entries.values())
ctx.actions.run(cmd, category = "cxx_compilation_database_merge")
return DefaultInfo(default_outputs = [db], other_outputs = other_outputs)

View file

@ -0,0 +1,448 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:paths.bzl", "paths")
load("@prelude//linking:lto.bzl", "LtoMode")
load(
"@prelude//utils:utils.bzl",
"flatten",
)
load(":attr_selection.bzl", "cxx_by_language_ext")
load(
":compiler.bzl",
"get_flags_for_colorful_output",
"get_flags_for_reproducible_build",
"get_headers_dep_files_flags_factory",
"get_output_flags",
"get_pic_flags",
)
load(":cxx_context.bzl", "get_cxx_toolchain_info")
load(":debug.bzl", "SplitDebugMode")
load(
":headers.bzl",
"CPrecompiledHeaderInfo",
)
load(":platform.bzl", "cxx_by_platform")
load(
":preprocessor.bzl",
"CPreprocessor", # @unused Used as a type
"CPreprocessorInfo", # @unused Used as a type
"cxx_attr_preprocessor_flags",
"cxx_merge_cpreprocessors",
)
# Supported Cxx file extensions
CxxExtension = enum(
".cpp",
".cc",
".cxx",
".c++",
".c",
".s",
".S",
".m",
".mm",
".cu",
".hip",
".asm",
".asmpp",
".h",
".hpp",
)
# Information on argsfiles created for Cxx compilation.
_CxxCompileArgsfile = record(
# The generated argsfile
file = field("artifact"),
# This argfile as a command form that would use the argfile
cmd_form = field("cmd_args"),
# The args that was written to the argfile
argfile_args = field("cmd_args"),
# The args in their prisitine form without shell quoting
args = field("cmd_args"),
# Hidden args necessary for the argsfile to reference
hidden_args = field([["artifacts", "cmd_args"]]),
)
_HeadersDepFiles = record(
# An executable to wrap the actual command with for post-processing of dep
# files into the format that Buck2 recognizes (i.e. one artifact per line).
processor = field("cmd_args"),
# The tag that was added to headers.
tag = field("artifact_tag"),
# A function that produces new cmd_args to append to the compile command to
# get it to emit the dep file. This will receive the output dep file as an
# input.
mk_flags = field("function"),
)
# Information about how to compile a source file of particular extension.
_CxxCompileCommand = record(
# The compiler and any args which are independent of the rule.
base_compile_cmd = field("cmd_args"),
# The argsfile of arguments from the rule and it's dependencies.
argsfile = field(_CxxCompileArgsfile.type),
headers_dep_files = field([_HeadersDepFiles.type, None]),
compiler_type = field(str.type),
)
# Information about how to compile a source file.
CxxSrcCompileCommand = record(
# Source file to compile.
src = field("artifact"),
# If we have multiple source entries with same files but different flags,
# specify an index so we can differentiate them. Otherwise, use None.
index = field(["int", None], None),
# The CxxCompileCommand to use to compile this file.
cxx_compile_cmd = field(_CxxCompileCommand.type),
# Arguments specific to the source file.
args = field(["_arg"]),
)
# Output of creating compile commands for Cxx source files.
CxxCompileCommandOutput = record(
# List of compile commands for each source file
src_compile_cmds = field([CxxSrcCompileCommand.type]),
# Argsfiles to generate in order to compile these source files
argsfiles_info = field(DefaultInfo.type),
# Each argsfile by the file extension for which it is used
argsfile_by_ext = field({str.type: "artifact"}),
)
# Output of creating compile commands for Cxx source files.
CxxCompileCommandOutputForCompDb = record(
# Output of creating compile commands for Cxx source files.
source_commands = field(CxxCompileCommandOutput.type),
# this field is only to be used in CDB generation
comp_db_commands = field(CxxCompileCommandOutput.type),
)
# An input to cxx compilation, consisting of a file to compile and optional
# file specific flags to compile with.
CxxSrcWithFlags = record(
file = field("artifact"),
flags = field(["resolved_macro"], []),
# If we have multiple source entries with same files but different flags,
# specify an index so we can differentiate them. Otherwise, use None.
index = field(["int", None], None),
)
CxxCompileOutput = record(
# The compiled `.o` file.
object = field("artifact"),
object_has_external_debug_info = field(bool.type, False),
# Externally referenced debug info, which doesn't get linked with the
# object (e.g. the above `.o` when using `-gsplit-dwarf=single` or the
# the `.dwo` when using `-gsplit-dwarf=split`).
external_debug_info = field(["artifact", None], None),
)
def create_compile_cmds(
ctx: "context",
impl_params: "CxxRuleConstructorParams",
own_preprocessors: [CPreprocessor.type],
inherited_preprocessor_infos: [CPreprocessorInfo.type]) -> CxxCompileCommandOutputForCompDb.type:
"""
Forms the CxxSrcCompileCommand to use for each source file based on it's extension
and optional source file flags. Returns CxxCompileCommandOutput containing an array
of the generated compile commands and argsfile output.
"""
srcs_with_flags = []
for src in impl_params.srcs:
srcs_with_flags.append(src)
header_only = False
if len(srcs_with_flags) == 0 and len(impl_params.additional.srcs) == 0:
all_headers = flatten([x.headers for x in own_preprocessors])
if len(all_headers) == 0:
all_raw_headers = flatten([x.raw_headers for x in own_preprocessors])
if len(all_raw_headers) != 0:
header_only = True
for header in all_raw_headers:
if header.extension in [".h", ".hpp"]:
srcs_with_flags.append(CxxSrcWithFlags(file = header))
else:
return CxxCompileCommandOutputForCompDb(
source_commands = CxxCompileCommandOutput(src_compile_cmds = [], argsfiles_info = DefaultInfo(), argsfile_by_ext = {}),
comp_db_commands = CxxCompileCommandOutput(src_compile_cmds = [], argsfiles_info = DefaultInfo(), argsfile_by_ext = {}),
)
else:
header_only = True
for header in all_headers:
if header.artifact.extension in [".h", ".hpp"]:
srcs_with_flags.append(CxxSrcWithFlags(file = header.artifact))
# TODO(T110378129): Buck v1 validates *all* headers used by a compilation
# at compile time, but that doing that here/eagerly might be expensive (but
# we should figure out something).
_validate_target_headers(ctx, own_preprocessors)
# Combine all preprocessor info and prepare it for compilations.
pre = cxx_merge_cpreprocessors(
ctx,
filter(None, own_preprocessors + impl_params.extra_preprocessors),
inherited_preprocessor_infos,
)
headers_tag = ctx.actions.artifact_tag()
src_compile_cmds = []
cxx_compile_cmd_by_ext = {}
argsfile_by_ext = {}
for src in srcs_with_flags:
ext = CxxExtension(src.file.extension)
# Deduplicate shared arguments to save memory. If we compile multiple files
# of the same extension they will have some of the same flags. Save on
# allocations by caching and reusing these objects.
if not ext in cxx_compile_cmd_by_ext:
toolchain = get_cxx_toolchain_info(ctx)
compiler_info = _get_compiler_info(toolchain, ext)
base_compile_cmd = _get_compile_base(compiler_info)
headers_dep_files = None
if _supports_dep_files(ext) and toolchain.use_dep_files:
mk_dep_files_flags = get_headers_dep_files_flags_factory(compiler_info.compiler_type)
if mk_dep_files_flags:
headers_dep_files = _HeadersDepFiles(
processor = cmd_args(compiler_info.dep_files_processor),
mk_flags = mk_dep_files_flags,
tag = headers_tag,
)
argsfile_by_ext[ext.value] = _mk_argsfile(ctx, compiler_info, pre, ext, headers_tag)
cxx_compile_cmd_by_ext[ext] = _CxxCompileCommand(
base_compile_cmd = base_compile_cmd,
argsfile = argsfile_by_ext[ext.value],
headers_dep_files = headers_dep_files,
compiler_type = compiler_info.compiler_type,
)
cxx_compile_cmd = cxx_compile_cmd_by_ext[ext]
src_args = []
src_args.extend(src.flags)
src_args.extend(["-c", src.file])
src_compile_command = CxxSrcCompileCommand(src = src.file, cxx_compile_cmd = cxx_compile_cmd, args = src_args, index = src.index)
src_compile_cmds.append(src_compile_command)
# Create an output file of all the argsfiles generated for compiling these source files.
argsfiles = []
argsfile_names = cmd_args()
other_outputs = []
argsfile_artifacts_by_ext = {}
for ext, argsfile in argsfile_by_ext.items():
argsfiles.append(argsfile.file)
argsfile_names.add(cmd_args(argsfile.file).ignore_artifacts())
other_outputs.extend(argsfile.hidden_args)
argsfile_artifacts_by_ext[ext] = argsfile.file
for argsfile in impl_params.additional.argsfiles:
argsfiles.append(argsfile.file)
argsfile_names.add(cmd_args(argsfile.file).ignore_artifacts())
other_outputs.extend(argsfile.hidden_args)
argsfiles_summary = ctx.actions.write("argsfiles", argsfile_names)
# Create a provider that will output all the argsfiles necessary and generate those argsfiles.
argsfiles = DefaultInfo(default_outputs = [argsfiles_summary] + argsfiles, other_outputs = other_outputs)
if header_only:
return CxxCompileCommandOutputForCompDb(
source_commands = CxxCompileCommandOutput(src_compile_cmds = [], argsfiles_info = DefaultInfo(), argsfile_by_ext = {}),
comp_db_commands = CxxCompileCommandOutput(src_compile_cmds = src_compile_cmds, argsfiles_info = argsfiles, argsfile_by_ext = argsfile_artifacts_by_ext),
)
else:
return CxxCompileCommandOutputForCompDb(
source_commands = CxxCompileCommandOutput(src_compile_cmds = src_compile_cmds, argsfiles_info = argsfiles, argsfile_by_ext = argsfile_artifacts_by_ext),
comp_db_commands = CxxCompileCommandOutput(src_compile_cmds = src_compile_cmds, argsfiles_info = argsfiles, argsfile_by_ext = argsfile_artifacts_by_ext),
)
def compile_cxx(
ctx: "context",
src_compile_cmds: [CxxSrcCompileCommand.type],
pic: bool.type = False) -> [CxxCompileOutput.type]:
"""
For a given list of src_compile_cmds, generate output artifacts.
"""
toolchain = get_cxx_toolchain_info(ctx)
linker_info = toolchain.linker_info
objects = []
for src_compile_cmd in src_compile_cmds:
identifier = src_compile_cmd.src.short_path
if src_compile_cmd.index != None:
# Add a unique postfix if we have duplicate source files with different flags
identifier = identifier + "_" + str(src_compile_cmd.index)
filename_base = identifier + (".pic" if pic else "")
object = ctx.actions.declare_output(
paths.join("__objects__", "{}.{}".format(filename_base, linker_info.object_file_extension)),
)
cmd = cmd_args(src_compile_cmd.cxx_compile_cmd.base_compile_cmd)
compiler_type = src_compile_cmd.cxx_compile_cmd.compiler_type
cmd.add(get_output_flags(compiler_type, object))
args = cmd_args()
if pic:
args.add(get_pic_flags(compiler_type))
args.add(src_compile_cmd.cxx_compile_cmd.argsfile.cmd_form)
args.add(src_compile_cmd.args)
cmd.add(args)
action_dep_files = {}
headers_dep_files = src_compile_cmd.cxx_compile_cmd.headers_dep_files
if headers_dep_files:
intermediary_dep_file = ctx.actions.declare_output(
paths.join("__dep_files_intermediaries__", filename_base),
).as_output()
dep_file = ctx.actions.declare_output(
paths.join("__dep_files__", filename_base),
).as_output()
dep_file_flags = headers_dep_files.mk_flags(intermediary_dep_file)
cmd.add(dep_file_flags)
# API: First argument is the dep file source path, second is the
# dep file destination path, other arguments are the actual compile
# command.
cmd = cmd_args([
headers_dep_files.processor,
intermediary_dep_file,
headers_dep_files.tag.tag_artifacts(dep_file),
cmd,
])
action_dep_files["headers"] = headers_dep_files.tag
if pic:
identifier += " (pic)"
ctx.actions.run(cmd, category = "cxx_compile", identifier = identifier, dep_files = action_dep_files)
# If we're building with split debugging, where the debug info is in the
# original object, then add the object as external debug info, *unless*
# we're doing LTO, which generates debug info at link time (*except* for
# fat LTO, which still generates native code and, therefore, debug info).
object_has_external_debug_info = (
toolchain.split_debug_mode == SplitDebugMode("single") and
linker_info.lto_mode in (LtoMode("none"), LtoMode("fat"))
)
objects.append(CxxCompileOutput(
object = object,
object_has_external_debug_info = object_has_external_debug_info,
))
return objects
def _validate_target_headers(ctx: "context", preprocessor: [CPreprocessor.type]):
path_to_artifact = {}
all_headers = flatten([x.headers for x in preprocessor])
for header in all_headers:
header_path = paths.join(header.namespace, header.name)
artifact = path_to_artifact.get(header_path)
if artifact != None:
if artifact != header.artifact:
fail("Conflicting headers {} and {} map to {} in target {}".format(artifact, header.artifact, header_path, ctx.label))
else:
path_to_artifact[header_path] = header.artifact
def _get_compiler_info(toolchain: "CxxToolchainInfo", ext: CxxExtension.type) -> "_compiler_info":
if ext.value in (".cpp", ".cc", ".mm", ".cxx", ".c++", ".h", ".hpp"):
return toolchain.cxx_compiler_info
elif ext.value in (".c", ".m"):
return toolchain.c_compiler_info
elif ext.value in (".s", ".S"):
return toolchain.as_compiler_info
elif ext.value == ".cu":
return toolchain.cuda_compiler_info
elif ext.value == ".hip":
return toolchain.hip_compiler_info
elif ext.value in (".asm", ".asmpp"):
return toolchain.asm_compiler_info
else:
# This should be unreachable as long as we handle all enum values
fail("Unknown C++ extension: " + ext.value)
def _get_compile_base(compiler_info: "_compiler_info") -> "cmd_args":
"""
Given a compiler info returned by _get_compiler_info, form the base compile args.
"""
cmd = cmd_args(compiler_info.compiler)
return cmd
def _supports_dep_files(ext: CxxExtension.type) -> bool.type:
# Raw assembly doesn't make sense to capture dep files for.
if ext.value in (".s", ".S", ".asm"):
return False
elif ext.value == ".hip":
# TODO (T118797886): HipCompilerInfo doesn't have dep files processor.
# Should it?
return False
return True
def _add_compiler_info_flags(compiler_info: "_compiler_info", ext: CxxExtension.type, cmd: "cmd_args"):
cmd.add(compiler_info.preprocessor_flags or [])
cmd.add(compiler_info.compiler_flags or [])
cmd.add(get_flags_for_reproducible_build(compiler_info.compiler_type))
if ext.value not in (".asm", ".asmpp"):
# Clang's asm compiler doesn't support colorful output, so we skip this there.
cmd.add(get_flags_for_colorful_output(compiler_info.compiler_type))
def _mk_argsfile(ctx: "context", compiler_info: "_compiler_info", preprocessor: CPreprocessorInfo.type, ext: CxxExtension.type, headers_tag: "artifact_tag") -> _CxxCompileArgsfile.type:
"""
Generate and return an {ext}.argsfile artifact and command args that utilize the argsfile.
"""
args = cmd_args()
_add_compiler_info_flags(compiler_info, ext, args)
args.add(headers_tag.tag_artifacts(preprocessor.set.project_as_args("args")))
# Different preprocessors will contain whether to use modules,
# and the modulemap to use, so we need to get the final outcome.
if preprocessor.set.reduce("uses_modules"):
args.add(headers_tag.tag_artifacts(preprocessor.set.project_as_args("modular_args")))
args.add(cxx_attr_preprocessor_flags(ctx, ext.value))
args.add(_attr_compiler_flags(ctx, ext.value))
args.add(headers_tag.tag_artifacts(preprocessor.set.project_as_args("include_dirs")))
# Workaround as that's not precompiled, but working just as prefix header.
# Another thing is that it's clang specific, should be generalized.
if ctx.attrs.precompiled_header != None:
args.add(["-include", headers_tag.tag_artifacts(ctx.attrs.precompiled_header[CPrecompiledHeaderInfo].header)])
if ctx.attrs.prefix_header != None:
args.add(["-include", headers_tag.tag_artifacts(ctx.attrs.prefix_header)])
shell_quoted_args = cmd_args(args, quote = "shell")
argfile, _ = ctx.actions.write(ext.value + ".argsfile", shell_quoted_args, allow_args = True)
hidden_args = [args]
cmd_form = cmd_args(argfile, format = "@{}").hidden(hidden_args)
return _CxxCompileArgsfile(file = argfile, cmd_form = cmd_form, argfile_args = shell_quoted_args, args = args, hidden_args = hidden_args)
def _attr_compiler_flags(ctx: "context", ext: str.type) -> [""]:
return (
ctx.attrs.compiler_flags +
cxx_by_language_ext(ctx.attrs.lang_compiler_flags, ext) +
flatten(cxx_by_platform(ctx, ctx.attrs.platform_compiler_flags)) +
flatten(cxx_by_platform(ctx, cxx_by_language_ext(ctx.attrs.lang_platform_compiler_flags, ext)))
)

View file

@ -0,0 +1,68 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
# TODO(T110378132): Added here for compat with v1, but this might make more
# sense on the toolchain definition.
def get_flags_for_reproducible_build(compiler_type: str.type) -> [str.type]:
"""
Return flags needed to make compilations reproducible (e.g. avoiding
embedding the working directory into debug info.
"""
flags = []
if compiler_type in ["clang_cl", "windows"]:
flags.append("/Brepro")
if compiler_type in ["clang", "clang_windows", "clang_cl"]:
flags.extend(["-Xclang", "-fdebug-compilation-dir", "-Xclang", "."])
if compiler_type == "clang_windows":
flags.append("-mno-incremental-linker-compatible")
return flags
def get_flags_for_colorful_output(compiler_type: str.type) -> [str.type]:
"""
Return flags for enabling colorful diagnostic output.
"""
flags = []
if compiler_type in ["clang", "clang_windows", "clang_cl"]:
# https://clang.llvm.org/docs/UsersManual.html
flags.append("-fcolor-diagnostics")
elif compiler_type == "gcc":
# https://gcc.gnu.org/onlinedocs/gcc/Diagnostic-Message-Formatting-Options.html
flags.append("-fdiagnostics-color=always")
return flags
def cc_dep_files(output: "_arglike") -> cmd_args.type:
return cmd_args(["-MD", "-MF", output])
def windows_cc_dep_files(_output: "_arglike") -> cmd_args.type:
return cmd_args(["/showIncludes"])
def get_headers_dep_files_flags_factory(compiler_type: str.type) -> ["function", None]:
if compiler_type in ["clang", "gcc", "clang_windows"]:
return cc_dep_files
if compiler_type in ["windows", "clang_cl"]:
return windows_cc_dep_files
return None
def get_pic_flags(compiler_type: str.type) -> [str.type]:
if compiler_type in ["clang", "gcc"]:
return ["-fPIC"]
else:
return []
def get_output_flags(compiler_type: str.type, output: "artifact") -> [""]:
if compiler_type in ["windows", "clang_cl", "windows_ml64"]:
return [cmd_args(output.as_output(), format = "/Fo{}")]
else:
return ["-o", output.as_output()]

View file

@ -0,0 +1,608 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//cxx:link_groups.bzl",
"LinkGroupInfo", # @unused Used as a type
)
load(
"@prelude//linking:link_groups.bzl",
"merge_link_group_lib_info",
)
load(
"@prelude//linking:link_info.bzl",
"Archive",
"ArchiveLinkable",
"LinkArgs",
"LinkInfo",
"LinkInfos",
"LinkStyle",
"Linkage",
"LinkedObject",
"SharedLibLinkable",
"create_merged_link_info",
"get_actual_link_style",
"get_link_args",
"get_link_styles_for_linkage",
)
load(
"@prelude//linking:linkable_graph.bzl",
"AnnotatedLinkableRoot",
"LinkableGraph",
"create_linkable_graph",
"create_linkable_graph_node",
"create_linkable_node",
)
load("@prelude//linking:shared_libraries.bzl", "SharedLibraryInfo", "create_shared_libraries", "merge_shared_libraries")
load(
"@prelude//tests:re_utils.bzl",
"get_re_executor_from_props",
)
load(
"@prelude//utils:utils.bzl",
"expect",
"filter_and_map_idx",
"flatten",
"value_or",
)
load("@prelude//test/inject_test_run_info.bzl", "inject_test_run_info")
load(
":compile.bzl",
"CxxSrcWithFlags", # @unused Used as a type
)
load(":cxx_context.bzl", "get_cxx_toolchain_info")
load(":cxx_executable.bzl", "cxx_executable")
load(":cxx_library.bzl", "cxx_library_parameterized")
load(
":cxx_library_utility.bzl",
"cxx_attr_deps",
"cxx_attr_exported_deps",
"cxx_attr_exported_linker_flags",
"cxx_attr_exported_post_linker_flags",
"cxx_attr_preferred_linkage",
"cxx_inherited_link_info",
"cxx_mk_shlib_intf",
"cxx_platform_supported",
"cxx_use_shlib_intfs",
)
load(
":cxx_types.bzl",
"CxxRuleConstructorParams",
"CxxRuleProviderParams",
"CxxRuleSubTargetParams",
)
load(
":groups.bzl",
"Group", # @unused Used as a type
"MATCH_ALL_LABEL",
"NO_MATCH_LABEL",
)
load(
":headers.bzl",
"CPrecompiledHeaderInfo",
"cxx_get_regular_cxx_headers_layout",
)
load(
":link.bzl",
_cxx_link_into_shared_library = "cxx_link_into_shared_library",
)
load(
":link_groups.bzl",
"LinkGroupLibSpec",
"get_link_group_info",
)
load(
":linker.bzl",
"get_link_whole_args",
"get_shared_library_name",
)
load(
":omnibus.bzl",
"create_linkable_root",
"is_known_omnibus_root",
)
load(":platform.bzl", "cxx_by_platform")
load(
":preprocessor.bzl",
"CPreprocessor",
"cxx_attr_exported_preprocessor_flags",
"cxx_exported_preprocessor_info",
"cxx_inherited_preprocessor_infos",
"cxx_merge_cpreprocessors",
)
cxx_link_into_shared_library = _cxx_link_into_shared_library
#####################################################################
# Attributes
# The source files
def get_srcs_with_flags(ctx: "context") -> [CxxSrcWithFlags.type]:
all_srcs = ctx.attrs.srcs + flatten(cxx_by_platform(ctx, ctx.attrs.platform_srcs))
# src -> flags_hash -> flags
flags_sets_by_src = {}
for x in all_srcs:
if type(x) == type(()):
artifact = x[0]
flags = x[1]
else:
artifact = x
flags = []
flags_hash = hash(str(flags))
flag_sets = flags_sets_by_src.setdefault(artifact, {})
flag_sets[flags_hash] = flags
# Go through collected (source, flags) pair and set the index field if there are duplicate source files
cxx_src_with_flags_records = []
for (artifact, flag_sets) in flags_sets_by_src.items():
needs_indices = len(flag_sets) > 1
for i, flags in enumerate(flag_sets.values()):
index = i if needs_indices else None
cxx_src_with_flags_records.append(CxxSrcWithFlags(file = artifact, flags = flags, index = index))
return cxx_src_with_flags_records
#####################################################################
# Operations
def _get_shared_link_style_sub_targets_and_providers(
link_style: LinkStyle.type,
_ctx: "context",
_executable: "artifact",
_external_debug_info: ["_arglike"],
dwp: ["artifact", None]) -> ({str.type: ["provider"]}, ["provider"]):
if link_style != LinkStyle("shared") or dwp == None:
return ({}, [])
return ({"dwp": [DefaultInfo(default_outputs = [dwp])]}, [])
def cxx_library_impl(ctx: "context") -> ["provider"]:
if ctx.attrs.can_be_asset and ctx.attrs.used_by_wrap_script:
fail("Cannot use `can_be_asset` and `used_by_wrap_script` in the same rule")
if ctx.attrs._is_building_android_binary:
sub_target_params, provider_params = _get_params_for_android_binary_cxx_library()
else:
sub_target_params = CxxRuleSubTargetParams()
provider_params = CxxRuleProviderParams()
params = CxxRuleConstructorParams(
rule_type = "cxx_library",
headers_layout = cxx_get_regular_cxx_headers_layout(ctx),
srcs = get_srcs_with_flags(ctx),
link_style_sub_targets_and_providers_factory = _get_shared_link_style_sub_targets_and_providers,
is_omnibus_root = is_known_omnibus_root(ctx),
force_emit_omnibus_shared_root = ctx.attrs.force_emit_omnibus_shared_root,
generate_sub_targets = sub_target_params,
generate_providers = provider_params,
)
output = cxx_library_parameterized(ctx, params)
return output.providers
def _only_shared_mappings(group: Group.type) -> bool.type:
"""
Return whether this group only has explicit "shared" linkage mappings,
which indicates a group that re-uses pre-linked libs.
"""
for mapping in group.mappings:
if mapping.preferred_linkage != Linkage("shared"):
return False
return True
def get_cxx_auto_link_group_specs(ctx: "context", link_group_info: [LinkGroupInfo.type, None]) -> [[LinkGroupLibSpec.type], None]:
if link_group_info == None or not ctx.attrs.auto_link_groups:
return None
specs = []
linker_info = get_cxx_toolchain_info(ctx).linker_info
for group in link_group_info.groups:
if group.name in (MATCH_ALL_LABEL, NO_MATCH_LABEL):
continue
# TODO(agallagher): We should probably add proper handling for "provided"
# system handling to avoid needing this special case.
if _only_shared_mappings(group):
continue
specs.append(
LinkGroupLibSpec(
name = get_shared_library_name(linker_info, group.name),
is_shared_lib = True,
group = group,
),
)
return specs
def cxx_binary_impl(ctx: "context") -> ["provider"]:
link_group_info = get_link_group_info(ctx, filter_and_map_idx(LinkableGraph, cxx_attr_deps(ctx)))
params = CxxRuleConstructorParams(
rule_type = "cxx_binary",
headers_layout = cxx_get_regular_cxx_headers_layout(ctx),
srcs = get_srcs_with_flags(ctx),
link_group_info = link_group_info,
auto_link_group_specs = get_cxx_auto_link_group_specs(ctx, link_group_info),
)
output, comp_db_info, xcode_data_info = cxx_executable(ctx, params)
return [
DefaultInfo(
default_outputs = [output.binary],
other_outputs = output.runtime_files,
sub_targets = output.sub_targets,
),
RunInfo(args = cmd_args(output.binary).hidden(output.runtime_files)),
comp_db_info,
xcode_data_info,
]
def _prebuilt_item(
ctx: "context",
item: ["", None],
platform_items: [[(str.type, "_a")], None]) -> ["_a", None]:
"""
Parse the given item that can be specified by regular and platform-specific
parameters.
"""
if item != None:
return item
if platform_items != None:
items = cxx_by_platform(ctx, platform_items)
if len(items) == 0:
return None
if len(items) != 1:
fail("expected single platform match: name={}//{}:{}, platform_items={}, items={}".format(ctx.label.cell, ctx.label.package, ctx.label.name, str(platform_items), str(items)))
return items[0]
return None
def _prebuilt_linkage(ctx: "context") -> Linkage.type:
"""
Construct the preferred linkage to use for the given prebuilt library.
"""
if ctx.attrs.header_only:
return Linkage("any")
if ctx.attrs.force_static:
return Linkage("static")
preferred_linkage = cxx_attr_preferred_linkage(ctx)
if preferred_linkage != Linkage("any"):
return preferred_linkage
if ctx.attrs.provided:
return Linkage("shared")
return Linkage("any")
def prebuilt_cxx_library_impl(ctx: "context") -> ["provider"]:
# Versioned params should be intercepted and converted away via the stub.
expect(not ctx.attrs.versioned_exported_lang_platform_preprocessor_flags)
expect(not ctx.attrs.versioned_exported_lang_preprocessor_flags)
expect(not ctx.attrs.versioned_exported_platform_preprocessor_flags)
expect(not ctx.attrs.versioned_exported_preprocessor_flags)
expect(not ctx.attrs.versioned_header_dirs)
expect(not ctx.attrs.versioned_shared_lib)
expect(not ctx.attrs.versioned_static_lib)
expect(not ctx.attrs.versioned_static_pic_lib)
if not cxx_platform_supported(ctx):
return [DefaultInfo(default_outputs = [])]
providers = []
linker_info = get_cxx_toolchain_info(ctx).linker_info
linker_type = linker_info.type
# Parse library parameters.
static_lib = _prebuilt_item(
ctx,
ctx.attrs.static_lib,
ctx.attrs.platform_static_lib,
)
static_pic_lib = _prebuilt_item(
ctx,
ctx.attrs.static_pic_lib,
ctx.attrs.platform_static_pic_lib,
)
shared_lib = _prebuilt_item(
ctx,
ctx.attrs.shared_lib,
ctx.attrs.platform_shared_lib,
)
header_dirs = _prebuilt_item(
ctx,
ctx.attrs.header_dirs,
ctx.attrs.platform_header_dirs,
)
soname = value_or(ctx.attrs.soname, get_shared_library_name(linker_info, ctx.label.name))
preferred_linkage = _prebuilt_linkage(ctx)
# Use ctx.attrs.deps instead of cxx_attr_deps, since prebuilt rules don't have platform_deps.
first_order_deps = ctx.attrs.deps
exported_first_order_deps = cxx_attr_exported_deps(ctx)
# Exported preprocessor info.
inherited_pp_infos = cxx_inherited_preprocessor_infos(exported_first_order_deps)
generic_exported_pre = cxx_exported_preprocessor_info(ctx, cxx_get_regular_cxx_headers_layout(ctx), [])
args = cxx_attr_exported_preprocessor_flags(ctx)
if header_dirs != None:
for x in header_dirs:
args += ["-isystem", x]
specific_exportd_pre = CPreprocessor(args = args)
providers.append(cxx_merge_cpreprocessors(
ctx,
[generic_exported_pre, specific_exportd_pre],
inherited_pp_infos,
))
inherited_link = cxx_inherited_link_info(ctx, first_order_deps)
inherited_exported_link = cxx_inherited_link_info(ctx, exported_first_order_deps)
exported_linker_flags = cxx_attr_exported_linker_flags(ctx)
# Gather link infos, outputs, and shared libs for effective link style.
outputs = {}
libraries = {}
solibs = {}
sub_targets = {}
for link_style in get_link_styles_for_linkage(preferred_linkage):
args = []
outs = []
# Add exported linker flags first.
args.extend(cxx_attr_exported_linker_flags(ctx))
post_link_flags = cxx_attr_exported_post_linker_flags(ctx)
linkable = None
# If we have sources to compile, generate the necessary libraries and
# add them to the exported link info.
if not ctx.attrs.header_only:
def archive_linkable(lib):
return ArchiveLinkable(
archive = Archive(artifact = lib),
linker_type = linker_type,
link_whole = ctx.attrs.link_whole,
)
if link_style == LinkStyle("static"):
if static_lib:
outs.append(static_lib)
linkable = archive_linkable(static_lib)
elif link_style == LinkStyle("static_pic"):
lib = static_pic_lib or static_lib
if lib:
outs.append(lib)
linkable = archive_linkable(lib)
else: # shared
# If no shared library was provided, link one from the static libraries.
if shared_lib != None:
shared_lib = LinkedObject(output = shared_lib)
else:
lib = static_pic_lib or static_lib
if lib:
shlink_args = []
# TODO(T110378143): Support post link flags properly.
shlink_args.extend(exported_linker_flags)
shlink_args.extend(get_link_whole_args(linker_type, [lib]))
shared_lib = cxx_link_into_shared_library(
ctx,
soname,
[
LinkArgs(flags = shlink_args),
# TODO(T110378118): As per v1, we always link against "shared"
# dependencies when building a shaerd library.
get_link_args(inherited_exported_link, LinkStyle("shared")),
],
)
if shared_lib:
outs.append(shared_lib.output)
# Some prebuilt shared libs don't set a SONAME (e.g.
# IntelComposerXE), so we can't link them via just the shared
# lib (otherwise, we'll may embed buid-time paths in `DT_NEEDED`
# tags).
if ctx.attrs.link_without_soname:
if ctx.attrs.supports_shared_library_interface:
fail("cannot use `link_without_soname` with shlib interfaces")
linkable = SharedLibLinkable(
lib = shared_lib.output,
link_without_soname = True,
)
else:
shared_lib_for_linking = shared_lib.output
# Generate a shared library interface if the rule supports it.
if ctx.attrs.supports_shared_library_interface and cxx_use_shlib_intfs(ctx):
shared_lib_for_linking = cxx_mk_shlib_intf(ctx, ctx.attrs.name, shared_lib.output)
linkable = SharedLibLinkable(lib = shared_lib_for_linking)
# Provided means something external to the build will provide
# the libraries, so we don't need to propagate anything.
if not ctx.attrs.provided:
solibs[soname] = shared_lib
# Provide a sub-target that always provides the shared lib
# using the soname.
if soname and shared_lib.output.basename != soname:
soname_lib = ctx.actions.copy_file(soname, shared_lib.output)
else:
soname_lib = shared_lib.output
sub_targets["soname-lib"] = [DefaultInfo(default_outputs = [soname_lib])]
# TODO(cjhopman): is it okay that we sometimes don't have a linkable?
outputs[link_style] = outs
libraries[link_style] = LinkInfos(
default = LinkInfo(
name = ctx.attrs.name,
pre_flags = args,
post_flags = post_link_flags,
linkables = [linkable] if linkable else [],
),
)
sub_targets[link_style.value.replace("_", "-")] = [DefaultInfo(
default_outputs = outputs[link_style],
)]
# Create the default ouput for the library rule given it's link style and preferred linkage
link_style = get_cxx_toolchain_info(ctx).linker_info.link_style
actual_link_style = get_actual_link_style(link_style, preferred_linkage)
output = outputs[actual_link_style]
providers.append(DefaultInfo(
default_outputs = output,
sub_targets = sub_targets,
))
# Propagate link info provider.
providers.append(create_merged_link_info(
ctx,
# Add link info for each link style,
libraries,
preferred_linkage = preferred_linkage,
# Export link info from non-exported deps (when necessary).
deps = [inherited_link],
# Export link info from out (exported) deps.
exported_deps = [inherited_exported_link],
))
# Propagate shared libraries up the tree.
providers.append(merge_shared_libraries(
ctx.actions,
create_shared_libraries(ctx, solibs),
filter(None, [x.get(SharedLibraryInfo) for x in exported_first_order_deps]),
))
# Create, augment and provide the linkable graph.
deps_linkable_graph = create_linkable_graph(
ctx,
deps = exported_first_order_deps,
)
# Omnibus root provider.
known_omnibus_root = is_known_omnibus_root(ctx)
linkable_root = None
if LinkStyle("static_pic") in libraries and (static_pic_lib or static_lib) and not ctx.attrs.header_only:
# TODO(cjhopman): This doesn't support thin archives
linkable_root = create_linkable_root(
ctx,
name = soname,
link_infos = LinkInfos(default = LinkInfo(
name = soname,
pre_flags = cxx_attr_exported_linker_flags(ctx),
linkables = [ArchiveLinkable(
archive = Archive(
artifact = static_pic_lib or static_lib,
),
linker_type = linker_type,
link_whole = True,
)],
post_flags = cxx_attr_exported_post_linker_flags(ctx),
)),
deps = exported_first_order_deps,
graph = deps_linkable_graph,
create_shared_root = known_omnibus_root,
)
providers.append(linkable_root)
roots = {}
if linkable_root != None and known_omnibus_root:
roots[ctx.label] = AnnotatedLinkableRoot(root = linkable_root)
linkable_graph = create_linkable_graph(
ctx,
node = create_linkable_graph_node(
ctx,
linkable_node = create_linkable_node(
ctx = ctx,
preferred_linkage = preferred_linkage,
exported_deps = exported_first_order_deps,
# If we don't have link input for this link style, we pass in `None` so
# that omnibus knows to avoid it.
link_infos = libraries,
shared_libs = solibs,
),
roots = roots,
excluded = {ctx.label: None} if not value_or(ctx.attrs.supports_merged_linking, True) else {},
),
children = [deps_linkable_graph],
)
providers.append(linkable_graph)
providers.append(
merge_link_group_lib_info(
deps = first_order_deps + exported_first_order_deps,
),
)
return providers
def cxx_precompiled_header_impl(ctx: "context") -> ["provider"]:
inherited_pp_infos = cxx_inherited_preprocessor_infos(ctx.attrs.deps)
inherited_link = cxx_inherited_link_info(ctx, ctx.attrs.deps)
return [
DefaultInfo(default_outputs = [ctx.attrs.src]),
cxx_merge_cpreprocessors(ctx, [], inherited_pp_infos),
inherited_link,
CPrecompiledHeaderInfo(header = ctx.attrs.src),
]
def cxx_test_impl(ctx: "context") -> ["provider"]:
link_group_info = get_link_group_info(ctx, filter_and_map_idx(LinkableGraph, cxx_attr_deps(ctx)))
# TODO(T110378115): have the runinfo contain the correct test running args
params = CxxRuleConstructorParams(
rule_type = "cxx_test",
headers_layout = cxx_get_regular_cxx_headers_layout(ctx),
srcs = get_srcs_with_flags(ctx),
link_group_info = link_group_info,
auto_link_group_specs = get_cxx_auto_link_group_specs(ctx, link_group_info),
)
output, comp_db_info, xcode_data_info = cxx_executable(ctx, params, is_cxx_test = True)
command = [cmd_args(output.binary).hidden(output.runtime_files)] + ctx.attrs.args
# Setup a RE executor based on the `remote_execution` param.
re_executor = get_re_executor_from_props(ctx.attrs.remote_execution)
return inject_test_run_info(
ctx,
ExternalRunnerTestInfo(
type = "gtest",
command = command,
env = ctx.attrs.env,
labels = ctx.attrs.labels,
contacts = ctx.attrs.contacts,
default_executor = re_executor,
# We implicitly make this test via the project root, instead of
# the cell root (e.g. fbcode root).
run_from_project_root = any([
"buck2_run_from_project_root" in (ctx.attrs.labels or []),
re_executor != None,
]),
use_project_relative_paths = re_executor != None,
),
) + [
DefaultInfo(default_outputs = [output.binary], other_outputs = output.runtime_files, sub_targets = output.sub_targets),
comp_db_info,
xcode_data_info,
]
def _get_params_for_android_binary_cxx_library() -> (CxxRuleSubTargetParams.type, CxxRuleProviderParams.type):
sub_target_params = CxxRuleSubTargetParams(
argsfiles = False,
compilation_database = False,
headers = False,
link_group_map = False,
xcode_data = False,
)
provider_params = CxxRuleProviderParams(
compilation_database = False,
omnibus_root = False,
preprocessor_for_tests = False,
)
return sub_target_params, provider_params

View file

@ -0,0 +1,87 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
# BOLT (Binary Optimization Layout Tool) is a post link profile guided optimizer used for
# performance-critical services in fbcode: https://www.internalfb.com/intern/wiki/HHVM-BOLT/
load("@prelude//:local_only.bzl", "link_cxx_binary_locally")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
def cxx_use_bolt(ctx: "context") -> bool.type:
cxx_toolchain_info = get_cxx_toolchain_info(ctx)
return cxx_toolchain_info.bolt_enabled and ctx.attrs.bolt_profile != None
def bolt_gdb_index(ctx: "context", bolt_output: "artifact", identifier: [str.type, None]) -> "artifact":
# Run gdb-indexer
# gdb-indexer <input_binary> -o <output_binary>
gdb_index_output_name = bolt_output.short_path.removesuffix("-pre_gdb_index") + "-gdb_index"
gdb_index_output = ctx.actions.declare_output(gdb_index_output_name)
gdb_index_args = cmd_args(
ctx.attrs.bolt_gdb_index,
bolt_output,
"-o",
gdb_index_output.as_output(),
)
ctx.actions.run(
gdb_index_args,
category = "gdb_index",
identifier = identifier,
local_only = link_cxx_binary_locally(ctx),
)
# Run objcopy
# objcopy -R .gdb_index --add-section=.gdb_index=<gdb_index_binary> <input_binary> <output_binary>
objcopy_output_name = gdb_index_output_name.removesuffix("-gdb_index")
objcopy_output = ctx.actions.declare_output(objcopy_output_name)
objcopy_args = cmd_args(
get_cxx_toolchain_info(ctx).binary_utilities_info.objcopy,
"-R",
".gdb_index",
cmd_args(gdb_index_output, format = "--add-section=.gdb_index={}"),
bolt_output,
objcopy_output.as_output(),
)
ctx.actions.run(
objcopy_args,
category = "objcopy",
identifier = identifier,
local_only = link_cxx_binary_locally(ctx),
)
return objcopy_output
def bolt(ctx: "context", prebolt_output: "artifact", identifier: [str.type, None]) -> "artifact":
output_name = prebolt_output.short_path.removesuffix("-wrapper") + ("-pre_gdb_index" if (ctx.attrs.bolt_gdb_index != None) else "")
postbolt_output = ctx.actions.declare_output(output_name)
bolt_msdk = get_cxx_toolchain_info(ctx).binary_utilities_info.bolt_msdk
if not bolt_msdk or not cxx_use_bolt(ctx):
fail("Cannot use bolt if bolt_msdk is not available or bolt profile is not available")
args = cmd_args()
# bolt command format:
# {llvm_bolt} {input_bin} -o $OUT -data={fdata} {args}
args.add(
cmd_args(bolt_msdk, format = "{}/bin/llvm-bolt"),
prebolt_output,
"-o",
postbolt_output.as_output(),
cmd_args(ctx.attrs.bolt_profile, format = "-data={}"),
ctx.attrs.bolt_flags,
)
ctx.actions.run(
args,
category = "bolt",
identifier = identifier,
local_only = get_cxx_toolchain_info(ctx).linker_info.link_binaries_locally,
)
if ctx.attrs.bolt_gdb_index != None:
return bolt_gdb_index(ctx, postbolt_output, identifier)
return postbolt_output

View file

@ -0,0 +1,34 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//apple:apple_toolchain_types.bzl", "AppleToolchainInfo")
load("@prelude//cxx:cxx_toolchain_types.bzl", "CxxPlatformInfo", "CxxToolchainInfo")
# The functions below allow the Cxx rules to find toolchain providers
# from different rule contexts. For example, the Cxx functions are
# re-used by non-`cxx_` rules (e.g., the Apple rules) but the toolchain
# setup on such rules might/would be different.
#
# The functions should be used throughout the Cxx rules to get
# the required providers instead of going via the `_cxx_toolchain`
# field of the `ctx`.
#
# In an ideal world, we would have been injecting all these from
# the top level but as part of the transition to support
# `apple_toolchain`, we want to make progress now.
def get_cxx_platform_info(ctx: "context") -> "CxxPlatformInfo":
apple_toolchain = getattr(ctx.attrs, "_apple_toolchain", None)
if apple_toolchain:
return apple_toolchain[AppleToolchainInfo].cxx_platform_info
return ctx.attrs._cxx_toolchain[CxxPlatformInfo]
def get_cxx_toolchain_info(ctx: "context") -> "CxxToolchainInfo":
apple_toolchain = getattr(ctx.attrs, "_apple_toolchain", None)
if apple_toolchain:
return apple_toolchain[AppleToolchainInfo].cxx_toolchain_info
return ctx.attrs._cxx_toolchain[CxxToolchainInfo]

View file

@ -0,0 +1,530 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:local_only.bzl", "link_cxx_binary_locally")
load(
"@prelude//:resources.bzl",
"create_resource_db",
"gather_resources",
)
load(
"@prelude//apple:apple_frameworks.bzl",
"build_link_args_with_deduped_framework_flags",
"create_frameworks_linkable",
"get_frameworks_link_info_by_deduping_link_infos",
)
load(
"@prelude//cxx:cxx_bolt.bzl",
"cxx_use_bolt",
)
load(
"@prelude//ide_integrations:xcode.bzl",
"XCODE_DATA_SUB_TARGET",
"generate_xcode_data",
)
load(
"@prelude//linking:link_groups.bzl",
"LinkGroupLib",
"gather_link_group_libs",
)
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
"LinkInfo",
"LinkInfos",
"LinkStyle",
"LinkedObject", # @unused Used as a type
"ObjectsLinkable",
"SharedLibLinkable",
"merge_link_infos",
)
load(
"@prelude//linking:linkable_graph.bzl",
"create_linkable_graph",
"get_linkable_graph_node_map_func",
)
load(
"@prelude//linking:linkables.bzl",
"linkables",
)
load(
"@prelude//linking:shared_libraries.bzl",
"merge_shared_libraries",
"traverse_shared_library_info",
)
load(
"@prelude//utils:utils.bzl",
"expect",
"flatten_dict",
)
load(
":comp_db.bzl",
"CxxCompilationDbInfo", # @unused Used as a type
"create_compilation_database",
"make_compilation_db_info",
)
load(
":compile.bzl",
"compile_cxx",
"create_compile_cmds",
)
load(":cxx_context.bzl", "get_cxx_platform_info", "get_cxx_toolchain_info")
load(
":cxx_library_utility.bzl",
"ARGSFILES_SUBTARGET",
"cxx_attr_deps",
"cxx_attr_link_style",
"cxx_attr_linker_flags",
"cxx_attr_resources",
"cxx_is_gnu",
)
load(
":cxx_link_utility.bzl",
"executable_shared_lib_arguments",
)
load(
":cxx_types.bzl",
"CxxRuleConstructorParams", # @unused Used as a type
)
load(
":link.bzl",
"cxx_link",
)
load(
":link_groups.bzl",
"LINK_GROUP_MAP_DATABASE_SUB_TARGET",
"create_link_group",
"get_filtered_labels_to_links_map",
"get_filtered_links",
"get_filtered_targets",
"get_link_group",
"get_link_group_map_json",
"get_link_group_preferred_linkage",
)
load(
":preprocessor.bzl",
"cxx_inherited_preprocessor_infos",
"cxx_private_preprocessor_info",
)
_CxxExecutableOutput = record(
binary = "artifact",
# Files that will likely need to be included as .hidden() arguments
# when executing the executable (ex. RunInfo())
runtime_files = ["_arglike"],
sub_targets = {str.type: [DefaultInfo.type]},
# The LinkArgs used to create the final executable in 'binary'.
link_args = [LinkArgs.type],
# External components needed to debug the executable.
external_debug_info = ["_arglike"],
shared_libs = {str.type: LinkedObject.type},
# All link group links that were generated in the executable.
auto_link_groups = field({str.type: LinkedObject.type}, {}),
)
# returns a tuple of the runnable binary as an artifact, a list of its runtime files as artifacts and a sub targets map, and the CxxCompilationDbInfo provider
def cxx_executable(ctx: "context", impl_params: CxxRuleConstructorParams.type, is_cxx_test: bool.type = False) -> (_CxxExecutableOutput.type, CxxCompilationDbInfo.type, "XcodeDataInfo"):
# Gather preprocessor inputs.
preprocessor_deps = cxx_attr_deps(ctx) + filter(None, [ctx.attrs.precompiled_header])
(own_preprocessor_info, test_preprocessor_infos) = cxx_private_preprocessor_info(
ctx,
impl_params.headers_layout,
raw_headers = ctx.attrs.raw_headers,
extra_preprocessors = impl_params.extra_preprocessors,
non_exported_deps = preprocessor_deps,
is_test = is_cxx_test,
)
inherited_preprocessor_infos = cxx_inherited_preprocessor_infos(preprocessor_deps) + impl_params.extra_preprocessors_info
# The link style to use.
link_style = cxx_attr_link_style(ctx)
sub_targets = {}
# Compile objects.
compile_cmd_output = create_compile_cmds(
ctx,
impl_params,
[own_preprocessor_info] + test_preprocessor_infos,
inherited_preprocessor_infos,
)
cxx_outs = compile_cxx(ctx, compile_cmd_output.source_commands.src_compile_cmds, pic = link_style != LinkStyle("static"))
sub_targets[ARGSFILES_SUBTARGET] = [compile_cmd_output.source_commands.argsfiles_info]
# Compilation DB.
comp_db = create_compilation_database(ctx, compile_cmd_output.comp_db_commands.src_compile_cmds)
sub_targets["compilation-database"] = [comp_db]
comp_db_info = make_compilation_db_info(compile_cmd_output.comp_db_commands.src_compile_cmds, get_cxx_toolchain_info(ctx), get_cxx_platform_info(ctx))
# Link deps
link_deps = linkables(cxx_attr_deps(ctx)) + impl_params.extra_link_deps
# Link Groups
link_group = get_link_group(ctx)
link_group_info = impl_params.link_group_info
if link_group_info:
link_groups = link_group_info.groups
link_group_mappings = link_group_info.mappings
link_group_deps = [mapping.root.node.linkable_graph for group in link_group_info.groups for mapping in group.mappings if mapping.root != None]
else:
link_groups = []
link_group_mappings = {}
link_group_deps = []
link_group_preferred_linkage = get_link_group_preferred_linkage(link_groups)
# Create the linkable graph with the binary's deps and any link group deps.
linkable_graph = create_linkable_graph(
ctx,
children = [d.linkable_graph for d in link_deps] + link_group_deps,
)
# Gather link inputs.
own_link_flags = cxx_attr_linker_flags(ctx) + impl_params.extra_link_flags + impl_params.extra_exported_link_flags
own_binary_link_flags = ctx.attrs.binary_linker_flags + own_link_flags
inherited_link = merge_link_infos(ctx, [d.merged_link_info for d in link_deps])
frameworks_linkable = create_frameworks_linkable(ctx)
# Link group libs.
link_group_libs = {}
auto_link_groups = {}
labels_to_links_map = {}
if not link_group_mappings:
dep_links = build_link_args_with_deduped_framework_flags(
ctx,
inherited_link,
frameworks_linkable,
link_style,
prefer_stripped = ctx.attrs.prefer_stripped_objects,
)
else:
linkable_graph_node_map = get_linkable_graph_node_map_func(linkable_graph)()
# If we're using auto-link-groups, where we generate the link group links
# in the prelude, the link group map will give us the link group libs.
# Otherwise, pull them from the `LinkGroupLibInfo` provider from out deps.
if impl_params.auto_link_group_specs != None:
for link_group_spec in impl_params.auto_link_group_specs:
# NOTE(agallagher): It might make sense to move this down to be
# done when we generated the links for the executable, so we can
# handle the case when a link group can depend on the executable.
link_group_lib = create_link_group(
ctx = ctx,
spec = link_group_spec,
executable_deps = [
dep.linkable_graph.nodes.value.label
for dep in link_deps
],
root_link_group = link_group,
linkable_graph_node_map = linkable_graph_node_map,
linker_flags = own_link_flags,
link_group_mappings = link_group_mappings,
link_group_preferred_linkage = {},
#link_style = LinkStyle("static_pic"),
# TODO(agallagher): We should generate link groups via post-order
# traversal to get inter-group deps correct.
#link_group_libs = {},
prefer_stripped_objects = ctx.attrs.prefer_stripped_objects,
category_suffix = "link_group",
)
auto_link_groups[link_group_spec.group.name] = link_group_lib
if link_group_spec.is_shared_lib:
link_group_libs[link_group_spec.group.name] = LinkGroupLib(
shared_libs = {link_group_spec.name: link_group_lib},
shared_link_infos = LinkInfos(
default = LinkInfo(
linkables = [
SharedLibLinkable(lib = link_group_lib.output),
],
),
),
)
else:
link_group_libs = gather_link_group_libs(
children = [d.link_group_lib_info for d in link_deps],
)
# TODO(T110378098): Similar to shared libraries, we need to identify all the possible
# scenarios for which we need to propagate up link info and simplify this logic. For now
# base which links to use based on whether link groups are defined.
labels_to_links_map = get_filtered_labels_to_links_map(
linkable_graph_node_map,
link_group,
link_group_mappings,
link_group_preferred_linkage,
link_group_libs = link_group_libs,
link_style = link_style,
deps = [d.linkable_graph.nodes.value.label for d in link_deps],
is_executable_link = True,
prefer_stripped = ctx.attrs.prefer_stripped_objects,
)
if is_cxx_test and link_group != None:
# if a cpp_unittest is part of the link group, we need to traverse through all deps
# from the root again to ensure we link in gtest deps
labels_to_links_map = labels_to_links_map | get_filtered_labels_to_links_map(
linkable_graph_node_map,
None,
link_group_mappings,
link_group_preferred_linkage,
link_style,
deps = [d.linkable_graph.nodes.value.label for d in link_deps],
is_executable_link = True,
prefer_stripped = ctx.attrs.prefer_stripped_objects,
)
filtered_links = get_filtered_links(labels_to_links_map)
filtered_targets = get_filtered_targets(labels_to_links_map)
# Unfortunately, link_groups does not use MergedLinkInfo to represent the args
# for the resolved nodes in the graph.
# Thus, we have no choice but to traverse all the nodes to dedupe the framework linker args.
frameworks_link_info = get_frameworks_link_info_by_deduping_link_infos(ctx, filtered_links, frameworks_linkable)
if frameworks_link_info:
filtered_links.append(frameworks_link_info)
dep_links = LinkArgs(infos = filtered_links)
sub_targets[LINK_GROUP_MAP_DATABASE_SUB_TARGET] = [get_link_group_map_json(ctx, filtered_targets)]
# Set up shared libraries symlink tree only when needed
shared_libs = {}
# Only setup a shared library symlink tree when shared linkage or link_groups is used
gnu_use_link_groups = cxx_is_gnu(ctx) and link_group_mappings
if link_style == LinkStyle("shared") or gnu_use_link_groups:
shlib_info = merge_shared_libraries(
ctx.actions,
deps = [d.shared_library_info for d in link_deps],
)
def is_link_group_shlib(label: "label"):
# If this maps to a link group which we have a `LinkGroupLibInfo` for,
# then we'll handlet his below.
# buildifier: disable=uninitialized
if label in link_group_mappings and link_group_mappings[label] in link_group_libs:
return False
# if using link_groups, only materialize the link_group shlibs
return label in labels_to_links_map and labels_to_links_map[label].link_style == LinkStyle("shared") # buildifier: disable=uninitialized
for name, shared_lib in traverse_shared_library_info(shlib_info).items():
label = shared_lib.label
if not gnu_use_link_groups or is_link_group_shlib(label):
shared_libs[name] = shared_lib.lib
if gnu_use_link_groups:
# All explicit link group libs (i.e. libraries that set `link_group`).
link_group_names = {n: None for n in link_group_mappings.values()}
for name, link_group_lib in link_group_libs.items():
# Is it possible to find a link group lib in our graph without it
# having a mapping setup?
expect(name in link_group_names)
shared_libs.update(link_group_lib.shared_libs)
toolchain_info = get_cxx_toolchain_info(ctx)
linker_info = toolchain_info.linker_info
links = [
LinkArgs(infos = [
LinkInfo(
pre_flags = own_binary_link_flags,
linkables = [ObjectsLinkable(
objects = [out.object for out in cxx_outs],
linker_type = linker_info.type,
link_whole = True,
)],
external_debug_info = (
[out.object for out in cxx_outs if out.object_has_external_debug_info] +
[out.external_debug_info for out in cxx_outs if out.external_debug_info != None]
),
),
]),
dep_links,
] + impl_params.extra_link_args
binary, runtime_files, shared_libs_symlink_tree, extra_args = _link_into_executable(
ctx,
links,
# If shlib lib tree generation is enabled, pass in the shared libs (which
# will trigger the necessary link tree and link args).
shared_libs if impl_params.exe_shared_libs_link_tree else {},
linker_info.link_weight,
linker_info.binary_extension,
prefer_local = False if impl_params.force_full_hybrid_if_capable else link_cxx_binary_locally(ctx),
enable_distributed_thinlto = ctx.attrs.enable_distributed_thinlto,
strip = impl_params.strip_executable,
strip_args_factory = impl_params.strip_args_factory,
link_postprocessor = impl_params.link_postprocessor,
force_full_hybrid_if_capable = impl_params.force_full_hybrid_if_capable,
)
# Define the xcode data sub target
xcode_data_default_info, xcode_data_info = generate_xcode_data(
ctx,
rule_type = impl_params.rule_type,
output = binary.output,
populate_rule_specific_attributes_func = impl_params.cxx_populate_xcode_attributes_func,
srcs = impl_params.srcs + impl_params.additional.srcs,
argsfiles_by_ext = compile_cmd_output.source_commands.argsfile_by_ext,
product_name = get_cxx_excutable_product_name(ctx),
)
sub_targets[XCODE_DATA_SUB_TARGET] = xcode_data_default_info
# Info about dynamic-linked libraries for fbpkg integration:
# - the symlink dir that's part of RPATH
# - sub-sub-targets that reference shared library dependencies and their respective dwp
# - [shared-libraries] - a json map that references the above rules.
if shared_libs_symlink_tree:
sub_targets["rpath-tree"] = [DefaultInfo(default_outputs = [shared_libs_symlink_tree])]
sub_targets["shared-libraries"] = [DefaultInfo(
default_outputs = [ctx.actions.write_json(
binary.output.basename + ".shared-libraries.json",
{
"libraries": ["{}:{}[shared-libraries][{}]".format(ctx.label.path, ctx.label.name, name) for name in shared_libs.keys()],
"librariesdwp": ["{}:{}[shared-libraries][{}][dwp]".format(ctx.label.path, ctx.label.name, name) for name, lib in shared_libs.items() if lib.dwp],
"rpathtree": ["{}:{}[rpath-tree]".format(ctx.label.path, ctx.label.name)] if shared_libs_symlink_tree else [],
},
)],
sub_targets = {
name: [DefaultInfo(
default_outputs = [lib.output],
sub_targets = {"dwp": [DefaultInfo(default_outputs = [lib.dwp])]} if lib.dwp else {},
)]
for name, lib in shared_libs.items()
},
)]
# TODO(T110378140): We can't really enable this yet, as Python binaries
# consuming C++ binaries as resources don't know how to handle the
# extraneous debug paths and will crash. We probably need to add a special
# exported resources provider and make sure we handle the workflows.
# Add any referenced debug paths to runtime files.
#runtime_files.extend(binary.external_debug_info)
# If we have some resources, write it to the resources JSON file and add
# it and all resources to "runtime_files" so that we make to materialize
# them with the final binary.
resources = flatten_dict(gather_resources(
label = ctx.label,
resources = cxx_attr_resources(ctx),
deps = cxx_attr_deps(ctx),
).values())
if resources:
runtime_files.append(create_resource_db(
ctx = ctx,
name = binary.output.basename + ".resources.json",
binary = binary.output,
resources = resources,
))
for resource, other in resources.values():
runtime_files.append(resource)
runtime_files.extend(other)
if binary.dwp:
# A `dwp` sub-target which generates the `.dwp` file for this binary.
sub_targets["dwp"] = [DefaultInfo(default_outputs = [binary.dwp])]
# If bolt is not ran, binary.prebolt_output will be the same as binary.output. Only
# expose binary.prebolt_output if cxx_use_bolt(ctx) is True to avoid confusion
if cxx_use_bolt(ctx):
sub_targets["prebolt"] = [DefaultInfo(default_outputs = [binary.prebolt_output])]
(linker_map, binary_for_linker_map) = _linker_map(
ctx,
binary,
[LinkArgs(flags = extra_args)] + links,
prefer_local = link_cxx_binary_locally(ctx, toolchain_info),
link_weight = linker_info.link_weight,
)
sub_targets["linker-map"] = [DefaultInfo(default_outputs = [linker_map], other_outputs = [binary_for_linker_map])]
sub_targets["linker.argsfile"] = [DefaultInfo(
default_outputs = [binary.linker_argsfile],
)]
if linker_info.supports_distributed_thinlto and ctx.attrs.enable_distributed_thinlto:
sub_targets["index.argsfile"] = [DefaultInfo(
default_outputs = [binary.index_argsfile],
)]
return _CxxExecutableOutput(
binary = binary.output,
runtime_files = runtime_files,
sub_targets = sub_targets,
link_args = links,
external_debug_info = binary.external_debug_info,
shared_libs = shared_libs,
auto_link_groups = auto_link_groups,
), comp_db_info, xcode_data_info
# Returns a tuple of:
# - the resulting executable
# - list of files/directories that should be present for executable to be run successfully
# - optional shared libs symlink tree symlinked_dir action
# - extra linking args (for the shared_libs)
def _link_into_executable(
ctx: "context",
links: [LinkArgs.type],
shared_libs: {str.type: LinkedObject.type},
link_weight: int.type,
binary_extension: str.type,
prefer_local: bool.type = False,
enable_distributed_thinlto = False,
strip: bool.type = False,
strip_args_factory = None,
link_postprocessor: ["cmd_args", None] = None,
force_full_hybrid_if_capable: bool.type = False) -> (LinkedObject.type, ["_arglike"], ["artifact", None], [""]):
output = ctx.actions.declare_output("{}{}".format(get_cxx_excutable_product_name(ctx), "." + binary_extension if binary_extension else ""))
extra_args, runtime_files, shared_libs_symlink_tree = executable_shared_lib_arguments(
ctx.actions,
get_cxx_toolchain_info(ctx),
output,
shared_libs,
)
exe = cxx_link(
ctx,
[LinkArgs(flags = extra_args)] + links,
output,
prefer_local = prefer_local,
link_weight = link_weight,
enable_distributed_thinlto = enable_distributed_thinlto,
category_suffix = "executable",
strip = strip,
strip_args_factory = strip_args_factory,
executable_link = True,
link_postprocessor = link_postprocessor,
force_full_hybrid_if_capable = force_full_hybrid_if_capable,
)
return (exe, runtime_files, shared_libs_symlink_tree, extra_args)
def _linker_map(
ctx: "context",
binary: LinkedObject.type,
links: [LinkArgs.type],
prefer_local: bool.type,
link_weight: int.type) -> ("artifact", "artifact"):
identifier = binary.output.short_path + ".linker-map-binary"
binary_for_linker_map = ctx.actions.declare_output(identifier)
linker_map = ctx.actions.declare_output(binary.output.short_path + ".linker-map")
cxx_link(
ctx,
links,
binary_for_linker_map,
category_suffix = "linker_map",
linker_map = linker_map,
prefer_local = prefer_local,
link_weight = link_weight,
identifier = identifier,
generate_dwp = False,
)
return (
linker_map,
binary_for_linker_map,
)
def get_cxx_excutable_product_name(ctx: "context") -> str.type:
return ctx.label.name + ("-wrapper" if cxx_use_bolt(ctx) else "")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,165 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:paths.bzl", "paths")
load(
"@prelude//linking:link_info.bzl",
"LinkStyle",
"Linkage",
"MergedLinkInfo",
"merge_link_infos",
)
load(
"@prelude//utils:utils.bzl",
"expect",
"flatten",
"from_named_set",
"value_or",
)
load(":cxx_context.bzl", "get_cxx_platform_info", "get_cxx_toolchain_info")
load(
":headers.bzl",
"cxx_attr_header_namespace",
)
load(
":linker.bzl",
"get_shared_library_name",
)
load(":platform.bzl", "cxx_by_platform")
ARGSFILES_SUBTARGET = "argsfiles"
# The dependencies
def cxx_attr_deps(ctx: "context") -> ["dependency"]:
return (
ctx.attrs.deps +
flatten(cxx_by_platform(ctx, ctx.attrs.platform_deps)) +
(getattr(ctx.attrs, "deps_query", []) or [])
)
def cxx_attr_exported_deps(ctx: "context") -> ["dependency"]:
return ctx.attrs.exported_deps + flatten(cxx_by_platform(ctx, ctx.attrs.exported_platform_deps))
def cxx_attr_exported_linker_flags(ctx: "context") -> [""]:
return (
ctx.attrs.exported_linker_flags +
flatten(cxx_by_platform(ctx, ctx.attrs.exported_platform_linker_flags))
)
def cxx_attr_exported_post_linker_flags(ctx: "context") -> [""]:
return (
ctx.attrs.exported_post_linker_flags +
flatten(cxx_by_platform(ctx, ctx.attrs.exported_post_platform_linker_flags))
)
def cxx_inherited_link_info(ctx, first_order_deps: ["dependency"]) -> MergedLinkInfo.type:
# We filter out nones because some non-cxx rule without such providers could be a dependency, for example
# cxx_binary "fbcode//one_world/cli/util/process_wrapper:process_wrapper" depends on
# python_library "fbcode//third-party-buck/$platform/build/glibc:__project__"
return merge_link_infos(ctx, filter(None, [x.get(MergedLinkInfo) for x in first_order_deps]))
# Linker flags
def cxx_attr_linker_flags(ctx: "context") -> [""]:
return (
ctx.attrs.linker_flags +
flatten(cxx_by_platform(ctx, ctx.attrs.platform_linker_flags))
)
def cxx_attr_link_style(ctx: "context") -> LinkStyle.type:
if ctx.attrs.link_style != None:
return LinkStyle(ctx.attrs.link_style)
if ctx.attrs.defaults != None:
# v1 equivalent code is in CxxConstructorArg::getDefaultFlavors and ParserWithConfigurableAttributes::applyDefaultFlavors
# Only values in the map are used by v1 as flavors, copy this behavior and return the first value which is compatible with link style.
v1_flavors = ctx.attrs.defaults.values()
for s in [LinkStyle("static"), LinkStyle("static_pic"), LinkStyle("shared")]:
if s.value in v1_flavors:
return s
return get_cxx_toolchain_info(ctx).linker_info.link_style
def cxx_attr_preferred_linkage(ctx: "context") -> Linkage.type:
preferred_linkage = ctx.attrs.preferred_linkage
# force_static is deprecated, but it has precedence over preferred_linkage
if getattr(ctx.attrs, "force_static", False):
preferred_linkage = "static"
return Linkage(preferred_linkage)
def cxx_attr_resources(ctx: "context") -> {str.type: ("artifact", ["_arglike"])}:
"""
Return the resources provided by this rule, as a map of resource name to
a tuple of the resource artifact and any "other" outputs exposed by it.
"""
resources = {}
namespace = cxx_attr_header_namespace(ctx)
# Use getattr, as apple rules don't have a `resources` parameter.
for name, resource in from_named_set(getattr(ctx.attrs, "resources", {})).items():
if type(resource) == "artifact":
other = []
else:
info = resource[DefaultInfo]
expect(
len(info.default_outputs) == 1,
"expected exactly one default output from {} ({})"
.format(resource, info.default_outputs),
)
[resource] = info.default_outputs
other = info.other_outputs
resources[paths.join(namespace, name)] = (resource, other)
return resources
def cxx_mk_shlib_intf(
ctx: "context",
name: str.type,
shared_lib: "artifact") -> "artifact":
"""
Convert the given shared library into an interface used for linking.
"""
linker_info = get_cxx_toolchain_info(ctx).linker_info
args = cmd_args(linker_info.mk_shlib_intf[RunInfo])
args.add(shared_lib)
output = ctx.actions.declare_output(
get_shared_library_name(linker_info, name + "-interface"),
)
args.add(output.as_output())
ctx.actions.run(args, category = "generate_shared_library_interface")
return output
def cxx_is_gnu(ctx: "context") -> bool.type:
return get_cxx_toolchain_info(ctx).linker_info.type == "gnu"
def cxx_use_link_groups(ctx: "context") -> bool.type:
# Link groups is enabled by default in darwin
return cxx_is_gnu(ctx) and value_or(ctx.attrs.use_link_groups, False)
def cxx_use_shlib_intfs(ctx: "context") -> bool.type:
"""
Return whether we should use shared library interfaces for linking.
"""
linker_info = get_cxx_toolchain_info(ctx).linker_info
# TODO(T110378128): Apple currently uses the same configuration as fbcode
# platforms, so only explicitly enable for linux until this is fixed.
return linker_info.shlib_interfaces != "disabled" and linker_info.type == "gnu"
def cxx_platform_supported(ctx: "context") -> bool.type:
"""
Return whether this rule's `supported_platforms_regex` matches the current
platform name.
"""
if ctx.attrs.supported_platforms_regex == None:
return True
return regex_match(
ctx.attrs.supported_platforms_regex,
get_cxx_platform_info(ctx).name,
)

View file

@ -0,0 +1,201 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//cxx:cxx_toolchain_types.bzl", "CxxToolchainInfo")
load("@prelude//cxx:debug.bzl", "SplitDebugMode")
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
"LinkInfo",
"unpack_link_args",
"unpack_link_args_filelist",
)
load("@prelude//linking:lto.bzl", "LtoMode")
load("@prelude//utils:utils.bzl", "expect")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
def linker_map_args(ctx, linker_map) -> LinkArgs.type:
darwin_flags = [
"-Xlinker",
"-map",
"-Xlinker",
linker_map,
]
gnu_flags = [
"-Xlinker",
"-Map",
"-Xlinker",
linker_map,
"-Xlinker",
# A linker map is useful even when the output executable can't be correctly created
# (e.g. due to relocation overflows). Turn errors into warnings so the
# path/to:binary[linker-map] sub-target succesfully runs and produces the linker map file.
"-noinhibit-exec",
# If linking hits relocation overflows these will produce a huge amount of almost identical logs.
"-Xlinker",
"--error-limit=1",
]
return LinkArgs(flags = darwin_flags if get_cxx_toolchain_info(ctx).linker_info.type == "darwin" else gnu_flags)
def map_link_args_for_dwo(ctx: "context", links: ["LinkArgs"], dwo_dir_name: [str.type, None]) -> (["LinkArgs"], ["artifact", None]):
"""
Takes LinkArgs, and if they enable the DWO output dir hack, returns updated
args and a DWO dir as output. If they don't, just returns the args as-is.
"""
# TODO(T110378131): Once we have first-class support for ThinLTO and
# split-dwarf, we can move way from this hack and have the rules add this
# parameter appropriately. But, for now, to maintain compatibility for how
# the macros setup ThinLTO+split-dwarf, use a macro hack to intercept when
# we're setting an explicitly tracked dwo dir and pull into the explicit
# tracking we do at the `LinkedObject` level.
#
# Can't mutate a variable, so put it in a list and mutate the innards
dwo_dir = [None]
def adjust_flag(flag: "_arglike") -> "_arglike":
if "HACK-OUTPUT-DWO-DIR" in repr(flag):
expect(dwo_dir_name != None)
expect(dwo_dir[0] == None)
dwo_dir[0] = ctx.actions.declare_output(dwo_dir_name)
return cmd_args(dwo_dir[0].as_output(), format = "dwo_dir={}")
else:
return flag
def adjust_link_info(link_info: LinkInfo.type) -> LinkInfo.type:
return LinkInfo(
name = link_info.name,
linkables = link_info.linkables,
pre_flags = [adjust_flag(x) for x in link_info.pre_flags],
post_flags = [adjust_flag(x) for x in link_info.post_flags],
use_link_groups = link_info.use_link_groups,
)
links = [
LinkArgs(
tset = link.tset,
flags = [adjust_flag(flag) for flag in link.flags] if link.flags != None else None,
infos = [adjust_link_info(info) for info in link.infos] if link.infos != None else None,
)
for link in links
]
return (links, dwo_dir[0])
def make_link_args(
ctx: "context",
links: ["LinkArgs"],
suffix = None,
dwo_dir_name: [str.type, None] = None,
is_shared: [bool.type, None] = None,
link_ordering: ["LinkOrdering", None] = None) -> ("_arglike", ["_hidden"], ["artifact", None]):
"""
Merges LinkArgs. Returns the args, files that must be present for those
args to work when passed to a linker, and optionally an artifact where DWO
outputs will be written to.
"""
suffix = "" if suffix == None else "-" + suffix
args = cmd_args()
filelists = filter(None, [unpack_link_args_filelist(link) for link in links])
cxx_toolchain_info = get_cxx_toolchain_info(ctx)
linker_type = cxx_toolchain_info.linker_info.type
if filelists:
if linker_type == "gnu":
fail("filelist populated for gnu linker")
elif linker_type == "darwin":
path = ctx.actions.write("filelist%s.txt" % suffix, filelists)
args.add(["-Xlinker", "-filelist", "-Xlinker", path])
else:
fail("Linker type {} not supported".format(linker_type))
# On Apple platforms, DWARF data is contained in the object files
# and executables contains paths to the object files (N_OSO stab).
#
# By default, ld64 will use absolute file paths in N_OSO entries
# which machine-dependent executables. Such executables would not
# be debuggable on any host apart from the host which performed
# the linking. Instead, we want produce machine-independent
# hermetic executables, so we need to relativize those paths.
#
# This is accomplished by passing the `oso-prefix` flag to ld64,
# which will strip the provided prefix from the N_OSO paths.
#
# The flag accepts a special value, `.`, which means it will
# use the current workding directory. This will make all paths
# relative to the parent of `buck-out`.
#
# Because all actions in Buck2 are run from the project root
# and `buck-out` is always inside the project root, we can
# safely pass `.` as the `-oso_prefix` without having to
# write a wrapper script to compute it dynamically.
if linker_type == "darwin":
args.add(["-Wl,-oso_prefix,."])
# Not all C/C++ codebases use split-DWARF. Apple uses dSYM files, instead.
#
# If we aren't going to use .dwo/.dwp files, avoid the codepath.
# Historically we've seen that going down this path bloats
# the memory usage of FBiOS by 12% (which amounts to Gigabytes.)
#
# Context: D36669131
dwo_dir = None
if cxx_toolchain_info.linker_info.lto_mode != LtoMode("none") and cxx_toolchain_info.split_debug_mode != SplitDebugMode("none"):
links, dwo_dir = map_link_args_for_dwo(ctx, links, dwo_dir_name)
for link in links:
args.add(unpack_link_args(link, is_shared, link_ordering = link_ordering))
return (args, [args] + filelists, dwo_dir)
def shared_libs_symlink_tree_name(output: "artifact") -> str.type:
return "__{}__shared_libs_symlink_tree".format(output.short_path)
# Returns a tuple of:
# - list of extra arguments,
# - list of files/directories that should be present for executable to be run successfully
# - optional shared libs symlink tree symlinked_dir action
def executable_shared_lib_arguments(
actions: "actions",
cxx_toolchain: CxxToolchainInfo.type,
output: "artifact",
shared_libs: {str.type: "LinkedObject"}) -> ([""], ["_arglike"], ["artifact", None]):
extra_args = []
runtime_files = []
shared_libs_symlink_tree = None
# Add external debug paths to runtime files, so that they're
# materialized when the binary is built.
for shlib in shared_libs.values():
runtime_files.extend(shlib.external_debug_info)
if len(shared_libs) > 0:
shared_libs_symlink_tree = actions.symlinked_dir(
shared_libs_symlink_tree_name(output),
{name: shlib.output for name, shlib in shared_libs.items()},
)
runtime_files.append(shared_libs_symlink_tree)
linker_type = cxx_toolchain.linker_info.type
if linker_type == "gnu":
rpath_reference = "$ORIGIN"
elif linker_type == "darwin":
rpath_reference = "@loader_path"
else:
fail("Linker type {} not supported".format(linker_type))
# We ignore_artifacts() here since we don't want the symlink tree to actually be there for the link.
rpath_arg = cmd_args(shared_libs_symlink_tree, format = "-Wl,-rpath,{}/{{}}".format(rpath_reference)).relative_to(output, parent = 1).ignore_artifacts()
extra_args.append(rpath_arg)
return (extra_args, runtime_files, shared_libs_symlink_tree)
# The command line for linking with C++
def cxx_link_cmd(ctx: "context") -> "cmd_args":
toolchain = get_cxx_toolchain_info(ctx)
command = cmd_args(toolchain.linker_info.linker)
command.add(toolchain.linker_info.linker_flags or [])
return command

View file

@ -0,0 +1,170 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//cxx:cxx_toolchain_types.bzl", "AsCompilerInfo", "AsmCompilerInfo", "BinaryUtilitiesInfo", "CCompilerInfo", "CudaCompilerInfo", "CxxCompilerInfo", "HipCompilerInfo", "LinkerInfo", "StripFlagsInfo", "cxx_toolchain_infos")
load("@prelude//cxx:debug.bzl", "SplitDebugMode")
load("@prelude//cxx:headers.bzl", "HeaderMode", "HeadersAsRawHeadersMode")
load("@prelude//cxx:linker.bzl", "LINKERS")
load("@prelude//linking:link_info.bzl", "LinkStyle")
load("@prelude//linking:lto.bzl", "LtoMode")
load("@prelude//utils:utils.bzl", "value_or")
def cxx_toolchain_impl(ctx):
c_info = CCompilerInfo(
compiler = ctx.attrs.c_compiler[RunInfo],
compiler_type = ctx.attrs.c_compiler_type or ctx.attrs.compiler_type,
compiler_flags = cmd_args(ctx.attrs.c_compiler_flags),
preprocessor_flags = cmd_args(ctx.attrs.c_preprocessor_flags),
dep_files_processor = ctx.attrs._dep_files_processor[RunInfo],
)
cxx_info = CxxCompilerInfo(
compiler = ctx.attrs.cxx_compiler[RunInfo],
compiler_type = ctx.attrs.cxx_compiler_type or ctx.attrs.compiler_type,
compiler_flags = cmd_args(ctx.attrs.cxx_compiler_flags),
preprocessor_flags = cmd_args(ctx.attrs.cxx_preprocessor_flags),
dep_files_processor = ctx.attrs._dep_files_processor[RunInfo],
)
asm_info = AsmCompilerInfo(
compiler = ctx.attrs.asm_compiler[RunInfo],
compiler_type = ctx.attrs.asm_compiler_type or ctx.attrs.compiler_type,
compiler_flags = cmd_args(ctx.attrs.asm_compiler_flags),
preprocessor_flags = cmd_args(ctx.attrs.asm_preprocessor_flags),
dep_files_processor = ctx.attrs._dep_files_processor[RunInfo],
) if ctx.attrs.asm_compiler else None
as_info = AsCompilerInfo(
compiler = ctx.attrs.assembler[RunInfo],
compiler_type = ctx.attrs.assembler_type or ctx.attrs.compiler_type,
compiler_flags = cmd_args(ctx.attrs.assembler_flags),
preprocessor_flags = cmd_args(ctx.attrs.assembler_preprocessor_flags),
dep_files_processor = ctx.attrs._dep_files_processor[RunInfo],
) if ctx.attrs.assembler else None
cuda_info = CudaCompilerInfo(
compiler = ctx.attrs.cuda_compiler[RunInfo],
compiler_type = ctx.attrs.cuda_compiler_type or ctx.attrs.compiler_type,
compiler_flags = cmd_args(ctx.attrs.cuda_compiler_flags),
preprocessor_flags = cmd_args(ctx.attrs.cuda_preprocessor_flags),
) if ctx.attrs.cuda_compiler else None
hip_info = HipCompilerInfo(
compiler = ctx.attrs.hip_compiler[RunInfo],
compiler_type = ctx.attrs.hip_compiler_type or ctx.attrs.compiler_type,
compiler_flags = cmd_args(ctx.attrs.hip_compiler_flags),
preprocessor_flags = cmd_args(ctx.attrs.hip_preprocessor_flags),
) if ctx.attrs.hip_compiler else None
linker_info = LinkerInfo(
archiver = ctx.attrs.archiver[RunInfo],
archiver_supports_argfiles = ctx.attrs.archiver_supports_argfiles,
archiver_type = ctx.attrs.archiver_type,
archive_contents = ctx.attrs.archive_contents,
archive_objects_locally = False,
binary_extension = value_or(ctx.attrs.binary_extension, ""),
link_binaries_locally = not value_or(ctx.attrs.cache_links, True),
link_libraries_locally = False,
link_style = LinkStyle("static"),
link_weight = 1,
link_ordering = ctx.attrs.link_ordering,
linker = ctx.attrs.linker[RunInfo],
linker_flags = cmd_args(ctx.attrs.linker_flags),
lto_mode = LtoMode("none"),
object_file_extension = ctx.attrs.object_file_extension or "o",
shlib_interfaces = "disabled",
independent_shlib_interface_linker_flags = ctx.attrs.shared_library_interface_flags,
requires_archives = value_or(ctx.attrs.requires_archives, True),
requires_objects = value_or(ctx.attrs.requires_objects, False),
supports_distributed_thinlto = ctx.attrs.supports_distributed_thinlto,
shared_dep_runtime_ld_flags = ctx.attrs.shared_dep_runtime_ld_flags,
shared_library_name_format = _get_shared_library_name_format(ctx),
shared_library_versioned_name_format = _get_shared_library_versioned_name_format(ctx),
static_dep_runtime_ld_flags = ctx.attrs.static_dep_runtime_ld_flags,
static_library_extension = ctx.attrs.static_library_extension or "a",
static_pic_dep_runtime_ld_flags = ctx.attrs.static_pic_dep_runtime_ld_flags,
type = ctx.attrs.linker_type,
use_archiver_flags = ctx.attrs.use_archiver_flags,
)
utilities_info = BinaryUtilitiesInfo(
nm = ctx.attrs.nm[RunInfo],
objcopy = ctx.attrs.objcopy_for_shared_library_interface[RunInfo],
ranlib = ctx.attrs.ranlib[RunInfo] if ctx.attrs.ranlib else None,
strip = ctx.attrs.strip[RunInfo],
dwp = None,
bolt_msdk = None,
)
strip_flags_info = StripFlagsInfo(
strip_debug_flags = ctx.attrs.strip_debug_flags,
strip_non_global_flags = ctx.attrs.strip_non_global_flags,
strip_all_flags = ctx.attrs.strip_all_flags,
)
platform_name = ctx.attrs.platform_name or ctx.attrs.name
return [
DefaultInfo(),
] + cxx_toolchain_infos(
platform_name = platform_name,
linker_info = linker_info,
binary_utilities_info = utilities_info,
bolt_enabled = value_or(ctx.attrs.bolt_enabled, False),
c_compiler_info = c_info,
cxx_compiler_info = cxx_info,
asm_compiler_info = asm_info,
as_compiler_info = as_info,
cuda_compiler_info = cuda_info,
hip_compiler_info = hip_info,
header_mode = _get_header_mode(ctx),
headers_as_raw_headers_mode = HeadersAsRawHeadersMode(ctx.attrs.headers_as_raw_headers_mode) if ctx.attrs.headers_as_raw_headers_mode != None else None,
conflicting_header_basename_allowlist = ctx.attrs.conflicting_header_basename_exemptions,
mk_hmap = ctx.attrs._mk_hmap[RunInfo],
mk_comp_db = ctx.attrs._mk_comp_db,
split_debug_mode = SplitDebugMode(ctx.attrs.split_debug_mode),
strip_flags_info = strip_flags_info,
# TODO(T138705365): Turn on dep files by default
use_dep_files = value_or(ctx.attrs.use_dep_files, _get_default_use_dep_files(platform_name)),
)
_APPLE_PLATFORM_NAME_PREFIXES = [
"iphonesimulator",
"iphoneos",
"maccatalyst",
"macosx",
"watchos",
"watchsimulator",
"appletvos",
"appletvsimulator",
]
def _get_default_use_dep_files(platform_name: str.type) -> bool.type:
# All Apple platforms use Clang which supports the standard dep files format
for apple_platform_name_prefix in _APPLE_PLATFORM_NAME_PREFIXES:
if apple_platform_name_prefix in platform_name:
return True
return False
def _get_header_mode(ctx: "context") -> HeaderMode.type:
if ctx.attrs.use_header_map:
if ctx.attrs.private_headers_symlinks_enabled or ctx.attrs.public_headers_symlinks_enabled:
return HeaderMode("symlink_tree_with_header_map")
else:
return HeaderMode("header_map_only")
else:
return HeaderMode("symlink_tree_only")
def _get_shared_library_name_format(ctx: "context") -> str.type:
linker_type = ctx.attrs.linker_type
extension = ctx.attrs.shared_library_extension
if extension == "":
extension = LINKERS[linker_type].default_shared_library_extension
prefix = "" if extension == "dll" else "lib"
return prefix + "{}." + extension
def _get_shared_library_versioned_name_format(ctx: "context") -> str.type:
linker_type = ctx.attrs.linker_type
extension_format = ctx.attrs.shared_library_versioned_extension_format.replace("%s", "{}")
if extension_format == "":
extension_format = LINKERS[linker_type].default_shared_library_versioned_extension_format
prefix = "" if extension_format == "dll" else "lib"
return prefix + "{}." + extension_format

View file

@ -0,0 +1,258 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//cxx:debug.bzl", "SplitDebugMode")
LinkerType = ["gnu", "darwin", "windows"]
# TODO(T110378149): Consider whether it makes sense to move these things to
# configurations/constraints rather than part of the toolchain.
LinkerInfo = provider(fields = [
"archiver",
"archiver_supports_argfiles",
"archiver_type",
"archive_contents",
"archive_objects_locally",
# "archiver_platform",
# "" on Unix, "exe" on Windows
"binary_extension", # str.type
# Whether to run native links locally. We support this for fbcode platforms
# to avoid issues with C++ static links (see comment in
# `platform/cxx_toolchains.bzl` for details).
"link_binaries_locally",
# Whether to run native shared library links locally. For certain use cases
# (e.g., large Apple frameworks), it's more efficient to link locally due
# GiBs of object files (which can also lead to RE errors/timesouts etc).
"link_libraries_locally",
"link_style", # LinkStyle.type
"link_weight", # int.type
"link_ordering", # LinkOrdering.type
"linker",
"linker_flags",
"lto_mode",
"mk_shlib_intf",
# "o" on Unix, "obj" on Windows
"object_file_extension", # str.type
"shlib_interfaces",
"shared_dep_runtime_ld_flags",
# "lib{}.so" on Linux, "lib{}.dylib" on Mac, "{}.dll" on Windows
"shared_library_name_format", # str.type
"shared_library_versioned_name_format", # str.type
"static_dep_runtime_ld_flags",
# "a" on Unix, "lib" on Windows
"static_library_extension", # str.type
"static_pic_dep_runtime_ld_flags",
"requires_archives",
"requires_objects",
"supports_distributed_thinlto",
"independent_shlib_interface_linker_flags",
"type", # of "LinkerType" type
"use_archiver_flags",
"force_full_hybrid_if_capable",
])
BinaryUtilitiesInfo = provider(fields = [
"bolt_msdk",
"dwp",
"nm",
"objcopy",
"ranlib",
"strip",
])
StripFlagsInfo = provider(fields = [
"strip_debug_flags", # [["str"], None]
"strip_non_global_flags", # [["str"], None]
"strip_all_flags", # [["str"], None]
])
# TODO(T110378147): There's a bunch of info encoded in random places in buck
# derived from information in these toolchains but hardcoded (for example,
# which file extensions are preprocessable/compilable). We should figure out
# how to move most of that into these toolchain infos.
# TODO(T110378146): The inclusion of compiler and preprocessor in here is really
# just a legacy thing that was never cleaned up. Historically, buck supported a
# mode where compilation was done in two, explicitly separate phases
# (preprocess and then compile). We don't support that today, and including
# both of these mostly just ends up with added complexity and with us
# duplicating flags in command lines.
# In cxx_library, we support a bunch of different types of files (ex. cuda),
# the toolchain for these follow this common pattern.
_compiler_fields = [
"compiler",
"compiler_type",
"compiler_flags",
"preprocessor",
"preprocessor_type",
"preprocessor_flags",
"dep_files_processor",
]
HipCompilerInfo = provider(fields = _compiler_fields)
CudaCompilerInfo = provider(fields = _compiler_fields)
CCompilerInfo = provider(fields = _compiler_fields)
CxxCompilerInfo = provider(fields = _compiler_fields)
AsmCompilerInfo = provider(fields = _compiler_fields)
AsCompilerInfo = provider(fields = _compiler_fields)
DistLtoToolsInfo = provider(
fields = ["planner", "opt", "prepare", "copy"],
)
# TODO(T110378094): We should consider if we can change this from a hardcoded
# list of compiler_info to something more general. We could maybe do a list of
# compiler_info where each one also declares what extensions it supports.
# TODO(T110378145): Could we split up this Info so that each of the compilers
# could be provided by different dependencies? That would allow a target to
# only depend on the compilers it actually needs.
CxxToolchainInfo = provider(fields = [
"conflicting_header_basename_allowlist",
"use_distributed_thinlto",
"header_mode",
"headers_as_raw_headers_mode",
"linker_info",
"binary_utilities_info",
"c_compiler_info",
"cxx_compiler_info",
"asm_compiler_info",
"as_compiler_info",
"hip_compiler_info",
"cuda_compiler_info",
"mk_comp_db",
"mk_hmap",
"dist_lto_tools_info",
"use_dep_files",
"strip_flags_info",
"split_debug_mode",
"bolt_enabled",
])
# Stores "platform"/flavor name used to resolve *platform_* arguments
CxxPlatformInfo = provider(fields = [
"name",
# List of aliases used to resolve platform_deps
"deps_aliases",
])
def _validate_linker_info(info: LinkerInfo.type):
if info.requires_archives and info.requires_objects:
fail("only one of `requires_archives` and `requires_objects` can be enabled")
if info.supports_distributed_thinlto and not info.requires_objects:
fail("distributed thinlto requires enabling `requires_objects`")
def cxx_toolchain_infos(
platform_name,
c_compiler_info,
cxx_compiler_info,
linker_info,
binary_utilities_info,
header_mode,
headers_as_raw_headers_mode = None,
conflicting_header_basename_allowlist = [],
asm_compiler_info = None,
as_compiler_info = None,
hip_compiler_info = None,
cuda_compiler_info = None,
mk_comp_db = None,
mk_hmap = None,
use_distributed_thinlto = False,
use_dep_files = False,
strip_flags_info = None,
dist_lto_tools_info: [DistLtoToolsInfo.type, None] = None,
split_debug_mode = SplitDebugMode("none"),
bolt_enabled = False,
platform_deps_aliases = []):
"""
Creates the collection of cxx-toolchain Infos for a cxx toolchain.
c and c++ compiler infos are required, as is a linker info. The rest are
optional, and it will be an error if any cxx_library or other rules have srcs
of those other types.
"""
# TODO(T110378099): verify types of the inner info objects.
_validate_linker_info(linker_info)
toolchain_info = CxxToolchainInfo(
conflicting_header_basename_allowlist = conflicting_header_basename_allowlist,
header_mode = header_mode,
headers_as_raw_headers_mode = headers_as_raw_headers_mode,
linker_info = linker_info,
binary_utilities_info = binary_utilities_info,
c_compiler_info = c_compiler_info,
cxx_compiler_info = cxx_compiler_info,
asm_compiler_info = asm_compiler_info,
as_compiler_info = as_compiler_info,
hip_compiler_info = hip_compiler_info,
cuda_compiler_info = cuda_compiler_info,
mk_comp_db = mk_comp_db,
mk_hmap = mk_hmap,
dist_lto_tools_info = dist_lto_tools_info,
use_distributed_thinlto = use_distributed_thinlto,
use_dep_files = use_dep_files,
strip_flags_info = strip_flags_info,
split_debug_mode = split_debug_mode,
bolt_enabled = bolt_enabled,
)
# Provide placeholder mappings, used primarily by cxx_genrule.
# We don't support these buck1 placeholders since we can't take an argument.
# $(ldflags-pic-filter <pattern>)
# $(ldflags-shared-filter <pattern>)
# $(ldflags-static-filter <pattern>)
unkeyed_variables = {
"ar": linker_info.archiver,
"cc": c_compiler_info.compiler,
"cflags": _shell_quote(c_compiler_info.compiler_flags),
"cppflags": _shell_quote(c_compiler_info.preprocessor_flags),
"cxx": cxx_compiler_info.compiler,
"cxxflags": _shell_quote(cxx_compiler_info.compiler_flags),
"cxxppflags": _shell_quote(cxx_compiler_info.preprocessor_flags),
"ld": linker_info.linker,
# NOTE(agallagher): The arg-less variants of the ldflags macro are
# identical, and are just separate to match v1's behavior (ideally,
# we just have a single `ldflags` macro for this case).
"ldflags-shared": _shell_quote(linker_info.linker_flags),
"ldflags-static": _shell_quote(linker_info.linker_flags),
"ldflags-static-pic": _shell_quote(linker_info.linker_flags),
# TODO(T110378148): $(platform-name) is almost unusued. Should we remove it?
"platform-name": platform_name,
}
if as_compiler_info != None:
unkeyed_variables["as"] = as_compiler_info.compiler
unkeyed_variables["asflags"] = _shell_quote(as_compiler_info.compiler_flags)
unkeyed_variables["asppflags"] = _shell_quote(as_compiler_info.preprocessor_flags)
if cuda_compiler_info != None:
unkeyed_variables["cuda"] = cuda_compiler_info.compiler
unkeyed_variables["cudaflags"] = _shell_quote(cuda_compiler_info.compiler_flags)
placeholders_info = TemplatePlaceholderInfo(unkeyed_variables = unkeyed_variables)
return [toolchain_info, placeholders_info, CxxPlatformInfo(name = platform_name, deps_aliases = platform_deps_aliases)]
def _shell_quote(xs):
return cmd_args(xs, quote = "shell")
# export these things under a single "cxx" struct
cxx = struct(
LinkerType = LinkerType,
LinkerInfo = LinkerInfo,
BinaryUtilitiesInfo = BinaryUtilitiesInfo,
HipCompilerInfo = HipCompilerInfo,
CudaCompilerInfo = CudaCompilerInfo,
CCompilerInfo = CCompilerInfo,
CxxCompilerInfo = CxxCompilerInfo,
AsmCompilerInfo = AsmCompilerInfo,
AsCompilerInfo = AsCompilerInfo,
CxxToolchainInfo = CxxToolchainInfo,
CxxPlatformInfo = CxxPlatformInfo,
StripFlagsInfo = StripFlagsInfo,
DistLtoToolsInfo = DistLtoToolsInfo,
cxx_toolchain_infos = cxx_toolchain_infos,
)

View file

@ -0,0 +1,160 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
)
load(
"@prelude//linking:linkables.bzl",
"LinkableProviders",
)
load(
":compile.bzl",
"CxxSrcWithFlags", # @unused Used as a type
)
load(
":headers.bzl",
"CxxHeadersLayout",
)
load(
":link_groups.bzl",
"LinkGroupInfo", # @unused Used as a type
"LinkGroupLibSpec", # @unused Used as a type
)
load(
":linker.bzl",
"SharedLibraryFlagOverrides",
)
load(
":preprocessor.bzl",
"CPreprocessor",
"CPreprocessorInfo",
)
load(
":xcode.bzl",
"cxx_populate_xcode_attributes",
)
# Parameters to control which sub targets to define when processing Cxx rules.
# By default, generates all subtargets.
CxxRuleSubTargetParams = record(
argsfiles = field(bool.type, True),
compilation_database = field(bool.type, True),
headers = field(bool.type, True),
link_group_map = field(bool.type, True),
link_style_outputs = field(bool.type, True),
xcode_data = field(bool.type, True),
)
# Parameters to control which providers to define when processing Cxx rules.
# By default, generates all providers.
CxxRuleProviderParams = record(
compilation_database = field(bool.type, True),
default = field(bool.type, True),
java_packaging_info = field(bool.type, True),
android_packageable_info = field(bool.type, True),
linkable_graph = field(bool.type, True),
link_style_outputs = field(bool.type, True),
merged_native_link_info = field(bool.type, True),
omnibus_root = field(bool.type, True),
preprocessors = field(bool.type, True),
resources = field(bool.type, True),
shared_libraries = field(bool.type, True),
template_placeholders = field(bool.type, True),
preprocessor_for_tests = field(bool.type, True),
)
# Params to handle an argsfile for non-Clang sources which may present in Apple rules.
CxxAdditionalArgsfileParams = record(
file = field("artifact"),
# Hidden args necessary for the argsfile to reference
hidden_args = field([["artifacts", "cmd_args"]]),
# An extension of a file for which this argsfile is generated.
extension = field(str.type),
)
# Parameters to handle non-Clang sources, e.g Swift on Apple's platforms.
CxxRuleAdditionalParams = record(
srcs = field([CxxSrcWithFlags.type], []),
argsfiles = field([CxxAdditionalArgsfileParams.type], []),
external_debug_info = field(["_arglike"], []),
)
# Parameters that allows to configure/extend generic implementation of C++ rules.
# Apple-specific rules (such as `apple_binary` and `apple_library`) and regular C++
# rules (such as `cxx_binary` and `cxx_library`) have too much in common, though
# some aspects of behavior (like layout of headers affecting inclusion of those
# or additional linking flags to support usage of platform frameworks) of are
# different and need to be specified. The following record holds the data which
# is needed to specialize user-facing rule from generic implementation.
CxxRuleConstructorParams = record(
# We need to build an empty shared library for rust_python_extensions so that they can link against the rust shared object
build_empty_so = field(bool.type, False),
# Name of the top level rule utilizing the cxx rule.
rule_type = str.type,
# If the rule is a test.
is_test = field(bool.type, False),
# Header layout to use importing headers.
headers_layout = CxxHeadersLayout.type,
# Additional information used to preprocess every unit of translation in the rule
extra_preprocessors = field([CPreprocessor.type], []),
extra_preprocessors_info = field([CPreprocessorInfo.type], []),
# Additional preprocessor info to export to other rules
extra_exported_preprocessors = field([CPreprocessor.type], []),
# Additional information used to link every object produced by the rule,
# flags are _both_ exported and used to link the target itself.
extra_exported_link_flags = field([""], []),
# Additional flags used _only_ when linking the target itself.
# These flags are _not_ propagated up the dep tree.
extra_link_flags = field([""], []),
# Additional artifacts to be linked together with the cxx compilation output
extra_link_input = field(["artifact"], []),
# Additional args to be used to link the target
extra_link_args = field([LinkArgs.type], []),
# The source files to compile as part of this rule. This list can be generated
# from ctx.attrs with the `get_srcs_with_flags` function.
srcs = field([CxxSrcWithFlags.type]),
additional = field(CxxRuleAdditionalParams.type, CxxRuleAdditionalParams()),
# A function which enables the caller to inject subtargets into the link_style provider
# as well as create custom providers based on the link styles.
link_style_sub_targets_and_providers_factory = field("function", lambda _link_style, _context, _executable, _external_debug_info: ({}, [])),
# Optinal postprocessor used to post postprocess the artifacts
link_postprocessor = field(["cmd_args", None], None),
# Linker flags that tell the linker to create shared libraries, overriding the default shared library flags.
# e.g. when building Apple tests, we want to link with `-bundle` instead of `-shared` to allow
# linking against the bundle loader.
shared_library_flags = field([SharedLibraryFlagOverrides.type, None], None),
# If passed to cxx_library_parameterized, this field will be used to determine
# a shared subtarget's default output should be stripped.
#
# If passed to cxx_executable, this field will be used to determine
# a shared subtarget's default output should be stripped.
strip_executable = field(bool.type, False),
strip_args_factory = field("function", lambda _: cmd_args()),
# Wether to embed the library name as the SONAME.
use_soname = field(bool.type, True),
# Use link group's linking logic regardless whether a link group map's present.
force_link_group_linking = field(bool.type, False),
# Function to use for setting Xcode attributes for the Xcode data sub target.
cxx_populate_xcode_attributes_func = field("function", cxx_populate_xcode_attributes),
# Define which sub targets to generate.
generate_sub_targets = field(CxxRuleSubTargetParams.type, CxxRuleSubTargetParams()),
# Define which providers to generate.
generate_providers = field(CxxRuleProviderParams.type, CxxRuleProviderParams()),
# Force this library to be a Python Omnibus root.
is_omnibus_root = field(bool.type, False),
# Emit an Omnibus shared root for this node even if it's not an Omnibus
# root. This only makes sense to use in tests.
force_emit_omnibus_shared_root = field(bool.type, False),
force_full_hybrid_if_capable = field(bool.type, False),
# Whether shared libs for executables should generate a shared lib link tree.
exe_shared_libs_link_tree = field(bool.type, True),
extra_link_deps = field([LinkableProviders.type], []),
auto_link_group_specs = field([[LinkGroupLibSpec.type], None], None),
link_group_info = field([LinkGroupInfo.type, None], None),
)

View file

@ -0,0 +1,23 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
# Model the various "split" debug scenarios (e.g. `-gsplit-dwarf`).
SplitDebugMode = enum(
# Debug info, if present, is inline in the object file, and will be linked
# into executables and shared libraries (e.g. traditional behavior when
# using `-g`).
"none",
# Debug info. if present is included in the object file, but will *not* be
# linked into executables and shared libraries. This style usually requires
# an additional step, separate from the link, to combine and package debug
# info (e.g. `dSYM`, `dwp`).
"single",
# FIXME(agallagher): Add support for "split", which probably just requires
# modifying `compile_cxx` to add a `.dwo` file as a hidden output in this
# case.
#"split",
)

View file

@ -0,0 +1,253 @@
# Distributed ThinLTO in Buck2
Sean Gillespie, April 2022
This document is a technical overview into Buck2's implementation of a distributed ThinLTO.
Like all rules in Buck2, this implementation is written entirely in Starlark, contained in
`dist_lto.bzl` (in this same directory).
## Motivation
First, I highly recommend watching [Teresa Johnson's CppCon2017 talk about ThinLTO](https://www.youtube.com/watch?v=p9nH2vZ2mNo),
which covers the topics in this section in much greater detail than I can.
C and C++ have long enjoyed significant optimizations at the hands of compilers. However, they have also
long suffered a fundamental limitation; a C or C++ compiler can only optimize code that it sees in a single
translation unit. For a language like C or C++, this means in practice that only code that is included via
the preprocessor or specified in the translation unit can be optimized as a single unit. C and C++ compilers
are unable to inline functions that are defined in different translation units. However, a crucial advantage
of this compilation model is that all C and C++ compiler invocations are *completely parallelizable*; despite
sacrificing some code quality, C and C++ compilation turns into a massively parallel problem with a serial
link step at the very end.
```
flowchart LR;
header.h;
header.h --> a.cpp;
header.h -->|#include| b.cpp;
header.h --> c.cpp;
a.cpp --> a.o;
b.cpp -->|clang++ -O2| b.o;
c.cpp --> c.o;
a.o --> main;
b.o -->|ld| main;
c.o --> main;
```
([Rendered](https://fburl.com/mermaid/rzup8o32). Compilation and optimization of a, b, and c can proceed in parallel.)
In cases where absolute performance is required, though, the inability to perform cross-translation-unit
(or "cross-module", in LLVM parlance) optimizations becomes more of a problem. To solve this, a new compilation
paradigm was designed, dubbed "Link-Time Optimization" (LTO). In this scheme, a compiler will not produce machine code
when processing a translation unit; rather, it will output the compiler's intermediate representation (e.g. LLVM bitcode).
Later on, when it is time for the linker to run, it will load all of the compiler IR into one giant module, run
optimization passes on the mega-module, and produce a final binary from that.
This works quite well, if all that you're looking for is run-time performance. A major drawback of the LTO approach is
that all of the parallelism gained from optimizing translation units individually is now completely lost; instead, the
linker (using a plugin) will do a single-threaded pass of *all code* produced by compilation steps. This is extremely
slow, memory-intensive, and unable to be run incrementally. There are targets at Meta that simply can't be LTO-compiled
because of their size.
```
flowchart LR;
header.h;
header.h --> a.cpp;
header.h -->|#include| b.cpp;
header.h --> c.cpp;
a.cpp --> a.bc;
b.cpp -->|clang++ -O2 -flto -x ir| b.bc;
c.cpp --> c.bc;
a.bc --> a_b_c.bc;
b.bc -->|linker driver| a_b_c.bc;
c.bc --> a_b_c.bc;
a_b_c.bc -->|opt| a_b_c_optimized.bc
a_b_c_optimized.bc -->|codegen| main.o
main.o --> |ld| main
```
([Rendered](https://fburl.com/mermaid/kid35io9). `a.bc`, `b.bc`, and `c.bc` are LLVM bitcode; they are all merged
together into a single module, `a_b_c_optimized.bc`, which is then optimized and codegen'd into a final binary.)
The idea of ThinLTO comes from a desire to maintain the ability to optimize modules in parallel while still
allowing for profitable cross-module optimizations. The idea is this:
1. Just like regular LTO, the compiler emits bitcode instead of machine code. However, it also contains some light
metadata such as a call graph of symbols within the module.
2. The monolithic LTO link is split into three steps: `index`, `opt`, and `link`.
```
flowchart LR;
header.h;
header.h --> a.cpp;
header.h -->|#include| b.cpp;
header.h --> c.cpp;
a.cpp --> a.bc;
b.cpp -->|clang++ -O2 -flto -x ir| b.bc;
c.cpp --> c.bc;
a.bc --> index;
b.bc --> index;
c.bc --> index;
index --> a.thinlto.bc;
index --> b.thinlto.bc;
index --> c.thinlto.bc;
a.thinlto.bc --> a.o;
b.thinlto.bc --> b.o;
b.bc --> a.o;
b.bc --> c.o;
c.thinlto.bc --> c.o;
a.o --> main;
b.o -->|ld| main;
c.o --> main;
```
([Rendered](https://fburl.com/mermaid/56oc99t5))
The `index` step looks like a link step. However, it does not produce a final binary; instead, it looks at every
compiler IR input file that it receives and heuristically determines which other IR modules it should be optimized
with in order to achieve profitable optimizations. These modules might include functions that the index step thinks
probably will get inlined, or globals that are read in the target IR input file. The output of the index step is a
series of files on disk that indicate which sibling object files should be present when optimizing a particular object
file, for each object file in the linker command-line.
The `opt` step runs in parallel for every object file. Each object file will be optimized using the compiler's
optimizer (e.g. `opt`, for LLVM). The optimizer will combine the objects that were referenced as part of the index
step as potentially profitable to include and optimize them all together.
The `link` step takes the outputs of `opt` and links them together, like a normal linker.
In practice, ThinLTO manages to recapture the inherent parallelism of C/C++ compilation by pushing the majority of work
to the parallel `opt` phase of execution. When LLVM performs ThinLTO by default, it will launch a thread pool and process
independent modules in parallel. ThinLTO does not produce as performant a binary as a monolithic LTO; however, in practice,
ThinLTO binaries [paired with AutoFDO](https://fburl.com/wiki/q480euco) perform comparably to monolithic LTO. Furthermore,
ThinLTO's greater efficiency allows for more expensive optimization passes to be run, which can further improve code quality
near that of a monolithic LTO.
This is all great, and ThinLTO has been in use at Meta for some time. However, Buck2 has the ability to take a step
further than Buck1 could ever have - Buck2 can distribute parallel `opt` actions across many machines via Remote Execution
to achieve drastic speedups in ThinLTO wall clock time, memory usage, and incrementality.
## Buck2's Implementation
Buck2's role in a distributed ThinLTO compilation is to construct a graph of actions that directly mirrors the graph
that the `index` step outputs. The graph that the `index` step outputs is entirely dynamic and, as such, the build
system is only aware of what the graph could be after the `index` step is complete. Unlike Buck1 (or even Blaze/Bazel),
Buck2 has explicit support for this paradigm [("dynamic dependencies")](https://fburl.com/gdoc/zklwhkll). Therefore, for Buck2, the basic strategy looks like:
1. Invoke `clang` to act as `index`. `index` will output a file for every object file that indicates what other modules
need to be present when running `opt` on the object file (an "imports file").
2. Read imports files and construct a graph of dynamic `opt` actions whose dependencies mirror the contents of the imports files.
3. Collect the outputs from the `opt` actions and invoke the linker to produce a final binary.
Action `2` is inherently dynamic, since it must read the contents of files produced as part of action `1`. Furthermore,
Buck2's support of `1` is complicated by the fact that certain Buck2 rules can produce an archive of object files as
an output (namely, the Rust compiler). As a result, Buck2's implementation of Distributed ThinLTO is highly dynamic.
Buck2's implementation contains four phases of actions:
1. `thin_lto_prepare`, which specifically handles archives containing LLVM IR and prepares them to be inputs to `thin_lto_index`,
2. `thin_lto_index`, which invokes LLVM's ThinLTO indexer to produce a imports list for every object file to be optimized,
3. `thin_lto_opt`, which optimizes each object file in parallel with its imports present,
4. `thin_lto_link`, which links together the optimized code into a final binary.
### thin_lto_prepare
It is a reality of Buck2 today that some rules don't produce a statically-known list of object files. The list of object
files is known *a priori* during C/C++ compilation, since they have a one-to-one correspondence to source files; however,
the Rust compiler emits an archive of object files; without inspecting the archive, Buck2 has no way of knowing what
the contents of the archive are, or even if they contain bitcode at all.
Future steps (particularly `thin_lto_index`) are defined to only operate on a list of object files - a limitation [inherited from LLVM](https://lists.llvm.org/pipermail/llvm-dev/2019-June/133145.html). Therefore, it is the job of `thin_lto_prepare` to turn an archive into a list of objects - namely, by extracting the archive into a directory.
Buck2 dispatches a `thin_lto_prepare` action for every archive. Each prepare action has two outputs:
1. An **output directory** (called `objects` in the code), a directory that contains the unextracted contents of the archive.
2. A **archive manifest**, a JSON document containing a list of object files that are contained in the output directory.
The core logic of this action is implemented in the Python script `dist_lto_prepare.py`, contained in the `tools` directory. In addition to unpacking each archive, Buck2
keeps track of the list of archives as a Starlark array that will be referenced by index
in later steps.
### thin_lto_index
With all archives prepared, the next step is to invoke LLVM's ThinLTO indexer. For the purposes of Buck2, the indexer
looks like a linker; because of this, Buck2 must construct a reasonable link line. Buck2 does this by iterating over the
list of linkables that it has been given and constructing a link line from them. Uniquely for distributed ThinLTO, Buck2
must wrap all objects that were derived from `thin_lto_prepare` (i.e. were extracted from archives) with `-Wl,--start-lib`
and `-Wl,--end-lib` to ensure that they are still treated as if they were archives by the indexer.
Invoking the indexer is relatively straightforward in that Buck2 invokes it like it would any other linker. However,
once the indexer returns, Buck2 must post-process its output into a format that Buck2's Starlark can understand and
translate into a graph of dynamic `opt` actions. The first thing that Buck2 is write a "meta file" to disk, which
communicates inputs and outputs of `thin_lto_index` to a Python script, `dist_lto_planner.py`. The meta file contains
a list of 7-tuples, whose members are:
1. The path to the source bitcode file. This is used as an index into
a dictionary that records much of the metadata coming
from these lines.
2. The path to an output file. `dist_lto_planner.py`is expected to place a
ThinLTO index file at this location (suffixed `.thinlto.bc`).
3. The path to an output plan. This script is expected to place a link
plan here (a JSON document indicating which other object files this)
object file depends on, among other things.
4. If this object file came from an archive, the index of the archive in
the Starlark archives array.
5. If this object file came from an archive, the name of the archive.
6. If this object file came from an archive, the path to an output plan.
This script is expected to produce an archive link plan here (a JSON)
document similar to the object link plan, except containing link
information for every file in the archive from which this object
came.
7. If this object file came from an archive, the indexes directory of that
archive. This script is expected to place all ThinLTO indexes derived
from object files originating from this archive in that directory.
There are two indices that are derived from this meta file: the object
index (`mapping["index"]`) and the archive index (`mapping["archive_index"]`).
These indices are indices into Starlark arrays for all objects and archive
linkables, respectively. `dist_lto_planner.py` script does not inspect them; rather,
it is expected to communicate these indices back to Starlark by writing them to the
link plan.
`dist_lto_planner.py` reads the index and imports file produced by LLVM and derives
a number of artifacts:
1. For each object file, a `thinlto.bc` file (`bitcode_file`). This file is the same as the input bitcode file, except that LLVM has inserted a number of module imports to refer to the other modules that will be present when the object file is optimized.
2. For each object file, an optimization plan (`plan`). The optimization plan is a JSON document indicating how to construct an `opt` action for this object file. This plan includes
this object file's module imports, whether or not this file contains bitcode at all, a location to place the optimized object file, and a list of archives that this object file imported.
3. For each archive, an optimization plan (`archive_plan`), which contains optimization plans for all of the object files contained within the archive.
This action is a dynamic action because, in the case that there are archives that needed to be preprocessed by `thin_lto_prepare`, this action must read the archive manifest.
### thin_lto_opt
After `thin_lto_index` completes, Buck2 launches `thin_lto_opt` actions for every object file and for every archive. For each object file, Buck2 reads that object file's optimization plan.
At this phase, it is Buck2's responsibility to declare dependencies on every object file referenced by that object's compilation plan; it does so here by adding `hidden` dependencies
on every object file and archive that the archive plan says that this object depends on.
`thin_lto_opt` uses a Python wrapper around LLVM because of a bug (T116695431) where LTO fatal errors don't prevent `clang` from returning an exit code of zero. The Python script wraps
`clang` and exits with a non-zero exit code if `clang` produced an empty object file.
For each archive, Buck2 reads the archive's optimization plan and constructs additional `thin_lto_opt` actions for each object file contained in the archive. Buck2 creates a directory of
symlinks (`opt_objects`) that either contains symlinks to optimized object files (if the object file contained bitcode) or the original object file (if it didn't). The purpose of this symlink directory is to allow the final link to consume object files directly
from this directory without having to know whether they were optimized or not. Paths to these files are passed to the link step
via the optimization manifest (`opt_manifest`).
### thin_lto_link
The final link step. Similar to `thin_lto_index`, this involves creating a link line to feed to the linker that uses the optimized artifacts that we just calculated. In cases where Buck2
would put an archive on the link line, it instead inserts `-Wl,--start-lib`, `-Wl,--end-lib`, and references to the objects in `opt_objects`.

View file

@ -0,0 +1,638 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:paths.bzl", "paths")
load(
"@prelude//cxx:cxx_bolt.bzl",
"bolt",
"cxx_use_bolt",
)
load(
"@prelude//cxx:cxx_link_utility.bzl",
"cxx_link_cmd",
)
load("@prelude//cxx:cxx_toolchain_types.bzl", "CxxToolchainInfo")
load("@prelude//cxx:debug.bzl", "SplitDebugMode")
load(
"@prelude//cxx:dwp.bzl",
"run_dwp_action",
)
load(
"@prelude//linking:link_info.bzl",
"ArchiveLinkable", # @unused Used as a type
"FrameworksLinkable", # @unused Used as a type
"LinkArgs",
"LinkableType",
"LinkedObject",
"ObjectsLinkable", # @unused Used as a type
"SharedLibLinkable", # @unused Used as a type
"append_linkable_args",
"map_to_link_infos",
)
def cxx_dist_link(
ctx: "context",
links: ["LinkArgs"],
# The destination for the link output.
output: "artifact",
linker_map: ["artifact", None] = None,
# A category suffix that will be added to the category of the link action that is generated.
category_suffix: [str.type, None] = None,
# An identifier that will uniquely name this link action in the context of a category. Useful for
# differentiating multiple link actions in the same rule.
identifier: [str.type, None] = None,
# This action will only happen if split_dwarf is enabled via the toolchain.
generate_dwp: bool.type = True,
executable_link: bool.type = True) -> LinkedObject.type:
"""
Perform a distributed thin-lto link into the supplied output
Distributed thinlto splits the link into three stages:
1. global "indexing" step
2. many individual compilation unit optimization steps
3. final global link step
The 2nd and 3rd of those are done just by constructing compiler/linker commands (in dynamic_output
sections) using the output of the first.
For the first, we need to post-process the linker index output to get it into a form
that is easy for us to consume from within bzl.
"""
def make_cat(c: str.type) -> str.type:
""" Used to make sure categories for our actions include the provided suffix """
if category_suffix != None:
return c + "_" + category_suffix
return c
def make_id(i: str.type) -> str.type:
""" Used to make sure identifiers for our actions include the provided identifier """
if identifier != None:
return identifier + "_" + i
return i
recorded_outputs = {}
def name_for_obj(link_name: str.type, object_artifact: "artifact") -> str.type:
""" Creates a unique name/path we can use for a particular object file input """
prefix = "{}/{}".format(link_name, object_artifact.short_path)
# it's possible (though unlikely) that we can get duplicate name/short_path, so just uniquify them
if prefix in recorded_outputs:
recorded_outputs[prefix] += 1
extra = recorded_outputs[prefix]
prefix = "{}-{}".format(prefix, extra)
else:
recorded_outputs[prefix] = 1
return prefix
names = {}
def name_for_link(info: "LinkInfo") -> str.type:
""" Creates a unique name for a LinkInfo that we are consuming """
name = info.name or "unknown"
if name not in names:
names[name] = 1
else:
names[name] += 1
name += "-{}".format(names[name])
return make_id(name)
links = [
LinkArgs(
tset = link.tset,
flags = link.flags,
infos = link.infos,
)
for link in links
]
link_infos = map_to_link_infos(links)
cxx_toolchain = ctx.attrs._cxx_toolchain[CxxToolchainInfo]
lto_planner = cxx_toolchain.dist_lto_tools_info.planner
lto_opt = cxx_toolchain.dist_lto_tools_info.opt
lto_prepare = cxx_toolchain.dist_lto_tools_info.prepare
lto_copy = cxx_toolchain.dist_lto_tools_info.copy
BitcodeLinkData = record(
name = str.type,
initial_object = "artifact",
bc_file = "artifact",
plan = "artifact",
opt_object = "artifact",
)
ArchiveLinkData = record(
name = str.type,
manifest = "artifact",
# A file containing paths to artifacts that are known to reside in opt_objects_dir.
opt_manifest = "artifact",
objects_dir = "artifact",
opt_objects_dir = "artifact",
indexes_dir = "artifact",
plan = "artifact",
link_whole = bool.type,
prepend = bool.type,
)
DataType = enum(
"bitcode",
"archive",
"cmd_args",
)
IndexLinkData = record(
data_type = DataType.type,
link_data = field([BitcodeLinkData.type, ArchiveLinkData.type]),
)
PrePostFlags = record(
pre_flags = list.type,
post_flags = list.type,
)
PREPEND_ARCHIVE_NAMES = [
# T130644072: If linked with `--whole-archive`, Clang builtins must be at the
# front of the argument list to avoid conflicts with identically-named Rust
# symbols from the `compiler_builtins` crate.
#
# Once linking of C++ binaries starts to use `rlib`s, this may no longer be
# necessary, because our Rust `rlib`s won't need to contain copies of
# `compiler_builtins` to begin with, unlike our Rust `.a`s which presently do
# (T130789782).
"clang_rt.builtins",
]
# Information used to construct thinlto index link command:
# Note: The index into index_link_data is important as it's how things will appear in
# the plans produced by indexing. That allows us to map those indexes back to
# the actual artifacts.
index_link_data = []
linkables_index = {}
pre_post_flags = {}
# buildifier: disable=uninitialized
def add_linkable(idx: int.type, linkable: [ArchiveLinkable.type, SharedLibLinkable.type, ObjectsLinkable.type, FrameworksLinkable.type]):
if idx not in linkables_index:
linkables_index[idx] = [linkable]
else:
linkables_index[idx].append(linkable)
# buildifier: disable=uninitialized
def add_pre_post_flags(idx: int.type, flags: PrePostFlags.type):
if idx not in pre_post_flags:
pre_post_flags[idx] = [flags]
else:
pre_post_flags[idx].append(flags)
# Information used to construct the dynamic plan:
plan_inputs = []
plan_outputs = []
# Information used to construct the opt dynamic outputs:
archive_opt_manifests = []
prepare_cat = make_cat("thin_lto_prepare")
for link in link_infos:
link_name = name_for_link(link)
idx = len(index_link_data)
add_pre_post_flags(idx, PrePostFlags(
pre_flags = link.pre_flags,
post_flags = link.post_flags,
))
for linkable in link.linkables:
if linkable._type == LinkableType("objects"):
add_linkable(idx, linkable)
for obj in linkable.objects:
name = name_for_obj(link_name, obj)
bc_output = ctx.actions.declare_output(name + ".thinlto.bc")
plan_output = ctx.actions.declare_output(name + ".opt.plan")
opt_output = ctx.actions.declare_output(name + ".opt.o")
data = IndexLinkData(
data_type = DataType("bitcode"),
link_data = BitcodeLinkData(
name = name,
initial_object = obj,
bc_file = bc_output,
plan = plan_output,
opt_object = opt_output,
),
)
index_link_data.append(data)
plan_outputs.extend([bc_output, plan_output])
elif linkable._type == LinkableType("archive"):
# Our implementation of Distributed ThinLTO operates on individual objects, not archives. Since these
# archives might still contain LTO-able bitcode, we first extract the objects within the archive into
# another directory and write a "manifest" containing the list of objects that the archive contained.
#
# Later actions in the LTO compilation pipeline will read this manifest and dynamically dispatch
# actions on the objects that the manifest reports.
name = name_for_obj(link_name, linkable.archive.artifact)
archive_manifest = ctx.actions.declare_output("%s/%s/manifest.json" % (prepare_cat, name))
archive_objects = ctx.actions.declare_output("%s/%s/objects" % (prepare_cat, name))
archive_opt_objects = ctx.actions.declare_output("%s/%s/opt_objects" % (prepare_cat, name))
archive_indexes = ctx.actions.declare_output("%s/%s/indexes" % (prepare_cat, name))
archive_plan = ctx.actions.declare_output("%s/%s/plan.json" % (prepare_cat, name))
archive_opt_manifest = ctx.actions.declare_output("%s/%s/opt_objects.manifest" % (prepare_cat, name))
prepare_args = cmd_args([
lto_prepare,
"--manifest-out",
archive_manifest.as_output(),
"--objects-out",
archive_objects.as_output(),
"--ar",
cxx_toolchain.linker_info.archiver,
"--archive",
linkable.archive.artifact,
"--name",
name,
])
ctx.actions.run(prepare_args, category = make_cat("thin_lto_prepare"), identifier = name)
data = IndexLinkData(
data_type = DataType("archive"),
link_data = ArchiveLinkData(
name = name,
manifest = archive_manifest,
opt_manifest = archive_opt_manifest,
objects_dir = archive_objects,
opt_objects_dir = archive_opt_objects,
indexes_dir = archive_indexes,
plan = archive_plan,
link_whole = linkable.link_whole,
prepend = link_name in PREPEND_ARCHIVE_NAMES,
),
)
index_link_data.append(data)
archive_opt_manifests.append(archive_opt_manifest)
plan_inputs.extend([archive_manifest, archive_objects])
plan_outputs.extend([archive_indexes, archive_plan])
else:
add_linkable(idx, linkable)
index_argsfile_out = ctx.actions.declare_output(output.basename + ".thinlto.index.argsfile")
final_link_index = ctx.actions.declare_output(output.basename + ".final_link_index")
def dynamic_plan(link_plan: "artifact", index_argsfile_out: "artifact", final_link_index: "artifact"):
def plan(ctx, artifacts, outputs):
# buildifier: disable=uninitialized
def add_pre_flags(idx: int.type):
if idx in pre_post_flags:
for flags in pre_post_flags[idx]:
index_args.add(flags.pre_flags)
# buildifier: disable=uninitialized
def add_post_flags(idx: int.type):
if idx in pre_post_flags:
for flags in pre_post_flags[idx]:
index_args.add(flags.post_flags)
# buildifier: disable=uninitialized
def add_linkables_args(idx: int.type):
if idx in linkables_index:
object_link_arg = cmd_args()
for linkable in linkables_index[idx]:
append_linkable_args(object_link_arg, linkable)
index_args.add(object_link_arg)
# index link command args
prepend_index_args = cmd_args()
index_args = cmd_args()
# See comments in dist_lto_planner.py for semantics on the values that are pushed into index_meta.
index_meta = cmd_args()
# buildifier: disable=uninitialized
for idx, artifact in enumerate(index_link_data):
add_pre_flags(idx)
add_linkables_args(idx)
link_data = artifact.link_data
if artifact.data_type == DataType("bitcode"):
index_meta.add(link_data.initial_object, outputs[link_data.bc_file].as_output(), outputs[link_data.plan].as_output(), str(idx), "", "", "")
elif artifact.data_type == DataType("archive"):
manifest = artifacts[link_data.manifest].read_json()
if not manifest["objects"]:
# Despite not having any objects (and thus not needing a plan), we still need to bind the plan output.
ctx.actions.write(outputs[link_data.plan].as_output(), "{}")
cmd = cmd_args(["/bin/sh", "-c", "mkdir", "-p", outputs[link_data.indexes_dir].as_output()])
ctx.actions.run(cmd, category = make_cat("thin_lto_mkdir"), identifier = link_data.name)
continue
archive_args = prepend_index_args if link_data.prepend else index_args
archive_args.hidden(link_data.objects_dir)
if not link_data.link_whole:
archive_args.add("-Wl,--start-lib")
for obj in manifest["objects"]:
index_meta.add(obj, "", "", str(idx), link_data.name, outputs[link_data.plan].as_output(), outputs[link_data.indexes_dir].as_output())
archive_args.add(obj)
if not link_data.link_whole:
archive_args.add("-Wl,--end-lib")
archive_args.hidden(link_data.objects_dir)
add_post_flags(idx)
# add any link_infos cmd_args that come after the last bitcode or archive
add_pre_flags(len(index_link_data))
add_linkables_args(len(index_link_data))
add_post_flags(len(index_link_data))
index_argfile, _ = ctx.actions.write(
outputs[index_argsfile_out].as_output(),
prepend_index_args.add(index_args),
allow_args = True,
)
index_cat = make_cat("thin_lto_index")
index_file_out = ctx.actions.declare_output(make_id(index_cat) + "/index")
index_out_dir = cmd_args(index_file_out.as_output()).parent()
index_cmd = cxx_link_cmd(ctx)
index_cmd.add(cmd_args(index_argfile, format = "@{}"))
output_as_string = cmd_args(output)
output_as_string.ignore_artifacts()
index_cmd.add("-o", output_as_string)
index_cmd.add(cmd_args(index_file_out.as_output(), format = "-Wl,--thinlto-index-only={}"))
index_cmd.add("-Wl,--thinlto-emit-imports-files")
index_cmd.add("-Wl,--thinlto-full-index")
index_cmd.add(cmd_args(index_out_dir, format = "-Wl,--thinlto-prefix-replace=;{}/"))
# Terminate the index file with a newline.
index_meta.add("")
index_meta_file = ctx.actions.write(
output.basename + ".thinlto.meta",
index_meta,
)
plan_cmd = cmd_args([lto_planner, "--meta", index_meta_file, "--index", index_out_dir, "--link-plan", outputs[link_plan].as_output(), "--final-link-index", outputs[final_link_index].as_output(), "--"])
plan_cmd.add(index_cmd)
plan_extra_inputs = cmd_args()
plan_extra_inputs.add(index_meta)
plan_extra_inputs.add(index_args)
plan_cmd.hidden(plan_extra_inputs)
ctx.actions.run(plan_cmd, category = index_cat, identifier = identifier, local_only = True)
# TODO(T117513091) - dynamic_output does not allow for an empty list of dynamic inputs. If we have no archives
# to process, we will have no dynamic inputs, and the plan action can be non-dynamic.
#
# However, buck2 disallows `dynamic_output` with a empty input list. We also can't call our `plan` function
# directly, since it uses `ctx.outputs` to bind its outputs. Instead of doing Starlark hacks to work around
# the lack of `ctx.outputs`, we declare an empty file as a dynamic input.
plan_inputs.append(ctx.actions.write("plan_hack.txt", ""))
plan_outputs.extend([link_plan, index_argsfile_out, final_link_index])
ctx.actions.dynamic_output(dynamic = plan_inputs, inputs = [], outputs = plan_outputs, f = plan)
link_plan_out = ctx.actions.declare_output(output.basename + ".link-plan.json")
dynamic_plan(link_plan = link_plan_out, index_argsfile_out = index_argsfile_out, final_link_index = final_link_index)
def prepare_opt_flags(link_infos: ["LinkInfo"]) -> "cmd_args":
opt_args = cmd_args()
opt_args.add(cxx_link_cmd(ctx))
# buildifier: disable=uninitialized
for link in link_infos:
for raw_flag in link.pre_flags + link.post_flags:
opt_args.add(raw_flag)
return opt_args
opt_common_flags = prepare_opt_flags(link_infos)
# We declare a separate dynamic_output for every object file. It would
# maybe be simpler to have a single dynamic_output that produced all the
# opt actions, but an action needs to re-run whenever the analysis that
# produced it re-runs. And so, with a single dynamic_output, we'd need to
# re-run all actions when any of the plans changed.
def dynamic_optimize(name: str.type, initial_object: "artifact", bc_file: "artifact", plan: "artifact", opt_object: "artifact"):
def optimize_object(ctx, artifacts, outputs):
plan_json = artifacts[plan].read_json()
# If the object was not compiled with thinlto flags, then there
# won't be valid outputs for it from the indexing, but we still
# need to bind the artifact.
if not plan_json["is_bc"]:
ctx.actions.write(outputs[opt_object], "")
return
opt_cmd = cmd_args(lto_opt)
opt_cmd.add("--out", outputs[opt_object].as_output())
opt_cmd.add("--input", initial_object)
opt_cmd.add("--index", bc_file)
# When invoking opt and llc via clang, clang will not respect IR metadata to generate
# dwo files unless -gsplit-dwarf is explicitly passed in. In other words, even if
# 'splitDebugFilename' set in IR 'DICompileUnit', we still need to force clang to tell
# llc to generate dwo sections.
#
# Local thinlto generates .dwo files by default. For distributed thinlto, however, we
# want to keep all dwo debug info in the object file to reduce the number of files to
# materialize.
if cxx_toolchain.split_debug_mode == SplitDebugMode("single"):
opt_cmd.add("--split-dwarf=single")
# Create an argsfile and dump all the flags to be processed later.
opt_argsfile = ctx.actions.declare_output(outputs[opt_object].basename + ".opt.argsfile")
ctx.actions.write(opt_argsfile.as_output(), opt_common_flags, allow_args = True)
opt_cmd.hidden(opt_common_flags)
opt_cmd.add("--args", opt_argsfile)
opt_cmd.add("--")
opt_cmd.add(cxx_toolchain.cxx_compiler_info.compiler)
imports = [index_link_data[idx].link_data.initial_object for idx in plan_json["imports"]]
archives = [index_link_data[idx].link_data.objects_dir for idx in plan_json["archive_imports"]]
opt_cmd.hidden(imports)
opt_cmd.hidden(archives)
ctx.actions.run(opt_cmd, category = make_cat("thin_lto_opt"), identifier = name)
ctx.actions.dynamic_output(dynamic = [plan], inputs = [], outputs = [opt_object], f = optimize_object)
def dynamic_optimize_archive(archive: ArchiveLinkData.type):
def optimize_archive(ctx, artifacts, outputs):
plan_json = artifacts[archive.plan].read_json()
if "objects" not in plan_json or not plan_json["objects"] or all([not entry["is_bc"] for entry in plan_json["objects"]]):
# Nothing in this directory was lto-able; let's just copy the archive.
ctx.actions.copy_file(outputs[archive.opt_objects_dir], archive.objects_dir)
ctx.actions.write(outputs[archive.opt_manifest], "")
return
output_dir = {}
output_manifest = cmd_args()
for entry in plan_json["objects"]:
base_dir = plan_json["base_dir"]
source_path = paths.relativize(entry["path"], base_dir)
if not entry["is_bc"]:
opt_object = ctx.actions.declare_output("%s/%s" % (make_cat("thin_lto_opt_copy"), source_path))
output_manifest.add(opt_object)
copy_cmd = cmd_args([
lto_copy,
"--to",
opt_object.as_output(),
"--from",
entry["path"],
])
copy_cmd.hidden(archive.objects_dir)
ctx.actions.run(copy_cmd, category = make_cat("thin_lto_opt_copy"), identifier = source_path)
output_dir[source_path] = opt_object
continue
opt_object = ctx.actions.declare_output("%s/%s" % (make_cat("thin_lto_opt"), source_path))
output_manifest.add(opt_object)
output_dir[source_path] = opt_object
opt_cmd = cmd_args(lto_opt)
opt_cmd.add("--out", opt_object.as_output())
opt_cmd.add("--input", entry["path"])
opt_cmd.add("--index", entry["bitcode_file"])
if cxx_toolchain.split_debug_mode == SplitDebugMode("single"):
opt_cmd.add("--split-dwarf=single")
opt_argsfile = ctx.actions.declare_output(opt_object.basename + ".opt.argsfile")
ctx.actions.write(opt_argsfile.as_output(), opt_common_flags, allow_args = True)
opt_cmd.add("--args", opt_argsfile)
opt_cmd.add("--")
opt_cmd.add(cxx_toolchain.cxx_compiler_info.compiler)
imports = [index_link_data[idx].link_data.initial_object for idx in entry["imports"]]
archives = [index_link_data[idx].link_data.objects_dir for idx in entry["archive_imports"]]
opt_cmd.hidden(imports)
opt_cmd.hidden(archives)
opt_cmd.hidden(archive.indexes_dir)
opt_cmd.hidden(archive.objects_dir)
ctx.actions.run(opt_cmd, category = make_cat("thin_lto_opt"), identifier = source_path)
ctx.actions.symlinked_dir(outputs[archive.opt_objects_dir], output_dir)
ctx.actions.write(outputs[archive.opt_manifest], output_manifest, allow_args = True)
archive_opt_inputs = [archive.plan]
archive_opt_outputs = [archive.opt_objects_dir, archive.opt_manifest]
ctx.actions.dynamic_output(dynamic = archive_opt_inputs, inputs = [], outputs = archive_opt_outputs, f = optimize_archive)
for artifact in index_link_data:
link_data = artifact.link_data
if artifact.data_type == DataType("bitcode"):
dynamic_optimize(
name = link_data.name,
initial_object = link_data.initial_object,
bc_file = link_data.bc_file,
plan = link_data.plan,
opt_object = link_data.opt_object,
)
elif artifact.data_type == DataType("archive"):
dynamic_optimize_archive(link_data)
linker_argsfile_out = ctx.actions.declare_output(output.basename + ".thinlto.link.argsfile")
def thin_lto_final_link(ctx, artifacts, outputs):
plan = artifacts[link_plan_out].read_json()
link_args = cmd_args()
plan_index = {int(k): v for k, v in plan["index"].items()}
# non_lto_objects are the ones that weren't compiled with thinlto
# flags. In that case, we need to link against the original object.
non_lto_objects = {int(k): 1 for k in plan["non_lto_objects"]}
current_index = 0
opt_objects = []
archives = []
for link in link_infos:
link_args.add(link.pre_flags)
for linkable in link.linkables:
if linkable._type == LinkableType("objects"):
new_objs = []
for obj in linkable.objects:
if current_index in plan_index:
new_objs.append(index_link_data[current_index].link_data.opt_object)
opt_objects.append(index_link_data[current_index].link_data.opt_object)
elif current_index in non_lto_objects:
new_objs.append(obj)
opt_objects.append(obj)
current_index += 1
linkable = ObjectsLinkable(
objects = new_objs,
linker_type = linkable.linker_type,
link_whole = linkable.link_whole,
)
# TODO(T113841827): @christylee enable link_groups for distributed_thinlto
elif linkable._type == LinkableType("archive"):
current_index += 1
link_args.add(link.post_flags)
link_cmd = cxx_link_cmd(ctx)
final_link_argfile, final_link_inputs = ctx.actions.write(
outputs[linker_argsfile_out].as_output(),
link_args,
allow_args = True,
)
# buildifier: disable=uninitialized
for artifact in index_link_data:
if artifact.data_type == DataType("archive"):
link_cmd.hidden(artifact.link_data.opt_objects_dir)
link_cmd.add(cmd_args(final_link_argfile, format = "@{}"))
link_cmd.add(cmd_args(final_link_index, format = "@{}"))
link_cmd.add("-o", outputs[output].as_output())
link_cmd_extra_inputs = cmd_args()
link_cmd_extra_inputs.add(final_link_inputs)
link_cmd.hidden(link_cmd_extra_inputs)
link_cmd.hidden(link_args)
link_cmd.hidden(opt_objects)
link_cmd.hidden(archives)
ctx.actions.run(link_cmd, category = make_cat("thin_lto_link"), identifier = identifier, local_only = True)
final_link_inputs = [link_plan_out, final_link_index] + archive_opt_manifests
ctx.actions.dynamic_output(
dynamic = final_link_inputs,
inputs = [],
outputs = [output] + ([linker_map] if linker_map else []) + [linker_argsfile_out],
f = thin_lto_final_link,
)
final_output = output if not (executable_link and cxx_use_bolt(ctx)) else bolt(ctx, output, identifier)
dwp_output = ctx.actions.declare_output(output.short_path.removesuffix("-wrapper") + ".dwp") if generate_dwp else None
if generate_dwp:
run_dwp_action(
ctx = ctx,
obj = final_output,
identifier = identifier,
category_suffix = category_suffix,
referenced_objects = final_link_inputs,
dwp_output = dwp_output,
# distributed thinlto link actions are ran locally, run llvm-dwp locally as well to
# ensure all dwo source files are available
local_only = True,
allow_huge_dwp = ctx.attrs.allow_huge_dwp,
)
return LinkedObject(
output = final_output,
prebolt_output = output,
dwp = dwp_output,
linker_argsfile = linker_argsfile_out,
index_argsfile = index_argsfile_out,
)

View file

@ -0,0 +1,29 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//cxx:cxx_toolchain_types.bzl", "DistLtoToolsInfo")
def _impl(ctx):
return [
DefaultInfo(),
DistLtoToolsInfo(
planner = ctx.attrs.planner[RunInfo],
prepare = ctx.attrs.prepare[RunInfo],
opt = ctx.attrs.opt[RunInfo],
copy = ctx.attrs.copy[RunInfo],
),
]
dist_lto_tools = rule(
impl = _impl,
attrs = {
"copy": attrs.dep(),
"opt": attrs.dep(),
"planner": attrs.dep(),
"prepare": attrs.dep(),
},
)

View file

@ -0,0 +1,44 @@
load("@prelude//cxx/dist_lto:tools.bzl", "dist_lto_tools")
prelude = native
prelude.python_bootstrap_binary(
name = "dist_lto_planner",
main = "dist_lto_planner.py",
visibility = ["PUBLIC"],
)
prelude.python_bootstrap_binary(
name = "dist_lto_opt",
main = "dist_lto_opt.py",
visibility = ["PUBLIC"],
)
prelude.python_bootstrap_binary(
name = "dist_lto_prepare",
main = "dist_lto_prepare.py",
visibility = ["PUBLIC"],
)
prelude.python_bootstrap_binary(
name = "dist_lto_copy",
main = "dist_lto_copy.py",
visibility = ["PUBLIC"],
)
dist_lto_tools(
name = "dist_lto_tools",
planner = ":dist_lto_planner",
opt = ":dist_lto_opt",
prepare = ":dist_lto_prepare",
copy = ":dist_lto_copy",
visibility = ["PUBLIC"],
)
prelude.python_test(
name = "test_dist_lto_opt",
srcs = [
"tests/test_dist_lto_opt.py",
"dist_lto_opt.py",
],
)

View file

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
import argparse
import shutil
import sys
from typing import List
def main(argv: List[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--to")
parser.add_argument("--from", dest="from_")
args = parser.parse_args(argv[1:])
shutil.copy(args.from_, args.to)
return 0
sys.exit(main(sys.argv))

View file

@ -0,0 +1,216 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
"""
Python wrapper around `clang` intended for use by the parallel opt phase of
a Distributed ThinLTO compilation. This script works around a LLVM bug where
LLVM will return a zero exit code in the case where ThinLTO fails with a
fatal error.
Instead of trusting the exit code of the compiler, this script checks the
output file and returns 1 if the file has zero size.
"""
import argparse
import os
import subprocess
import sys
from typing import List
EXIT_SUCCESS, EXIT_FAILURE = 0, 1
# Filter opt related flags
def _filter_flags(clang_flags: List[str]) -> List[str]: # noqa: C901
# List of llvm flags to be ignored.
# They either don't have an valid mapping or unused during opt.
IGNORE_OPT_FLAGS = [
"-Wl,-plugin-opt,-function-sections",
"-Wl,--lto-whole-program-visibility",
"-Wl,--no-lto-whole-program-visibility",
]
# Conservatively, we only translate llvms flags in our known list
KNOWN_LLVM_SHARED_LIBRARY_FLAGS = ["-shared"]
# Start with default flags for opt.
# The default values may change across compiler versions.
# Make sure they are always synced with the current values.
opt_flags = [
# TODO(T139459294):
# -O2 is the default optimization flag for the link-time optimizer
# this setting matches current llvm implementation:
# https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/LTO/Config.h#L57
"-O2",
# TODO(T139459170): Remove after clang-15. NPM is the default.
"-fexperimental-new-pass-manager",
"-ffunction-sections",
"-fdata-sections",
]
# Clang driver passes through lld flags with "-Wl," prefix. There are 4 type of flags with unique
# prefixes:
# 1. "--lto-...": these are native lld flags.
# 2. "-plugin-opt,..." or "-plugin-opt=...": these are the aliases of the native lld flags (1).
# 3. "-mllvm,...": these are llvm flags.
# 4. "-plugin-opt,-..." or "-plugin-opt=-...": these are the aliases of llvm flags (3). Note that they differ from (2) and always start with "-".
#
# For (1) and (2), we need to convert them case by case.
# For (3) and (4), we should be able to pass them through into the optimizer directly by prefixing "-mllvm".
# TODO(T139448744): Cover all the flags. Check available flags using "ld.lld --help | grep -A 1 '\-\-plugin-opt='"
PLUGIN_OPT_PREFIXES = ["-Wl,-plugin-opt,", "-Wl,-plugin-opt="]
def _find_plugin_opt_prefix(flag: str) -> str:
matched_prefix = [
prefix for prefix in PLUGIN_OPT_PREFIXES if flag.startswith(prefix)
]
if matched_prefix:
return matched_prefix[0]
return ""
plugin_opt_to_llvm_flag_map = {
"sample-profile=": "-fprofile-sample-use=",
"O": "-O",
}
def _plugin_opt_to_clang_flag(flag: str) -> str:
for k, v in plugin_opt_to_llvm_flag_map.items():
if flag.startswith(k):
return flag.replace(k, v)
return None
for raw_flag in clang_flags:
flag = raw_flag.replace('"', "")
if flag in IGNORE_OPT_FLAGS:
continue
if _find_plugin_opt_prefix(flag):
# Convert "-Wl,-plugin-opt,...".
flag = flag.replace(_find_plugin_opt_prefix(flag), "", 1)
if flag.startswith("-"):
# If flag starts with "-", it is an llvm flag. Pass it through directly.
opt_flags.extend(["-mllvm", flag])
else:
flag = _plugin_opt_to_clang_flag(flag)
if flag is None:
# Bail on any unknown flag.
print(f"error: unrecognized flag {raw_flag}")
return None
opt_flags.append(flag)
elif flag.startswith("-Wl,-mllvm,"):
# Convert "-Wl,-mllvm,...". It is an llvm flag. Pass it through directly.
flag = flag.replace("-Wl,-mllvm,", "", 1)
opt_flags.extend(["-mllvm", flag])
elif flag in KNOWN_LLVM_SHARED_LIBRARY_FLAGS:
# The target is a shared library, `-fPIC` is needed in opt phase to correctly generate PIC ELF.
opt_flags.append("-fPIC")
return opt_flags
# Clean up clang flags by obtaining the cc1 flags and filtering out those unwanted.
# clang_opt_flags is mutated after calling this function.
def _cleanup_flags(clang_opt_flags: List[str]) -> List[str]:
for i, arg in enumerate(clang_opt_flags):
if arg.startswith("--cc="):
# Find the clang binary path.
clang_opt_flags[i] = arg.replace("--cc=", "")
break
# Get the cc1 flag dump with '-###'
try:
output = (
subprocess.check_output(
clang_opt_flags + ["-###"], stderr=subprocess.STDOUT
)
.decode()
.splitlines()
)
except subprocess.CalledProcessError as e:
print(e.output.decode())
return None
# Flags that may conflict with the existing bitcode attributes.
# The value indicates if the flag is followed with a value.
flags_to_delete = {
"-mframe-pointer=none": False,
"-fmath-errno": False,
"-fno-rounding-math": False,
"-mconstructor-aliases": False,
"-munwind-tables": False,
"-target-cpu": True,
"-tune-cpu": True,
}
clean_output = []
skip_next = False
for f in output[-1].split()[1:]:
if skip_next:
skip_next = False
else:
f = f.strip('"')
if f in flags_to_delete:
skip_next = flags_to_delete[f]
else:
clean_output.append(f)
return clean_output
def main(argv: List[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--out", help="The output native object file.")
parser.add_argument("--input", help="The input bitcode object file.")
parser.add_argument("--index", help="The thinlto index file.")
parser.add_argument("--split-dwarf", required=False, help="Split dwarf option.")
parser.add_argument(
"--args", help="The argsfile containing unfiltered and unprocessed flags."
)
parser.add_argument("--debug", action="store_true", help="Dump clang -cc1 flags.")
parser.add_argument("opt_args", nargs=argparse.REMAINDER)
args = parser.parse_args(argv[1:])
with open(args.args, "r") as argsfile:
clang_opt_flags = _filter_flags(argsfile.read().splitlines())
if clang_opt_flags is None:
return EXIT_FAILURE
clang_opt_flags.extend(
[
"-o",
args.out,
"-x",
"ir",
"-c",
args.input,
f"-fthinlto-index={args.index}",
]
)
if args.split_dwarf:
clang_opt_flags.append(f"-gsplit-dwarf={args.split_dwarf}")
# The following args slices manipulating may be confusing. The first 3 element of opt_args are:
# 1. a spliter "--", it's not used anywhere;
# 2. the fbcc wrapper script path
# 3. the "-cc" arg pointing to the compiler we use
# EXAMPLE: ['--', 'buck-out/v2/gen/fbcode/8e3db19fe005003a/tools/build/buck/wrappers/__fbcc__/fbcc', '--cc=fbcode/third-party-buck/platform010/build/llvm-fb/12/bin/clang++', '--target=x86_64-redhat-linux-gnu', ...]
clang_cc1_flags = _cleanup_flags(args.opt_args[2:] + clang_opt_flags)
if clang_cc1_flags is None:
return EXIT_FAILURE
fbcc_cmd = args.opt_args[1:3] + clang_cc1_flags
if args.debug:
# Print fbcc commandline and exit.
print(" ".join(fbcc_cmd))
return EXIT_SUCCESS
subprocess.check_call(fbcc_cmd)
if os.stat(args.out).st_size == 0:
print("error: opt produced empty file")
return EXIT_FAILURE
return EXIT_SUCCESS
if __name__ == "__main__":
sys.exit(main(sys.argv))

View file

@ -0,0 +1,318 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
"""
A simple wrapper around a distributed thinlto index command to fit into buck2's
distributed thinlto build.
This reads in a couple of things:
1. The "meta" file. This is a list of tuples of (object file, index output,
plan output). All items are line-separated (so each tuple is three lines).
2. The index and link plan output paths
3. The commands for the actual index command.
It will invoke the index command and then copy the index outputs to the
requested locations and write a plan for each of those objects. This "plan" is
a simple json file with the most important thing being a list of the indices
of the imports needed for that file.
It will then additionally write a link plan, which is just a translation of
the thinlto index (which lists the objects actually needed for the final link).
Both opt and link plans use indices to refer to other files because it allows the bzl
code to easily map back to other objects held in buck memory.
"""
# pyre-unsafe
import argparse
import json
import os
import os.path
import subprocess
import sys
from typing import List
def _get_argsfile(args) -> str:
# go through the flags passed to linker and find the index argsfile
argsfiles = list(
filter(lambda arg: arg.endswith("thinlto.index.argsfile"), args.index_args)
)
assert (
len(argsfiles) == 1
), f"expect only 1 argsfile but seeing multiple ones: {argsfiles}"
argsfile = argsfiles[0]
if argsfile.startswith("@"):
argsfile = argsfile[1:]
return argsfile
def _extract_lib_search_path(argsfile_path: str) -> List[str]:
lib_search_path = []
with open(argsfile_path) as argsfile:
for line in argsfile:
if line.startswith("-L"):
lib_search_path.append(line.strip())
return lib_search_path
def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument("--meta")
parser.add_argument("--index")
parser.add_argument("--link-plan")
parser.add_argument("--final-link-index")
parser.add_argument("index_args", nargs=argparse.REMAINDER)
args = parser.parse_args(argv[1:])
subprocess.check_call(args.index_args[1:])
bitcode_suffix = ".thinlto.bc"
imports_suffix = ".imports"
opt_objects_suffix = ".opt.o" # please note the files are not exist yet, this is to generate the index file use in final link
with open(args.meta) as meta:
meta_lines = [line.strip() for line in meta.readlines()]
def read_imports(path, imports_path):
with open(imports_path) as infile:
return [line.strip() for line in infile.readlines()]
def index_path(path):
return os.path.join(args.index, path)
# The meta file comes directly from dist_lto.bzl and consists of a list of
# 7-tuples of information. It is easiest for us to write each tuple member
# as a separate line in Starlark, so these 7-tuples are encoded in groups
# of seven lines.
#
# The seven pieces of information are:
# 1. The path to the source bitcode file. This is used as an index into
# a dictionary (`mapping`) that records much of the metadata coming
# from these lines.
# 2. The path to an output bitcode file. This script is expected to place a
# ThinLTO index file at this location (suffixed `.thinlto.bc`).
# 3. The path to an output plan. This script is expected to place a link
# plan here (a JSON document indicating which other object files this)
# object file depends on, among other things.
# 4. The link data's index in the Starlark array.
# 5. If this object file came from an archive, the name of the archive. Otherwise,
# this line is empty.
# 6. If this object file came from an archive, the path to an output plan.
# This script is expected to produce an archive link plan here (a JSON)
# document similar to the object link plan, except containing link
# information for every file in the archive from which this object
# came. Otherwise, this line is empty.
# 7. If this object file came from an archive, the indexes directory of that
# archive. This script is expected to place all ThinLTO indexes derived
# from object files originating from this archive in that directory.
# Otherwise, this line is empty.
#
# There are two indices that are derived from this meta file: the object
# index (mapping["index"]) and the archive index (mapping["archive_index"]).
# These indices are indices into Starlark arrays for all objects and archive
# linkables, respectively. This script does not inspect them.
mapping = {}
archives = {}
for i in range(0, len(meta_lines), 7):
path = meta_lines[i]
output = meta_lines[i + 1]
plan_output = meta_lines[i + 2]
idx = int(meta_lines[i + 3])
archive_name = meta_lines[i + 4]
archive_plan = meta_lines[i + 5]
archive_index_dir = meta_lines[i + 6]
archive_idx = idx if output == "" else None # archives do not have outputs
mapping[path] = {
"output": output,
"plan_output": plan_output,
"index": idx,
"archive_index": archive_idx,
"archive_name": archive_name,
}
if archive_idx is not None:
archives[idx] = {
"name": archive_name,
"objects": [],
"plan": archive_plan,
"index_dir": archive_index_dir,
}
non_lto_objects = {}
for path, data in sorted(mapping.items(), key=lambda v: v[0]):
output_loc = data["output"]
if os.path.exists(output_loc):
continue
if data["archive_index"] is not None:
archives[data["archive_index"]]["objects"].append(path)
continue
bc_file = index_path(path) + bitcode_suffix
imports_path = index_path(path) + imports_suffix
os.makedirs(os.path.dirname(output_loc), exist_ok=True)
if os.path.exists(imports_path):
assert os.path.exists(bc_file), "missing bc file for %s" % path
os.rename(bc_file, output_loc)
imports = read_imports(path, imports_path)
imports_list = []
archives_list = []
for path in imports:
entry = mapping[path]
if entry["archive_index"] is not None:
archives_list.append(int(entry["archive_index"]))
else:
imports_list.append(entry["index"])
plan = {
"imports": imports_list,
"archive_imports": archives_list,
"index": data["index"],
"bitcode_file": bc_file,
"path": path,
"is_bc": True,
}
else:
non_lto_objects[data["index"]] = 1
with open(output_loc, "w"):
pass
plan = {
"is_bc": False,
}
with open(data["plan_output"], "w") as planout:
json.dump(plan, planout, sort_keys=True)
for archive in archives.values():
# For archives, we must produce a plan that provides Starlark enough
# information about how to launch a dynamic opt for each object file
# in the archive.
archive_plan = {}
# This is convenient to store, since it's difficult for Starlark to
# calculate it.
archive_plan["base_dir"] = os.path.dirname(archive["plan"])
object_plans = []
for obj in archive["objects"]:
imports_path = index_path(obj) + imports_suffix
output_path = archive["index_dir"]
os.makedirs(output_path, exist_ok=True)
if os.path.exists(imports_path):
bc_file = index_path(obj) + bitcode_suffix
os.rename(bc_file, os.path.join(output_path, os.path.basename(bc_file)))
imports = read_imports(path, imports_path)
imports_list = []
archives_list = []
for path in imports:
entry = mapping[path]
if entry["archive_index"] is not None:
archives_list.append(int(entry["archive_index"]))
else:
imports_list.append(entry["index"])
object_plans.append(
{
"is_bc": True,
"path": obj,
"imports": imports_list,
"archive_imports": archives_list,
"bitcode_file": os.path.join(
output_path, os.path.basename(bc_file)
),
}
)
else:
object_plans.append(
{
"is_bc": False,
"path": obj,
}
)
archive_plan["objects"] = object_plans
with open(archive["plan"], "w") as planout:
json.dump(archive_plan, planout, sort_keys=True)
# We read the `index`` and `index.full`` files produced by linker in index stage
# and translate them to 2 outputs:
# 1. A link plan build final_link args. (This one may be able to be removed if we refactor the workflow)
# 2. A files list (*.final_link_index) used for final link stage which includes all the
# files needed. it's based on index.full with some modification, like path updates
# and redundent(added by toolchain) dependencies removing.
index = {}
index_files_set = set()
# TODO(T130322878): since we call linker wrapper twice (in index and in final_link), to avoid these libs get
# added twice we remove them from the index file for now.
KNOWN_REMOVABLE_DEPS_SUFFIX = [
"glibc/lib/crt1.o",
"glibc/lib/crti.o",
"crtbegin.o",
"crtbeginS.o",
".build_info.o",
"crtend.o",
"crtendS.o",
"glibc/lib/crtn.o",
]
with open(index_path("index")) as indexfile:
for line in indexfile:
line = line.strip()
index_files_set.add(line)
path = os.path.relpath(line, start=args.index)
index[mapping[path]["index"]] = 1
with open(args.link_plan, "w") as outfile:
json.dump(
{
"non_lto_objects": non_lto_objects,
"index": index,
},
outfile,
indent=2,
sort_keys=True,
)
# Append all search path flags (e.g -Lfbcode/third-party-buck/platform010/build/glibc/lib) from argsfile to final_index
# this workaround is to make dist_lto compatible with link_group. see T136415235 for more info
argsfile = _get_argsfile(args)
lib_search_path = _extract_lib_search_path(argsfile)
# build index file for final link use
with open(index_path("index.full")) as full_index_input, open(
args.final_link_index, "w"
) as final_link_index_output:
final_link_index_output.write("\n".join(lib_search_path) + "\n")
for line in full_index_input:
line = line.strip()
if any(filter(line.endswith, KNOWN_REMOVABLE_DEPS_SUFFIX)):
continue
path = os.path.relpath(line, start=args.index)
if line in index_files_set:
if mapping[path]["output"]:
# handle files that were not extracted from archives
output = mapping[path]["output"].replace(
bitcode_suffix, opt_objects_suffix
)
final_link_index_output.write(output + "\n")
elif os.path.exists(index_path(path) + imports_suffix):
# handle files built from source that were extracted from archives
opt_objects_path = path.replace(
"/objects/", "/opt_objects/objects/"
)
final_link_index_output.write(opt_objects_path + "\n")
else:
# handle pre-built archives
final_link_index_output.write(line + "\n")
else:
# handle input files that did not come from linker input, e.g. linkerscirpts
final_link_index_output.write(line + "\n")
sys.exit(main(sys.argv))

View file

@ -0,0 +1,162 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
"""
Prepares for an object-only ThinLTO link by extracting a given archive and
producing a manifest of the objects contained within.
"""
import argparse
import enum
import json
import os
import subprocess
import sys
from typing import List, Tuple
class ArchiveKind(enum.IntEnum):
UNKNOWN = 0
ARCHIVE = 1
THIN_ARCHIVE = 2
def _gen_path(parent_path: str, filename: str) -> str:
# concat file path and check the file exist before return
obj_path = os.path.join(parent_path, filename)
assert os.path.exists(obj_path)
return obj_path
def _gen_filename(filename: str, num_of_instance: int) -> str:
# generate the filename based on the instance,
# for 1st instance, it's file.o
# for 2nd instance, it's file_1.o
if num_of_instance > 1:
basename, extension = os.path.splitext(filename)
return f"{basename}_{num_of_instance-1}{extension}"
else:
return filename
def identify_file(path: str) -> Tuple[ArchiveKind, str]:
path = os.path.realpath(path)
output = subprocess.check_output(["file", "-b", path]).decode()
if "ar archive" in output:
return (ArchiveKind.ARCHIVE, output)
elif "thin archive" in output:
return (ArchiveKind.THIN_ARCHIVE, output)
else:
with open(path, "rb") as infile:
head = infile.read(7)
if head == "!<thin>".encode():
return (ArchiveKind.THIN_ARCHIVE, output)
return (ArchiveKind.UNKNOWN, output)
def main(argv: List[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--manifest-out")
parser.add_argument("--objects-out")
parser.add_argument("--ar")
parser.add_argument("--name")
parser.add_argument("--archive")
args = parser.parse_args(argv[1:])
objects_path = args.objects_out
os.makedirs(objects_path, exist_ok=True)
known_objects = []
file_type, debug_output = identify_file(args.archive)
if file_type == ArchiveKind.ARCHIVE:
# Unfortunately, we use llvm-ar and, while binutils ar has had --output for
# a long time, llvm-ar does not support --output and the change in llvm-ar
# looks like it has stalled for years (https://reviews.llvm.org/D69418)
# So, we need to invoke ar in the directory that we want it to extract into, and so
# need to adjust some paths.
ar_path = os.path.relpath(args.ar, start=objects_path)
archive_path = os.path.relpath(args.archive, start=objects_path)
output = subprocess.check_output(
[ar_path, "t", archive_path], cwd=objects_path
).decode()
member_list = [member for member in output.split("\n") if member]
if len(set(member_list)) != len(member_list):
# duplication detected
counter = {}
for member in member_list:
current = counter.get(member, 0) + 1
counter[member] = current
if current == 1:
# just extract the file
output = subprocess.check_output(
[ar_path, "xN", str(current), archive_path, member],
cwd=objects_path,
).decode()
assert not output
known_objects.append(_gen_path(objects_path, member))
else:
# llvm doesn't allow --output so we need this clumsiness
tmp_filename = "tmp"
current_file = _gen_filename(member, current)
# rename current 'member' file to tmp
output = subprocess.check_output(
["mv", member, tmp_filename], cwd=objects_path
).decode()
assert not output
# extract the file from archive
output = subprocess.check_output(
[ar_path, "xN", str(current), archive_path, member],
cwd=objects_path,
).decode()
assert not output
# rename the newly extracted file
output = subprocess.check_output(
["mv", member, current_file], cwd=objects_path
).decode()
assert not output
# rename the tmp file back to 'member'
output = subprocess.check_output(
["mv", tmp_filename, member], cwd=objects_path
).decode()
assert not output
known_objects.append(_gen_path(objects_path, current_file))
else:
# no duplicated filename
output = subprocess.check_output(
[ar_path, "xv", archive_path], cwd=objects_path
).decode()
for line in output.splitlines():
assert line.startswith("x - ")
obj = line[4:]
known_objects.append(_gen_path(objects_path, obj))
elif file_type == ArchiveKind.THIN_ARCHIVE:
output = subprocess.check_output([args.ar, "t", args.archive]).decode()
for line in output.splitlines():
assert os.path.exists(line)
known_objects.append(line)
elif file_type == ArchiveKind.UNKNOWN:
raise AssertionError(
f"unknown archive kind for file {args.archive}: {debug_output}"
)
manifest = {
"debug": debug_output,
"objects": known_objects,
}
with open(os.path.join(args.manifest_out), "w") as outfile:
json.dump(manifest, outfile, indent=2, sort_keys=True)
return 0
sys.exit(main(sys.argv))

View file

@ -0,0 +1,276 @@
#!/usr/bin/env fbpython
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
import unittest
from cxx.dist_lto.tools.dist_lto_opt import _filter_flags
class TestDistLtoOpt(unittest.TestCase):
def test_filter_flags(self):
inputs = [
"-not-opt-flag",
"-Wl,-plugin-opt,-llvm_flag1",
"-Wl,-plugin-opt=-llvm_flag1_different_format",
"-Wl,-plugin-opt,sample-profile=/fbcode/abc/autofdo.profile",
"-Wl,--lto-whole-program-visibility",
"-Wl,-mllvm,-llvm-flag2",
"-Wl,-plugin-opt,O3",
"-Wl,-plugin-opt,-function-sections",
"-Wl,-mllvm,-hot-callsite-threshold=12000",
"-shared",
]
flags = _filter_flags(inputs)
self.assertListEqual(
flags,
[
"-O2",
"-fexperimental-new-pass-manager",
"-ffunction-sections",
"-fdata-sections",
"-mllvm",
"-llvm_flag1",
"-mllvm",
"-llvm_flag1_different_format",
"-fprofile-sample-use=/fbcode/abc/autofdo.profile",
"-mllvm",
"-llvm-flag2",
"-O3",
"-mllvm",
"-hot-callsite-threshold=12000",
"-fPIC",
],
)
def test_filter_flags_hhvm_case_rev_0f8618f31(self):
inputs = [
"--target=x86_64-redhat-linux-gnu",
"-nostdinc",
"-resource-dir",
"fbcode/third-party-buck/platform010/build/llvm-fb/12/lib/clang/stable",
"-idirafter",
"fbcode/third-party-buck/platform010/build/llvm-fb/12/lib/clang/stable/include",
"-idirafter",
"fbcode/third-party-buck/platform010/build/glibc/include",
"-idirafter",
"fbcode/third-party-buck/platform010/build/kernel-headers/include",
"-Bfbcode/third-party-buck/platform010/build/binutils/x86_64-facebook-linux/bin",
"--cflag=--target=x86_64-redhat-linux-gnu",
"--ar=fbcode/third-party-buck/platform010/build/llvm-fb/12/bin/llvm-ar",
"-Bfbcode/third-party-buck/platform010/build/glibc/lib",
"-Bfbcode/third-party-buck/platform010/tools/gcc/lib/gcc/x86_64-redhat-linux-gnu/trunk",
"-Lfbcode/third-party-buck/platform010/build/libgcc/lib/gcc/x86_64-facebook-linux/trunk",
"-Wl,-nostdlib",
"-Wl,--dynamic-linker,/usr/local/fbcode/platform010/lib/ld.so",
"-Wl,--disable-new-dtags",
"-Bfbcode/third-party-buck/platform010/build/binutils/x86_64-facebook-linux/bin",
"-Bbuck-out/v2/gen/fbcode/8e3db19fe005003a/third-party-buck/platform010/build/llvm-fb/12/__lld_path__/lld_path/bin",
"-Wl,--no-mmap-output-file",
"-nodefaultlibs",
"--target=x86_64-redhat-linux-gnu",
"-Lfbcode/third-party-buck/platform010/build/glibc/lib",
"-Lfbcode/third-party-buck/platform010/build/libgcc/lib",
"-Wl,-z,notext",
"-Wl,-z,relro",
"-Wl,--gc-sections",
"-fuse-ld=lld",
"-Wl,--discard-section=.nv_fatbin",
"-Wl,--discard-section=.nvFatBinSegment",
"-Wl,--discard-section=.rela.debug_info",
"-Wl,--discard-section=.rela.debug_ranges",
"-Wl,--discard-section=.rela.debug_loc",
"-Wl,--discard-section=.rela.debug_line",
"-Wl,--discard-section=.rela.debug_aranges",
"-Wl,--discard-section=.rela.debug_types",
"-Wl,-O1",
"-Wl,--build-id=sha1",
"-Wl,-mllvm,-hot-callsite-threshold=12000",
"-Wl,--lto-whole-program-visibility",
"-fwhole-program-vtables",
"-fexperimental-new-pass-manager",
"-Wl,--no-discard-section=.nv_fatbin",
"-Wl,--no-discard-section=.nvFatBinSegment",
"fbcode/tools/build/move_gpu_sections_implicit_linker_script.txt",
"-fuse-ld=lld",
"--build-info=full",
"--build-info-build-mode=opt-hhvm-lto",
"--build-info-build-tool=buck2",
"--build-info-compiler=clang",
"--build-info-fdo-profile=",
"--build-info-platform=platform010",
"--build-info-rule=fbcode:scripts/fnz/minimal:minimal",
"--build-info-rule-type=cpp_binary",
"-flto=thin",
"-Wl,-plugin-opt,-function-sections",
"-Wl,-plugin-opt,-profile-guided-section-prefix=false",
"-Wl,-plugin-opt,-generate-type-units",
"-Wl,-plugin-opt,-enable-lto-ir-verification=false",
"-Xlinker",
"--export-dynamic-symbol=mallocx",
"-Xlinker",
"--export-dynamic-symbol=rallocx",
"-Xlinker",
"--export-dynamic-symbol=xallocx",
"-Xlinker",
"--export-dynamic-symbol=sallocx",
"-Xlinker",
"--export-dynamic-symbol=dallocx",
"-Xlinker",
"--export-dynamic-symbol=sdallocx",
"-Xlinker",
"--export-dynamic-symbol=nallocx",
"-Xlinker",
"--export-dynamic-symbol=mallctl",
"-Xlinker",
"--export-dynamic-symbol=mallctlnametomib",
"-Xlinker",
"--export-dynamic-symbol=mallctlbymib",
"-Xlinker",
"--export-dynamic-symbol=malloc_stats_print",
"-Xlinker",
"--export-dynamic-symbol=malloc_usable_size",
"-Xlinker",
"--export-dynamic-symbol=malloc_message",
]
flags = _filter_flags(inputs)
self.assertIsNotNone(flags)
self.assertListEqual(
flags,
[
"-O2",
"-fexperimental-new-pass-manager",
"-ffunction-sections",
"-fdata-sections",
"-mllvm",
"-hot-callsite-threshold=12000",
"-mllvm",
"-profile-guided-section-prefix=false",
"-mllvm",
"-generate-type-units",
"-mllvm",
"-enable-lto-ir-verification=false",
],
)
def test_filter_flags_unicorn_case_rev_0f8618f31(self):
inputs = [
"--ld=fbcode/third-party-buck/platform010/build/llvm-fb/12/bin/clang++",
"--cc=buck-out/v2/gen/fbcode/8e3db19fe005003a/tools/build/buck/wrappers/__fbcc__/fbcc --cc=fbcode/third-party-buck/platform010/build/llvm-fb/12/bin/clang --target=x86_64-redhat-linux-gnu -nostdinc -resource-dir fbcode/third-party-buck/platform010/build/llvm-fb/12/lib/clang/stable -idirafter fbcode/third-party-buck/platform010/build/llvm-fb/12/lib/clang/stable/include -idirafter fbcode/third-party-buck/platform010/build/glibc/include -idirafter fbcode/third-party-buck/platform010/build/kernel-headers/include -Bfbcode/third-party-buck/platform010/build/binutils/x86_64-facebook-linux/bin",
"--cflag=--target=x86_64-redhat-linux-gnu",
"--ar=fbcode/third-party-buck/platform010/build/llvm-fb/12/bin/llvm-ar",
"-Bfbcode/third-party-buck/platform010/build/glibc/lib",
"-Bfbcode/third-party-buck/platform010/tools/gcc/lib/gcc/x86_64-redhat-linux-gnu/trunk",
"-Lfbcode/third-party-buck/platform010/build/libgcc/lib/gcc/x86_64-facebook-linux/trunk",
"-Wl,-nostdlib",
"-Wl,--dynamic-linker,/usr/local/fbcode/platform010/lib/ld.so",
"-Wl,--disable-new-dtags",
"-Bfbcode/third-party-buck/platform010/build/binutils/x86_64-facebook-linux/bin",
"-Bbuck-out/v2/gen/fbcode/8e3db19fe005003a/third-party-buck/platform010/build/llvm-fb/12/__lld_path__/lld_path/bin",
"-Wl,--no-mmap-output-file",
"-nodefaultlibs",
"--target=x86_64-redhat-linux-gnu",
"-Lfbcode/third-party-buck/platform010/build/glibc/lib",
"-Lfbcode/third-party-buck/platform010/build/libgcc/lib",
"-Wl,-z,notext",
"-Wl,-z,relro",
"-Wl,--gc-sections",
"-fuse-ld=lld",
"-Wl,--discard-section=.nv_fatbin",
"-Wl,--discard-section=.nvFatBinSegment",
"-Wl,--discard-section=.rela.debug_info",
"-Wl,--discard-section=.rela.debug_ranges",
"-Wl,--discard-section=.rela.debug_loc",
"-Wl,--discard-section=.rela.debug_line",
"-Wl,--discard-section=.rela.debug_aranges",
"-Wl,--discard-section=.rela.debug_types",
"-Wl,-O1",
"-Wl,--build-id=sha1",
"-fexperimental-new-pass-manager",
"-Xlinker",
"-znow",
"-Xlinker",
"--emit-relocs",
"--build-info=full",
"--build-info-build-mode=opt-clang-thinlto",
"--build-info-build-tool=buck2",
"--build-info-compiler=clang",
"--build-info-fdo-profile=fbcode//fdo/autofdo/default_profile:autofdo",
"--build-info-platform=platform010",
"--build-info-rule=fbcode:unicorn:index_server",
"--build-info-rule-type=cpp_binary",
"-flto=thin",
"-Wl,-plugin-opt,sample-profile=buck-out/v2/gen/fbcode/40fc99293b37c503/fdo/autofdo/default_profile/__autofdo__/out/profile",
"-Wl,-plugin-opt,-function-sections",
"-Wl,-plugin-opt,-profile-guided-section-prefix=false",
"-Wl,-plugin-opt,-generate-type-units",
"-Wl,-plugin-opt,-enable-lto-ir-verification=false",
"-Xlinker",
"--push-state",
"-Xlinker",
"--no-as-needed",
"-Xlinker",
"--pop-state",
"-Wl,--undefined,Global",
"-Wl,--undefined,Local",
"-Wl,--undefined,MockConnection",
"-Wl,--undefined,Global",
"-Wl,--undefined,Local",
"-Wl,--undefined,MockConnection",
"-Xlinker",
"--start-group",
"fbcode/third-party-buck/platform010/build/IntelComposerXE/mkl/lib/intel64/libmkl_intel_lp64.a",
"fbcode/third-party-buck/platform010/build/IntelComposerXE/mkl/lib/intel64/libmkl_core.a",
"fbcode/third-party-buck/platform010/build/IntelComposerXE/mkl/lib/intel64/libmkl_intel_thread.a",
"-Xlinker",
"--end-group",
"-lpthread",
"-Xlinker",
"--export-dynamic-symbol=mallocx",
"-Xlinker",
"--export-dynamic-symbol=rallocx",
"-Xlinker",
"--export-dynamic-symbol=xallocx",
"-Xlinker",
"--export-dynamic-symbol=sallocx",
"-Xlinker",
"--export-dynamic-symbol=dallocx",
"-Xlinker",
"--export-dynamic-symbol=sdallocx",
"-Xlinker",
"--export-dynamic-symbol=nallocx",
"-Xlinker",
"--export-dynamic-symbol=mallctl",
"-Xlinker",
"--export-dynamic-symbol=mallctlnametomib",
"-Xlinker",
"--export-dynamic-symbol=mallctlbymib",
"-Xlinker",
"--export-dynamic-symbol=malloc_stats_print",
"-Xlinker",
"--export-dynamic-symbol=malloc_usable_size",
"-Xlinker",
"--export-dynamic-symbol=malloc_message",
]
flags = _filter_flags(inputs)
self.assertIsNotNone(flags)
self.assertListEqual(
flags,
[
"-O2",
"-fexperimental-new-pass-manager",
"-ffunction-sections",
"-fdata-sections",
"-fprofile-sample-use=buck-out/v2/gen/fbcode/40fc99293b37c503/fdo/autofdo/default_profile/__autofdo__/out/profile",
"-mllvm",
"-profile-guided-section-prefix=false",
"-mllvm",
"-generate-type-units",
"-mllvm",
"-enable-lto-ir-verification=false",
],
)

View file

@ -0,0 +1,78 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:local_only.bzl", "link_cxx_binary_locally")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
def dwp_available(ctx: "context"):
dwp = get_cxx_toolchain_info(ctx).binary_utilities_info.dwp
return dwp != None
def run_dwp_action(
ctx: "context",
obj: "artifact",
identifier: [str.type, None],
category_suffix: [str.type, None],
referenced_objects: ["_arglike", ["artifact"]],
dwp_output: "artifact",
local_only: bool.type,
allow_huge_dwp: bool.type = False):
args = cmd_args()
dwp = get_cxx_toolchain_info(ctx).binary_utilities_info.dwp
args.add("/bin/sh", "-c", '"$1" {}-o "$2" -e "$3" && touch "$2"'.format("--continue-on-cu-index-overflow " if allow_huge_dwp else ""), "")
args.add(dwp, dwp_output.as_output(), obj)
# All object/dwo files referenced in the library/executable are implicitly
# processed by dwp.
args.hidden(referenced_objects)
category = "dwp"
if category_suffix != None:
category += "_" + category_suffix
ctx.actions.run(
args,
category = category,
identifier = identifier,
local_only = local_only,
)
def dwp(
ctx: "context",
# Executable/library to extra dwo paths from.
obj: "artifact",
# An identifier that will uniquely name this link action in the context of a category. Useful for
# differentiating multiple link actions in the same rule.
identifier: [str.type, None],
# A category suffix that will be added to the category of the link action that is generated.
category_suffix: [str.type, None],
# All `.o`/`.dwo` paths referenced in `obj`.
# TODO(T110378122): Ideally, referenced objects are a list of artifacts,
# but currently we don't track them properly. So, we just pass in the full
# link line and extract all inputs from that, which is a bit of an
# overspecification.
referenced_objects: ["_arglike", ["artifact"]],
# whether to enable dangerous option to allow huge dwp file. DWARF specs says dwp file
# should be less than 4GB to ensure a valid .debug_cu_index, llvm-dwp errors out on huge
# dwp file. allow_huge_dwp will toggle option to turn error to warning.
allow_huge_dwp: bool.type = False) -> "artifact":
# gdb/lldb expect to find a file named $file.dwp next to $file.
output = ctx.actions.declare_output(obj.short_path + ".dwp")
run_dwp_action(
ctx,
obj,
identifier,
category_suffix,
referenced_objects,
output,
# dwp produces ELF files on the same size scale as the corresponding @obj.
# The files are a concatentation of input DWARF debug info.
# Caching dwp has the same issues as caching binaries, so use the same local_only policy.
local_only = link_cxx_binary_locally(ctx),
allow_huge_dwp = allow_huge_dwp,
)
return output

View file

@ -0,0 +1,237 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//linking:link_info.bzl",
"Linkage",
)
load(
"@prelude//utils:build_target_pattern.bzl",
"BuildTargetPattern",
"label_matches_build_target_pattern",
"parse_build_target_pattern",
)
load(
"@prelude//utils:graph_utils.bzl",
"breadth_first_traversal_by",
)
# Types of group traversal
Traversal = enum(
# Includes the target and all of it's transitive dependencies in the group.
"tree",
# Includes only the target in the group.
"node",
)
# Optional type of filtering
FilterType = enum(
# Filters for targets with labels matching the regex pattern defined after `label:`.
"label",
# Filters for targets for the build target pattern defined after "pattern:".
"pattern",
)
# Label for special group mapping which makes every target associated with it to be included in all groups
MATCH_ALL_LABEL = "MATCH_ALL"
# Label for special group mapping which makes every target associated with it to be linked directly
# against the final binary
NO_MATCH_LABEL = "NO_MATCH"
GroupRoot = record(
label = "label",
# Data provided by the group (e.g. linkable graph and shared libs).
node = "_a",
)
# Representation of a parsed group mapping
GroupMapping = record(
# The root to apply this mapping to.
root = field([GroupRoot.type, None], None),
# The type of traversal to use.
traversal = field(Traversal.type, Traversal("tree")),
# Optional filter type to apply to the traversal. If present,
# either `label_regex` or `build_target_pattern` is required.
filter_type = field([FilterType.type, None], None),
# Optional label regex filter to apply to the traversal. If present,
# the `filter_type` is required.
label_regex = field(["regex", None], None),
# Optional build target pattern to apply to the traversal. If present,
# the `filter_type` is required.
build_target_pattern = field([BuildTargetPattern.type, None], None),
# Preferred linkage for this target when added to a link group.
preferred_linkage = field([Linkage.type, None], None),
)
# Representation of a parsed group
Group = record(
# The name for this group.
name = str.type,
# The mappings that are part of this group.
mappings = [GroupMapping.type],
)
GroupsMappings = record(
groups = [Group.type],
mappings = {"label": str.type},
)
def parse_groups_definitions(map: list.type, dep_to_node: "function" = lambda d: d) -> [Group.type]:
groups = []
for name, mappings in map:
parsed_mappings = []
for entry in mappings:
traversal = _parse_traversal_from_mapping(entry[1])
filter_type, label_regex, build_target_pattern = _parse_filter_from_mapping(entry[2])
root = None
if entry[0] != None:
root = GroupRoot(
label = entry[0].label,
node = dep_to_node(entry[0]),
)
mapping = GroupMapping(
root = root,
traversal = traversal,
filter_type = filter_type,
label_regex = label_regex,
build_target_pattern = build_target_pattern,
preferred_linkage = Linkage(entry[3]) if len(entry) > 3 and entry[3] else None,
)
parsed_mappings.append(mapping)
group = Group(name = name, mappings = parsed_mappings)
groups.append(group)
return groups
def _parse_traversal_from_mapping(entry: str.type) -> Traversal.type:
if entry == "tree":
return Traversal("tree")
elif entry == "node":
return Traversal("node")
else:
fail("Unrecognized group traversal type: " + entry)
def _parse_filter_from_mapping(entry: [str.type, None]) -> [(FilterType.type, "regex", None), (FilterType.type, None, BuildTargetPattern.type), (None, None, None)]:
filter_type = None
label_regex = None
build_target_pattern = None
if entry:
# We need the anchors "^"" and "$" because experimental_regex match anywhere in the text,
# while we want full text match for group label text.
if entry.startswith("label"):
filter_type = FilterType("label")
label_regex = experimental_regex("^{}$".format(entry[6:]))
elif entry.startswith("tag"):
filter_type = FilterType("label")
label_regex = experimental_regex("^{}$".format(entry[4:]))
elif entry.startswith("pattern"):
filter_type = FilterType("pattern")
build_target_pattern = parse_build_target_pattern(entry[8:])
else:
fail("Invalid group mapping filter: {}\nFilter must begin with `label:` or `pattern:`.".format(entry))
return filter_type, label_regex, build_target_pattern
def compute_mappings(groups: [Group.type], graph_map: {"label": "_b"}) -> {"label": str.type}:
"""
Returns the group mappings {target label -> group name} based on the provided groups and graph.
"""
if not groups:
return {}
target_to_group_map = {}
node_traversed_targets = {}
for group in groups:
for mapping in group.mappings:
targets_in_mapping = _find_targets_in_mapping(graph_map, mapping)
if not targets_in_mapping and group.name != NO_MATCH_LABEL:
warning("Could not find any targets for mapping: `{}` in group: `{}`".format(mapping, group.name))
continue
for target in targets_in_mapping:
_update_target_to_group_mapping(graph_map, target_to_group_map, node_traversed_targets, group.name, mapping, target)
return target_to_group_map
def _find_targets_in_mapping(
graph_map: {"label": "_b"},
mapping: GroupMapping.type) -> ["label"]:
# If we have no filtering, we don't need to do any traversal to find targets to include.
if mapping.filter_type == None:
if mapping.root == None:
fail("no filter or explicit root given: {}", mapping)
return [mapping.root.label]
# Else find all dependencies that match the filter.
matching_targets = {}
def matches_target(
target, # "label"
labels) -> bool.type: # labels: [str.type]
if mapping.filter_type == FilterType("label"):
# Use a for loop to avoid creating a temporary array in a BFS.
for label in labels:
if mapping.label_regex.match(label):
return True
return False
else:
return label_matches_build_target_pattern(target, mapping.build_target_pattern)
def find_matching_targets(node): # "label" -> ["label"]:
graph_node = graph_map[node]
if matches_target(node, graph_node.labels):
matching_targets[node] = None
if mapping.traversal == Traversal("tree"):
# We can stop traversing the tree at this point because we've added the
# build target to the list of all targets that will be traversed by the
# algorithm that applies the groups.
return []
return graph_node.deps + graph_node.exported_deps
if mapping.root == None:
for node in graph_map:
find_matching_targets(node)
else:
breadth_first_traversal_by(graph_map, [mapping.root.label], find_matching_targets)
return matching_targets.keys()
# Types removed to avoid unnecessary type checking which degrades performance.
def _update_target_to_group_mapping(
graph_map, # {"label": "_b"}
target_to_group_map, #: {"label": str.type}
node_traversed_targets, #: {"label": None}
group, # str.type,
mapping, # GroupMapping.type,
target): # "label"
def assign_target_to_group(
target: "label",
node_traversal: bool.type) -> bool.type:
# If the target hasn't already been assigned to a group, assign it to the
# first group claiming the target. Return whether the target was already assigned.
if target not in target_to_group_map:
target_to_group_map[target] = group
if node_traversal:
node_traversed_targets[target] = None
return False
else:
return True
def transitively_add_targets_to_group_mapping(node: "label") -> ["label"]:
previously_processed = assign_target_to_group(target = node, node_traversal = False)
# If the node has been previously processed, and it was via tree (not node), all child nodes have been assigned
if previously_processed and node not in node_traversed_targets:
return []
graph_node = graph_map[node]
return graph_node.deps + graph_node.exported_deps
if mapping.traversal == Traversal("node"):
assign_target_to_group(target = target, node_traversal = True)
else: # tree
breadth_first_traversal_by(graph_map, [target], transitively_add_targets_to_group_mapping)

View file

@ -0,0 +1,319 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:paths.bzl", "paths")
load("@prelude//utils:utils.bzl", "expect", "from_named_set", "value_or")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
load(":platform.bzl", "cxx_by_platform")
# Defines the varying bits of implementation affecting on how the end user
# should include the headers.
# Given there are 2 headers which are defined:
# a) one header in a list, as ["foo/bar/foobar.h"]
# b) one header in a dict (aka named header), as {"wfh/baz.h": "path/header.h"}
#
# `apple`:
# 1) header from the list should be included as NAMESPACE/PATH_BASENAME:
# #include "namespace/foobar.h"
# 2) header from the dict should be included as DICT_KEY (aka header name):
# #include "wfh/baz.h"
# 3) it should be possible to include list header from the same target via basename:
# #include "foobar.h"
#
# `regular`:
# 1) header from the list should be included as NAMESPACE/PATH:
# #include "namespace/foo/bar/foobar.h"
# 2) header from the dict should be included as NAMESPACE/DICT_KEY:
# #include "namespace/wfh/baz.h"
CxxHeadersNaming = enum("apple", "regular")
# Modes supporting implementing the `headers` parameter of C++ rules using raw
# headers instead of e.g. symlink trees.
HeadersAsRawHeadersMode = enum(
# Require that all headers be implemented as raw headers, failing if this
# is not possible.
"required",
# Attempt to implement headers via raw headers, falling to header maps or
# symlink tress when raw headers cannot be used (e.g. rule contains a
# generated header or remaps a header to an incompatible location in the
# header namespace).
"preferred",
"disabled",
)
HeaderMode = enum(
# Creates the header map that references the headers directly in the source
# tree.
"header_map_only",
# Creates the tree of symbolic links of headers.
"symlink_tree_only",
# Creates the tree of symbolic links of headers and creates the header map
# that references the symbolic links to the headers.
"symlink_tree_with_header_map",
)
HeaderStyle = enum(
"local",
"system",
)
Headers = record(
include_path = field("cmd_args"),
# NOTE(agallagher): Used for module hack replacement.
symlink_tree = field(["artifact", None], None),
)
CHeader = record(
# `"artifact"` pointing to the actual header file
artifact = "artifact",
# Basename as it should appear in include directive
name = str.type,
# Prefix before the basename as it should appear in include directive
namespace = str.type,
# Whether or not this header is provided via dict, where the corresponding key is a new name
named = bool.type,
)
# Parameters controlling the varying aspects of headers-related behavior.
# The contract on how headers could be used (i.e. end user inclusion rules)
# is different for `apple_library` and `cxx_library`. Those parameters
# allows generalizing the C++ rules implementation and are provided
# by top-level user-facing wrappers around those generalized methods.
CxxHeadersLayout = record(
# Prefix part of the header path in the include statement. Header name might
# not always be prepended by the namespace, `naming` parameter is controlling
# that behavior. The value is ready to be used and abstracts different naming
# for such prefix in user-facing attributes (e.g. `apple_binary.header_path_prefix`
# vs `cxx_binary.header_namespace`) and different default values when those
# attributes are omitted (package path for regular C++ rules vs target name for
# Apple-specific rules).
namespace = str.type,
# Selects the behavior in the implementation to support the specific way of how
# headers are allowed to be included (e.g. if header namespace is applied for
# headers from dicts). For more information see comment for `CxxHeadersNaming`
naming = CxxHeadersNaming.type,
)
CPrecompiledHeaderInfo = provider(fields = [
# Actual precompiled header ready to be used during compilation, "artifact"
"header",
])
def cxx_attr_header_namespace(ctx: "context") -> str.type:
return value_or(ctx.attrs.header_namespace, ctx.label.package)
def cxx_attr_exported_headers(ctx: "context", headers_layout: CxxHeadersLayout.type) -> [CHeader.type]:
headers = _get_attr_headers(ctx.attrs.exported_headers, headers_layout.namespace, headers_layout.naming)
platform_headers = _get_attr_headers(_headers_by_platform(ctx, ctx.attrs.exported_platform_headers), headers_layout.namespace, headers_layout.naming)
return headers + platform_headers
def cxx_attr_headers(ctx: "context", headers_layout: CxxHeadersLayout.type) -> [CHeader.type]:
headers = _get_attr_headers(ctx.attrs.headers, headers_layout.namespace, headers_layout.naming)
platform_headers = _get_attr_headers(_headers_by_platform(ctx, ctx.attrs.platform_headers), headers_layout.namespace, headers_layout.naming)
return headers + platform_headers
def cxx_get_regular_cxx_headers_layout(ctx: "context") -> CxxHeadersLayout.type:
namespace = cxx_attr_header_namespace(ctx)
return CxxHeadersLayout(namespace = namespace, naming = CxxHeadersNaming("regular"))
def cxx_attr_exported_header_style(ctx: "context") -> HeaderStyle.type:
return HeaderStyle(ctx.attrs.exported_header_style)
def _get_attr_headers(xs: "", namespace: str.type, naming: CxxHeadersNaming.type) -> [CHeader.type]:
if type(xs) == type([]):
return [CHeader(artifact = x, name = _get_list_header_name(x, naming), namespace = namespace, named = False) for x in xs]
else:
return [CHeader(artifact = xs[x], name = x, namespace = _get_dict_header_namespace(namespace, naming), named = True) for x in xs]
def _headers_by_platform(ctx: "context", xs: [(str.type, "")]) -> "":
res = {}
for deps in cxx_by_platform(ctx, xs):
res.update(from_named_set(deps))
return res
def as_raw_headers(
ctx: "context",
headers: {str.type: "artifact"},
mode: HeadersAsRawHeadersMode.type) -> [["label_relative_path"], None]:
"""
Return the include directories needed to treat the given headers as raw
headers, depending on the given `HeadersAsRawHeadersMode` mode.
Args:
mode:
disabled - always return `None`
preferred - return `None` if conversion isn't possible
required - fail if conversion isn't possible
"""
# If we're not supporting raw header conversion, return `None`.
if mode == HeadersAsRawHeadersMode("disabled"):
return None
return _as_raw_headers(
ctx,
headers,
# Don't fail if conversion isn't required.
no_fail = mode != HeadersAsRawHeadersMode("required"),
)
def prepare_headers(ctx: "context", srcs: {str.type: "artifact"}, name: str.type) -> [Headers.type, None]:
"""
Prepare all the headers we want to use, depending on the header_mode
set on the target's toolchain.
- In the case of a header map, we create a `name`.hmap file and
return it as part of the include path.
- In the case of a symlink tree, we create a directory of `name`
containing the headers and return it as part of the include path.
"""
if len(srcs) == 0:
return None
header_mode = get_cxx_toolchain_info(ctx).header_mode
# TODO(T110378135): There's a bug in clang where using header maps w/o
# explicit `-I` anchors breaks module map lookups. This will be fixed
# by https://reviews.llvm.org/D103930 so, until it lands, disable header
# maps when we see a module map.
if (header_mode == HeaderMode("symlink_tree_with_header_map") and
any([paths.basename(n) == "module.modulemap" for n in srcs.keys()])):
header_mode = HeaderMode("symlink_tree_only")
if header_mode == HeaderMode("header_map_only"):
hmap = _mk_hmap(ctx, name, {h: (a, "{}") for h, a in srcs.items()})
return Headers(
include_path = cmd_args(hmap).hidden(srcs.values()),
)
symlink_dir = ctx.actions.symlinked_dir(name, _normalize_header_srcs(srcs))
if header_mode == HeaderMode("symlink_tree_only"):
return Headers(include_path = cmd_args(symlink_dir), symlink_tree = symlink_dir)
if header_mode == HeaderMode("symlink_tree_with_header_map"):
hmap = _mk_hmap(ctx, name, {h: (symlink_dir, "{}/" + h) for h in srcs})
return Headers(
include_path = cmd_args(hmap).hidden(symlink_dir),
symlink_tree = symlink_dir,
)
fail("Unsupported header mode: {}".format(header_mode))
def _normalize_header_srcs(srcs: dict.type) -> dict.type:
normalized_srcs = {}
for key, val in srcs.items():
normalized_key = paths.normalize(key)
stored_val = normalized_srcs.get(normalized_key, None)
expect(
stored_val == None or stored_val == val,
"Got different values {} and {} for the same normalized header {}".format(
val,
stored_val,
normalized_key,
),
)
normalized_srcs[normalized_key] = val
return normalized_srcs
def _as_raw_headers(
ctx: "context",
headers: {str.type: "artifact"},
# Return `None` instead of failing.
no_fail: bool.type = False) -> [["label_relative_path"], None]:
"""
Return the include directories needed to treat the given headers as raw
headers.
"""
# Find the all the include dirs needed to treat the given headers as raw
# headers.
inc_dirs = {}
for name, header in headers.items():
inc_dir = _as_raw_header(
ctx,
name,
header,
no_fail = no_fail,
)
# If the conversion wasn't possible, `inc_dir` will be `None` and we
# should bail now.
if inc_dir == None:
return None
inc_dirs[inc_dir] = None
return [ctx.label.path.add(p) for p in inc_dirs]
def _as_raw_header(
ctx: "context",
# The full name used to include the header.
name: str.type,
header: "artifact",
# Return `None` instead of failing.
no_fail: bool.type = False) -> [str.type, None]:
"""
Return path to pass to `include_directories` to treat the given header as
a raw header.
"""
# We can't handle generated headers.
if not header.is_source:
if no_fail:
return None
fail("generated headers cannot be used as raw headers ({})"
.format(header))
# To include the header via its name using raw headers and include dirs,
# it needs to be a suffix of its original path, and we'll strip the include
# name to get the include dir used to include it.
path = paths.join(ctx.label.package, header.short_path)
base = paths.strip_suffix(path, name)
if base == None:
if no_fail:
return None
fail("header name must be a path suffix of the header path to be " +
"used as a raw header ({} => {})".format(name, header))
# If the include dir is underneath our package, then just relativize to find
# out package-relative path.
if len(base) > len(ctx.label.package):
return paths.relativize(base, ctx.label.package)
# Otherwise, this include dir needs to reference a parent dir.
expect(ctx.label.package.startswith(base))
num_parents = (
len(ctx.label.package.split("/")) -
(0 if not base else len(base.split("/")))
)
return "/".join([".."] * num_parents)
def _get_list_header_name(header: "artifact", naming: CxxHeadersNaming.type) -> str.type:
if naming.value == "regular":
return header.short_path
elif naming.value == "apple":
return header.basename
else:
fail("Unsupported header naming: {}".format(naming))
def _get_dict_header_namespace(namespace: str.type, naming: CxxHeadersNaming.type) -> str.type:
if naming.value == "regular":
return namespace
elif naming.value == "apple":
return ""
else:
fail("Unsupported header naming: {}".format(naming))
def _mk_hmap(ctx: "context", name: str.type, headers: {str.type: ("artifact", str.type)}) -> "artifact":
output = ctx.actions.declare_output(name + ".hmap")
cmd = cmd_args(get_cxx_toolchain_info(ctx).mk_hmap)
cmd.add(["--output", output.as_output()])
header_args = cmd_args()
for n, (path, fmt) in headers.items():
header_args.add(n)
header_args.add(cmd_args(path, format = fmt))
hmap_args_file = ctx.actions.write(output.basename + ".argsfile", cmd_args(header_args, quote = "shell"))
cmd.add(["--mappings-file", hmap_args_file]).hidden(header_args)
ctx.actions.run(cmd, category = "generate_hmap", identifier = name)
return output

View file

@ -0,0 +1,299 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//cxx:cxx_bolt.bzl",
"bolt",
"cxx_use_bolt",
)
load("@prelude//cxx:debug.bzl", "SplitDebugMode")
load(
"@prelude//cxx/dist_lto:dist_lto.bzl",
"cxx_dist_link",
)
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
"LinkOrdering",
"LinkedObject",
"unpack_external_debug_info",
"unpack_link_args",
)
load("@prelude//linking:link_postprocessor.bzl", "postprocess")
load("@prelude//linking:strip.bzl", "strip_shared_library")
load("@prelude//utils:utils.bzl", "value_or")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
load(
":cxx_link_utility.bzl",
"cxx_link_cmd",
"linker_map_args",
"make_link_args",
)
load(":dwp.bzl", "dwp", "dwp_available")
load(
":linker.bzl",
"SharedLibraryFlagOverrides", # @unused Used as a type
"get_import_library",
"get_output_flags",
"get_shared_library_flags",
"get_shared_library_name_linker_flags",
)
# Actually perform a link into the supplied output.
def cxx_link(
ctx: "context",
links: [LinkArgs.type],
# The destination for the link output.
output: "artifact",
linker_map: ["artifact", None] = None,
prefer_local: bool.type = False,
local_only: bool.type = False,
link_weight: int.type = 1,
enable_distributed_thinlto: bool.type = False,
# A category suffix that will be added to the category of the link action that is generated.
category_suffix: [str.type, None] = None,
# An identifier that will uniquely name this link action in the context of a category. Useful for
# differentiating multiple link actions in the same rule.
identifier: [str.type, None] = None,
is_shared: bool.type = False,
strip: bool.type = False,
# A function/lambda which will generate the strip args using the ctx.
strip_args_factory = None,
generate_dwp: bool.type = True,
executable_link = False,
link_postprocessor: ["cmd_args", None] = None,
force_full_hybrid_if_capable: bool.type = False,
import_library: ["artifact", None] = None) -> LinkedObject.type:
cxx_toolchain_info = get_cxx_toolchain_info(ctx)
linker_info = cxx_toolchain_info.linker_info
should_generate_dwp = generate_dwp and dwp_available(ctx) and cxx_toolchain_info.split_debug_mode != SplitDebugMode("none")
if linker_info.supports_distributed_thinlto and enable_distributed_thinlto:
if not linker_info.requires_objects:
fail("Cannot use distributed thinlto if the cxx toolchain doesn't require_objects")
return cxx_dist_link(
ctx,
links,
output,
linker_map,
category_suffix,
identifier,
should_generate_dwp,
executable_link,
)
if linker_map != None:
links += [linker_map_args(ctx, linker_map.as_output())]
(link_args, hidden, dwo_dir) = make_link_args(
ctx,
links,
suffix = identifier,
dwo_dir_name = output.short_path + ".dwo.d",
is_shared = is_shared,
link_ordering = LinkOrdering(linker_info.link_ordering) if linker_info.link_ordering else None,
)
external_debug_info = []
# If we're not stripping the output linked object, than add-in an externally
# referenced debug info that the linked object may reference (and which may
# need to be available for debugging).
if not (strip or getattr(ctx.attrs, "prefer_stripped_objects", False)):
for link in links:
external_debug_info.extend(unpack_external_debug_info(link))
# When using LTO+split-dwarf, the link step will generate externally
# referenced debug info.
if dwo_dir != None:
external_debug_info.append(dwo_dir)
if linker_info.type == "windows":
shell_quoted_args = cmd_args(link_args)
else:
shell_quoted_args = cmd_args(link_args, quote = "shell")
argfile, _ = ctx.actions.write(
output.short_path + ".linker.argsfile",
shell_quoted_args,
allow_args = True,
)
command = cxx_link_cmd(ctx)
command.add(get_output_flags(linker_info.type, output))
command.add(cmd_args(argfile, format = "@{}"))
command.hidden([hidden])
category = "cxx_link"
if category_suffix != None:
category += "_" + category_suffix
# If the linked object files don't contain debug info, clang may not
# generate a DWO directory, so make sure we at least `mkdir` and empty
# one to make v2/RE happy.
if dwo_dir != None:
cmd = cmd_args(["/bin/sh", "-c"])
cmd.add(cmd_args(dwo_dir.as_output(), format = 'mkdir -p {}; "$@"'))
cmd.add('""').add(command)
cmd.hidden(command)
command = cmd
# Enable hybrid execution only when prefer local is set to preserve isolation
if prefer_local and force_full_hybrid_if_capable:
fail("cannot use `force_full_hybrid_if_capable` when `prefer_local` is enabled")
if local_only and force_full_hybrid_if_capable:
fail("cannot use `force_full_hybrid_if_capable` when `local_only` is enabled")
ctx.actions.run(
command,
prefer_local = prefer_local,
local_only = local_only,
weight = link_weight,
category = category,
identifier = identifier,
force_full_hybrid_if_capable = force_full_hybrid_if_capable,
)
if strip:
strip_args = strip_args_factory(ctx) if strip_args_factory else cmd_args()
output = strip_shared_library(ctx, cxx_toolchain_info, output, strip_args)
if link_postprocessor:
output = postprocess(ctx, output, link_postprocessor)
final_output = output if not (executable_link and cxx_use_bolt(ctx)) else bolt(ctx, output, identifier)
dwp_artifact = None
if should_generate_dwp:
# TODO(T110378144): Once we track split dwarf from compiles, we should
# just pass in `binary.external_debug_info` here instead of all link
# args.
dwp_inputs = cmd_args()
for link in links:
dwp_inputs.add(unpack_link_args(link))
dwp_inputs.add(external_debug_info)
dwp_artifact = dwp(
ctx,
final_output,
identifier = identifier,
category_suffix = category_suffix,
# TODO(T110378142): Ideally, referenced objects are a list of
# artifacts, but currently we don't track them properly. So, we
# just pass in the full link line and extract all inputs from that,
# which is a bit of an overspecification.
referenced_objects = [dwp_inputs],
allow_huge_dwp = ctx.attrs.allow_huge_dwp if hasattr(ctx.attrs, "allow_huge_dwp") else False,
)
return LinkedObject(
output = final_output,
prebolt_output = output,
dwp = dwp_artifact,
external_debug_info = external_debug_info,
linker_argsfile = argfile,
import_library = import_library,
)
def _link_libraries_locally(ctx: "context", prefer_local: bool.type) -> bool.type:
if hasattr(ctx.attrs, "_link_libraries_locally_override"):
return value_or(ctx.attrs._link_libraries_locally_override, prefer_local)
return prefer_local
def cxx_link_shared_library(
ctx: "context",
# The destination for the link output.
output: "artifact",
# Optional soname to link into shared library.
name: [str.type, None] = None,
links: [LinkArgs.type] = [],
prefer_local: [bool.type, None] = None,
local_only: [bool.type, None] = None,
link_weight: int.type = 1,
enable_distributed_thinlto: bool.type = False,
# A category suffix that will be added to the category of the link action that is generated.
category_suffix: [str.type, None] = None,
# An identifier that will uniquely name this link action in the context of a category. Useful for
# differentiating multiple link actions in the same rule.
identifier: [str.type, None] = None,
# Overrides the default flags used to specify building shared libraries
shared_library_flags: [SharedLibraryFlagOverrides.type, None] = None,
strip: bool.type = False,
strip_args_factory = None,
link_postprocessor: ["cmd_args", None] = None,
force_full_hybrid_if_capable: [bool.type, None] = None) -> LinkedObject.type:
"""
Link a shared library into the supplied output.
"""
linker_info = get_cxx_toolchain_info(ctx).linker_info
linker_type = linker_info.type
extra_args = []
extra_args.extend(get_shared_library_flags(linker_type, shared_library_flags)) # e.g. "-shared"
if name != None:
extra_args.extend(get_shared_library_name_linker_flags(linker_type, name, shared_library_flags))
(import_library, import_library_args) = get_import_library(
ctx,
linker_type,
output.short_path,
)
extra_args.extend(import_library_args)
prefer_local_value = value_or(prefer_local, value_or(linker_info.link_libraries_locally, False))
return cxx_link(
ctx,
[LinkArgs(flags = extra_args)] + links,
output,
prefer_local = _link_libraries_locally(ctx, prefer_local_value),
local_only = value_or(local_only, False),
link_weight = link_weight,
enable_distributed_thinlto = enable_distributed_thinlto,
category_suffix = category_suffix,
identifier = identifier,
is_shared = True,
strip = strip,
strip_args_factory = strip_args_factory,
link_postprocessor = link_postprocessor,
force_full_hybrid_if_capable = value_or(force_full_hybrid_if_capable, False),
import_library = import_library,
)
def cxx_link_into_shared_library(
ctx: "context",
name: str.type,
links: [LinkArgs.type] = [],
# Wether to embed the library name as the SONAME.
soname: bool.type = True,
prefer_local: [bool.type, None] = None,
local_only: [bool.type, None] = None,
link_weight: int.type = 1,
enable_distributed_thinlto: bool.type = False,
# A category suffix that will be added to the category of the link action that is generated.
category_suffix: [str.type, None] = None,
# An identifier that will uniquely name this link action in the context of a category. Useful for
# differentiating multiple link actions in the same rule.
identifier: [str.type, None] = None,
# Overrides the default flags used to specify building shared libraries
shared_library_flags: [SharedLibraryFlagOverrides.type, None] = None,
strip: bool.type = False,
strip_args_factory = None,
link_postprocessor: ["cmd_args", None] = None,
force_full_hybrid_if_capable: [bool.type, None] = None) -> LinkedObject.type:
output = ctx.actions.declare_output(name)
return cxx_link_shared_library(
ctx,
output,
name = name if soname else None,
links = links,
prefer_local = prefer_local,
local_only = local_only,
link_weight = link_weight,
enable_distributed_thinlto = enable_distributed_thinlto,
category_suffix = category_suffix,
identifier = identifier,
shared_library_flags = shared_library_flags,
strip = strip,
strip_args_factory = strip_args_factory,
link_postprocessor = link_postprocessor,
force_full_hybrid_if_capable = force_full_hybrid_if_capable,
)

View file

@ -0,0 +1,433 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//linking:link_groups.bzl",
"LinkGroupLib", # @unused Used as a type
"LinkGroupLibInfo",
)
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
"LinkInfo", # @unused Used as a type
"LinkStyle",
"Linkage",
"LinkedObject", # @unused Used as a type
"get_actual_link_style",
"set_linkable_link_whole",
get_link_info_from_link_infos = "get_link_info",
)
load(
"@prelude//linking:linkable_graph.bzl",
"LinkableGraph", # @unused Used as a type
"LinkableNode", # @unused Used as a type
"create_linkable_graph",
"get_link_info",
"get_linkable_graph_node_map_func",
)
load(
"@prelude//linking:linkables.bzl",
"linkable",
)
load(
"@prelude//utils:graph_utils.bzl",
"breadth_first_traversal_by",
)
load(
"@prelude//utils:set.bzl",
"set",
"set_record",
)
load(
"@prelude//utils:utils.bzl",
"expect",
)
load(
":groups.bzl",
"Group", # @unused Used as a type
"MATCH_ALL_LABEL",
"NO_MATCH_LABEL",
"compute_mappings",
"parse_groups_definitions",
)
load(
":link.bzl",
"cxx_link_shared_library",
)
LINK_GROUP_MAP_DATABASE_SUB_TARGET = "link-group-map-database"
LINK_GROUP_MAP_FILE_NAME = "link_group_map.json"
LinkGroupInfo = provider(fields = [
"groups", # [Group.type]
"groups_hash", # str.type
"mappings", # {"label": str.type}
])
LinkGroupLinkInfo = record(
link_info = field(LinkInfo.type),
link_style = field(LinkStyle.type),
)
LinkGroupLibSpec = record(
# The output name given to the linked shared object.
name = field(str.type),
# Used to differentiate normal native shared libs from e.g. Python native
# extensions (which are techncially shared libs, but don't set a SONAME
# and aren't managed by `SharedLibraryInfo`s).
is_shared_lib = field(bool.type, True),
# The link group to link.
group = field(Group.type),
)
def parse_link_group_definitions(mappings: list.type) -> [Group.type]:
return parse_groups_definitions(mappings, linkable)
def get_link_group(ctx: "context") -> [str.type, None]:
return ctx.attrs.link_group
def get_link_group_info(
ctx: "context",
executable_deps: [[LinkableGraph.type], None] = None) -> [LinkGroupInfo.type, None]:
"""
Parses the currently analyzed context for any link group definitions
and returns a list of all link groups with their mappings.
"""
link_group_map = ctx.attrs.link_group_map
if not link_group_map:
return None
# If specified as a dep that provides the `LinkGroupInfo`, use that.
if type(link_group_map) == "dependency":
return link_group_map[LinkGroupInfo]
# Otherwise build one from our graph.
expect(executable_deps != None)
link_groups = parse_link_group_definitions(link_group_map)
linkable_graph = create_linkable_graph(
ctx,
children = executable_deps,
)
linkable_graph_node_map = get_linkable_graph_node_map_func(linkable_graph)()
mappings = compute_mappings(groups = link_groups, graph_map = linkable_graph_node_map)
return LinkGroupInfo(
groups = link_groups,
groups_hash = hash(str(link_groups)),
mappings = mappings,
)
def get_auto_link_group_libs(ctx: "context") -> [{str.type: LinkGroupLib.type}, None]:
"""
Return link group libs created by the link group map rule.
"""
link_group_map = ctx.attrs.link_group_map
if not link_group_map:
return None
if type(link_group_map) == "dependency":
info = link_group_map.get(LinkGroupLibInfo)
if info == None:
return None
return info.libs
fail("Link group maps must be provided as a link_group_map rule dependency.")
def get_link_group_preferred_linkage(link_groups: [Group.type]) -> {"label": Linkage.type}:
return {
mapping.root.label: mapping.preferred_linkage
for group in link_groups
for mapping in group.mappings
if mapping.root != None and mapping.preferred_linkage != None
}
def get_filtered_labels_to_links_map(
linkable_graph_node_map: {"label": LinkableNode.type},
link_group: [str.type, None],
link_group_mappings: [{"label": str.type}, None],
link_group_preferred_linkage: {"label": Linkage.type},
link_style: LinkStyle.type,
deps: ["label"],
link_group_libs: {str.type: LinkGroupLib.type} = {},
prefer_stripped: bool.type = False,
is_executable_link: bool.type = False) -> {"label": LinkGroupLinkInfo.type}:
"""
Given a linkable graph, link style and link group mappings, finds all links
to consider for linking traversing the graph as necessary and then
identifies which link infos and targets belong the in the provided link group.
If no link group is provided, all unmatched link infos are returned.
"""
def get_traversed_deps(node: "label") -> ["label"]:
linkable_node = linkable_graph_node_map[node] # buildifier: disable=uninitialized
# Always link against exported deps
node_linkables = list(linkable_node.exported_deps)
# If the preferred linkage is `static` or `any` with a link style that is
# not shared, we need to link against the deps too.
should_traverse = False
if linkable_node.preferred_linkage == Linkage("static"):
should_traverse = True
elif linkable_node.preferred_linkage == Linkage("any"):
should_traverse = link_style != Linkage("shared")
if should_traverse:
node_linkables += linkable_node.deps
return node_linkables
# Get all potential linkable targets
linkables = breadth_first_traversal_by(
linkable_graph_node_map,
deps,
get_traversed_deps,
)
# An index of target to link group names, for all link group library nodes.
# Provides fast lookup of a link group root lib via it's label.
link_group_roots = {
lib.label: name
for name, lib in link_group_libs.items()
if lib.label != None
}
linkable_map = {}
# Keep track of whether we've already added a link group to the link line
# already. This avoids use adding the same link group lib multiple times,
# for each of the possible multiple nodes that maps to it.
link_group_added = {}
def add_link(target: "label", link_style: LinkStyle.type):
linkable_map[target] = LinkGroupLinkInfo(
link_info = get_link_info(linkable_graph_node_map[target], link_style, prefer_stripped),
link_style = link_style,
) # buildifier: disable=uninitialized
def add_link_group(target: "label", target_group: str.type):
# If we've already added this link group to the link line, we're done.
if target_group in link_group_added:
return
# In some flows, we may not have access to the actual link group lib
# in our dep tree (e.g. https://fburl.com/code/pddmkptb), so just bail
# in this case.
# NOTE(agallagher): This case seems broken, as we're not going to set
# DT_NEEDED tag correctly, or detect missing syms at link time.
link_group_lib = link_group_libs.get(target_group)
if link_group_lib == None:
return
expect(target_group != link_group)
link_group_added[target_group] = None
linkable_map[target] = LinkGroupLinkInfo(
link_info = get_link_info_from_link_infos(link_group_lib.shared_link_infos),
link_style = LinkStyle("shared"),
) # buildifier: disable=uninitialized
for target in linkables:
node = linkable_graph_node_map[target]
actual_link_style = get_actual_link_style(link_style, link_group_preferred_linkage.get(target, node.preferred_linkage))
# Always link any shared dependencies
if actual_link_style == LinkStyle("shared"):
# If this target is a link group root library, we
# 1) don't propagate shared linkage down the tree, and
# 2) use the provided link info in lieu of what's in the grph.
target_link_group = link_group_roots.get(target)
if target_link_group != None and target_link_group != link_group:
add_link_group(target, target_link_group)
else:
add_link(target, LinkStyle("shared"))
# Mark transitive deps as shared.
for exported_dep in node.exported_deps:
exported_node = linkable_graph_node_map[exported_dep]
if exported_node.preferred_linkage == Linkage("any"):
link_group_preferred_linkage[exported_dep] = Linkage("shared")
else: # static or static_pic
target_link_group = link_group_mappings.get(target)
if not target_link_group and not link_group:
# Ungrouped linkable targets belong to the unlabeled executable
add_link(target, actual_link_style)
elif is_executable_link and target_link_group == NO_MATCH_LABEL:
# Targets labeled NO_MATCH belong to the unlabeled executable
add_link(target, actual_link_style)
elif target_link_group == MATCH_ALL_LABEL or target_link_group == link_group:
# If this belongs to the match all link group or the group currently being evaluated
add_link(target, actual_link_style)
elif target_link_group not in (None, NO_MATCH_LABEL, MATCH_ALL_LABEL):
add_link_group(target, target_link_group)
return linkable_map
# Find all link group libraries that are first order deps or exported deps of
# the exectuble or another link group's libs
def get_public_link_group_nodes(
linkable_graph_node_map: {"label": LinkableNode.type},
link_group_mappings: [{"label": str.type}, None],
executable_deps: ["label"],
root_link_group: [str.type, None]) -> set_record.type:
external_link_group_nodes = set()
# TODO(@christylee): do we need to traverse root link group and NO_MATCH_LABEL exported deps?
# buildifier: disable=uninitialized
def crosses_link_group_boundary(current_group: [str.type, None], new_group: [str.type, None]):
# belongs to root binary
if new_group == root_link_group:
return False
if new_group == NO_MATCH_LABEL:
# Using NO_MATCH with an explicitly defined root_link_group is undefined behavior
expect(root_link_group == None or root_link_group == NO_MATCH_LABEL)
return False
# private node in link group
if new_group == current_group:
return False
return True
# Check the direct deps of the executable since the executable is not in linkable_graph_node_map
for label in executable_deps:
group = link_group_mappings.get(label)
if crosses_link_group_boundary(root_link_group, group):
external_link_group_nodes.add(label)
# get all nodes that cross function boundaries
# TODO(@christylee): dlopen-able libs that depend on the main executable does not have a
# linkable internal edge to the main executable. Symbols that are not referenced during the
# executable link might be dropped unless the dlopen-able libs are linked against the main
# executable. We need to force export those symbols to avoid undefined symbls.
for label, node in linkable_graph_node_map.items():
current_group = link_group_mappings.get(label)
for dep in node.deps + node.exported_deps:
new_group = link_group_mappings.get(dep)
if crosses_link_group_boundary(current_group, new_group):
external_link_group_nodes.add(dep)
SPECIAL_LINK_GROUPS = [MATCH_ALL_LABEL, NO_MATCH_LABEL]
# buildifier: disable=uninitialized
def get_traversed_deps(node: "label") -> ["label"]:
exported_deps = []
for exported_dep in linkable_graph_node_map[node].exported_deps:
group = link_group_mappings.get(exported_dep)
if group != root_link_group and group not in SPECIAL_LINK_GROUPS:
exported_deps.append(exported_dep)
return exported_deps
external_link_group_nodes.update(
# get transitive exported deps
breadth_first_traversal_by(
linkable_graph_node_map,
external_link_group_nodes.list(),
get_traversed_deps,
),
)
return external_link_group_nodes
def get_filtered_links(
labels_to_links_map: {"label": LinkGroupLinkInfo.type},
public_link_group_nodes: [set_record.type, None] = None):
if public_link_group_nodes == None:
return [link_group_info.link_info for link_group_info in labels_to_links_map.values()]
infos = []
for label, link_group_info in labels_to_links_map.items():
info = link_group_info.link_info
if public_link_group_nodes.contains(label):
linkables = [set_linkable_link_whole(linkable) for linkable in info.linkables]
infos.append(
LinkInfo(
name = info.name,
pre_flags = info.pre_flags,
post_flags = info.post_flags,
linkables = linkables,
external_debug_info = info.external_debug_info,
),
)
else:
infos.append(info)
return infos
def get_filtered_targets(labels_to_links_map: {"label": LinkGroupLinkInfo.type}):
return [label.raw_target() for label in labels_to_links_map.keys()]
def get_link_group_map_json(ctx: "context", targets: ["target_label"]) -> DefaultInfo.type:
json_map = ctx.actions.write_json(LINK_GROUP_MAP_FILE_NAME, sorted(targets))
return DefaultInfo(default_outputs = [json_map])
def create_link_group(
ctx: "context",
spec: LinkGroupLibSpec.type,
# The deps of the top-level executable.
executable_deps: ["label"] = [],
root_link_group = [str.type, None],
linkable_graph_node_map: {"label": LinkableNode.type} = {},
linker_flags: [""] = [],
link_group_mappings: {"label": str.type} = {},
link_group_preferred_linkage: {"label": Linkage.type} = {},
link_style: LinkStyle.type = LinkStyle("static_pic"),
link_group_libs: {str.type: LinkGroupLib.type} = {},
prefer_stripped_objects: bool.type = False,
prefer_local: bool.type = False,
category_suffix: [str.type, None] = None) -> LinkedObject.type:
"""
Link a link group library, described by a `LinkGroupLibSpec`. This is
intended to handle regular shared libs and e.g. Python extensions.
"""
inputs = []
# Add extra linker flags.
if linker_flags:
inputs.append(LinkInfo(pre_flags = linker_flags))
# Get roots to begin the linkable search.
# TODO(agallagher): We should use the groups "public" nodes as the roots.
roots = []
for mapping in spec.group.mappings:
# If there's no explicit root, this means use the executable deps.
if mapping.root == None:
roots.extend(executable_deps)
else:
roots.append(mapping.root.label)
# Add roots...
filtered_labels_to_links_map = get_filtered_labels_to_links_map(
linkable_graph_node_map,
spec.group.name,
link_group_mappings,
link_group_preferred_linkage,
link_group_libs = link_group_libs,
link_style = link_style,
deps = roots,
is_executable_link = False,
prefer_stripped = prefer_stripped_objects,
)
public_nodes = get_public_link_group_nodes(
linkable_graph_node_map,
link_group_mappings,
executable_deps,
root_link_group,
)
inputs.extend(get_filtered_links(filtered_labels_to_links_map, public_nodes))
# link the rule
return cxx_link_shared_library(
ctx,
ctx.actions.declare_output(spec.name),
name = spec.name if spec.is_shared_lib else None,
links = [LinkArgs(infos = inputs)],
category_suffix = category_suffix,
identifier = spec.name,
prefer_local = prefer_local,
)

View file

@ -0,0 +1,238 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//cxx:cxx_toolchain_types.bzl", "LinkerInfo")
load("@prelude//utils:utils.bzl", "expect")
# Platform-specific linker flags handling. Modeled after the `Linker` abstraction
# in v1 (https://fburl.com/diffusion/kqd2ylcy).
# TODO(T110378136): It might make more sense to pass these in via the toolchain.
Linker = record(
# The extension to use for the shared library if not set in the toolchain.
default_shared_library_extension = str.type,
# The format to use for the versioned shared library extension if not set in the toolchain.
default_shared_library_versioned_extension_format = str.type,
# How to format arguments to the linker to set a shared lib name.
shared_library_name_linker_flags_format = [str.type],
# Flags to pass to the linker to make it generate a shared library.
shared_library_flags = [str.type],
)
# Allows overriding the default shared library flags.
# e.g. when building Apple tests, we want to link with `-bundle` instead of `-shared` to allow
# linking against the bundle loader.
SharedLibraryFlagOverrides = record(
# How to format arguments to the linker to set a shared lib name.
shared_library_name_linker_flags_format = [str.type],
# Flags to pass to the linker to make it generate a shared library.
shared_library_flags = [str.type],
)
LINKERS = {
"darwin": Linker(
default_shared_library_extension = "dylib",
default_shared_library_versioned_extension_format = "{}.dylib",
shared_library_name_linker_flags_format = ["-install_name", "@rpath/{}"],
shared_library_flags = ["-shared"],
),
"gnu": Linker(
default_shared_library_extension = "so",
default_shared_library_versioned_extension_format = "so.{}",
shared_library_name_linker_flags_format = ["-Wl,-soname,{}"],
shared_library_flags = ["-shared"],
),
"windows": Linker(
default_shared_library_extension = "dll",
default_shared_library_versioned_extension_format = "dll",
# NOTE(agallagher): I *think* windows doesn't support a flag to set the
# library name, and relies on the basename.
shared_library_name_linker_flags_format = [],
shared_library_flags = ["/DLL"],
),
}
def _sanitize(s: str.type) -> str.type:
return s.replace("/", "_")
# NOTE(agallagher): Does this belong in the native/shared_libraries.bzl?
def get_shared_library_name(
linker_info: LinkerInfo.type,
short_name: str.type,
version: [str.type, None] = None):
"""
Generate a platform-specific shared library name based for the given rule.
"""
if version == None:
return linker_info.shared_library_name_format.format(short_name)
else:
return linker_info.shared_library_versioned_name_format.format(short_name, version)
def _parse_ext_macro(name: str.type) -> [(str.type, [str.type, None]), None]:
"""
Parse the `$(ext[ <version>])` macro from a user-specific library name,
which expands to a platform-specific suffix (e.g. `.so`, `.dylib`). If an
optional version argument is given (e.g. `$(ext 3.4)`) it expands to a
platform-specific versioned suffix (e.g. `.so.3.4`, `.3.4.dylib`).
"""
# If there's no macro, then there's nothing to do.
if ".$(ext" not in name:
return None
expect(name.endswith(")"))
# Otherwise, attempt to parse out the macro.
base, rest = name.split(".$(ext")
# If the macro is arg-less, then return w/o a version.
if rest == ")":
return (base, None)
# Otherwise, extract the version from the arg.
expect(rest.startswith(" "))
return (base, rest[1:-1])
def get_shared_library_name_for_param(linker_info: LinkerInfo.type, name: str.type):
"""
Format a user-provided shared library name, supporting v1's `$(ext)` suffix.
"""
parsed = _parse_ext_macro(name)
if parsed != None:
base, version = parsed
name = get_shared_library_name(
linker_info,
base.removeprefix("lib"),
version = version,
)
return name
# NOTE(agallagher): Does this belong in the native/shared_libraries.bzl?
def get_default_shared_library_name(linker_info: LinkerInfo.type, label: "label"):
"""
Generate a platform-specific shared library name based for the given rule.
"""
# TODO(T110378119): v1 doesn't use the cell/repo name, so we don't here for
# initial compatiblity, but maybe we should?
short_name = "{}_{}".format(_sanitize(label.package), _sanitize(label.name))
return get_shared_library_name(linker_info, short_name)
def get_shared_library_name_linker_flags(linker_type: str.type, soname: str.type, flag_overrides: [SharedLibraryFlagOverrides.type, None] = None) -> [str.type]:
"""
Arguments to pass to the linker to set the given soname.
"""
if flag_overrides:
shared_library_name_linker_flags_format = flag_overrides.shared_library_name_linker_flags_format
else:
shared_library_name_linker_flags_format = LINKERS[linker_type].shared_library_name_linker_flags_format
return [
f.format(soname)
for f in shared_library_name_linker_flags_format
]
def get_shared_library_flags(linker_type: str.type, flag_overrides: [SharedLibraryFlagOverrides.type, None] = None) -> [str.type]:
"""
Arguments to pass to the linker to link a shared library.
"""
if flag_overrides:
return flag_overrides.shared_library_flags
return LINKERS[linker_type].shared_library_flags
def get_link_whole_args(linker_type: str.type, inputs: ["artifact"]) -> [""]:
"""
Return linker args used to always link all the given inputs.
"""
args = []
if linker_type == "gnu":
args.append("-Wl,--whole-archive")
args.extend(inputs)
args.append("-Wl,--no-whole-archive")
elif linker_type == "darwin":
for inp in inputs:
args.append("-Xlinker")
args.append("-force_load")
args.append("-Xlinker")
args.append(inp)
elif linker_type == "windows":
for inp in inputs:
args.append(inp)
args.append("/WHOLEARCHIVE:" + inp.short_path)
else:
fail("Linker type {} not supported".format(linker_type))
return args
def get_objects_as_library_args(linker_type: str.type, objects: ["artifact"]) -> [""]:
"""
Return linker args used to link the given objects as a library.
"""
args = []
if linker_type == "gnu":
args.append("-Wl,--start-lib")
args.extend(objects)
args.append("-Wl,--end-lib")
elif linker_type == "windows":
args.extend(objects)
else:
fail("Linker type {} not supported".format(linker_type))
return args
def get_ignore_undefined_symbols_flags(linker_type: str.type) -> [str.type]:
"""
Return linker args used to suppress undefined symbol errors.
"""
args = []
if linker_type == "gnu":
args.append("-Wl,--allow-shlib-undefined")
args.append("-Wl,--unresolved-symbols=ignore-all")
elif linker_type == "darwin":
args.append("-Wl,-flat_namespace,-undefined,suppress")
else:
fail("Linker type {} not supported".format(linker_type))
return args
def get_no_as_needed_shared_libs_flags(linker_type: str.type) -> [str.type]:
"""
Return linker args used to prevent linkers from dropping unused shared
library dependencies from the e.g. DT_NEEDED tags of the link.
"""
args = []
if linker_type == "gnu":
args.append("-Wl,--no-as-needed")
elif linker_type == "darwin":
pass
else:
fail("Linker type {} not supported".format(linker_type))
return args
def get_output_flags(linker_type: str.type, output: "artifact") -> ["_argslike"]:
if linker_type == "windows":
return ["/Brepro", cmd_args(output.as_output(), format = "/OUT:{}")]
else:
return ["-o", output.as_output()]
def get_import_library(
ctx: "context",
linker_type: str.type,
output_short_path: str.type) -> (["artifact", None], ["_argslike"]):
if linker_type == "windows":
import_library = ctx.actions.declare_output(output_short_path + ".imp.lib")
return import_library, [cmd_args(import_library.as_output(), format = "/IMPLIB:{}")]
else:
return None, []

View file

@ -0,0 +1,949 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:local_only.bzl", "link_cxx_binary_locally")
load(
"@prelude//cxx:link.bzl",
"cxx_link_into_shared_library",
"cxx_link_shared_library",
)
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
"LinkInfo",
"LinkInfos",
"LinkStyle",
"Linkage",
"LinkedObject",
"SharedLibLinkable",
"get_actual_link_style",
"link_info_to_args",
get_link_info_from_link_infos = "get_link_info",
)
load(
"@prelude//linking:linkable_graph.bzl",
"AnnotatedLinkableRoot",
"LinkableGraph", # @unused Used as a type
"LinkableNode",
"LinkableRootAnnotation",
"LinkableRootInfo",
"get_deps_for_link",
"get_link_info",
"linkable_deps",
"linkable_graph",
)
load(
"@prelude//utils:graph_utils.bzl",
"breadth_first_traversal_by",
"topo_sort",
)
load("@prelude//utils:utils.bzl", "expect", "flatten", "value_or")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
load(
":linker.bzl",
"get_default_shared_library_name",
"get_ignore_undefined_symbols_flags",
"get_no_as_needed_shared_libs_flags",
"get_shared_library_name",
)
load(
":symbols.bzl",
"create_global_symbols_version_script",
"create_undefined_symbols_argsfile",
"extract_global_syms",
"extract_symbol_names",
"extract_undefined_syms",
)
OmnibusEnvironment = provider(fields = [
"dummy_omnibus",
"exclusions",
"roots",
"enable_explicit_roots",
"prefer_stripped_objects",
"shared_root_ld_flags",
"force_hybrid_links",
])
Disposition = enum("root", "excluded", "body")
OmnibusGraph = record(
nodes = field({"label": LinkableNode.type}),
# All potential root notes for an omnibus link (e.g. C++ libraries,
# C++ Python extensions).
roots = field({"label": AnnotatedLinkableRoot.type}),
# All nodes that should be excluded from libomnibus.
excluded = field({"label": None}),
)
# Bookkeeping information used to setup omnibus link rules.
OmnibusSpec = record(
body = field({"label": None}, {}),
excluded = field({"label": None}, {}),
roots = field({"label": AnnotatedLinkableRoot.type}, {}),
exclusion_roots = field(["label"]),
# All link infos.
link_infos = field({"label": LinkableNode.type}, {}),
dispositions = field({"label": Disposition.type}),
)
OmnibusPrivateRootProductCause = record(
category = field(str.type),
# Mis-assigned label
label = field(["label", None], default = None),
# Its actual disposiiton
disposition = field([Disposition.type, None], default = None),
)
OmnibusRootProduct = record(
shared_library = field(LinkedObject.type),
undefined_syms = field("artifact"),
global_syms = field("artifact"),
# If set, this explains why we had to use a private root for this product.
# If unset, this means the root was a shared root we reused.
private = field([OmnibusPrivateRootProductCause.type, None]),
)
AnnotatedOmnibusRootProduct = record(
product = field(OmnibusRootProduct.type),
annotation = field([LinkableRootAnnotation.type, None]),
)
SharedOmnibusRoot = record(
product = field(OmnibusRootProduct.type),
linker_type = field(str.type),
required_body = field(["label"]),
required_exclusions = field(["label"]),
prefer_stripped_objects = field(bool.type),
)
# The result of the omnibus link.
OmnibusSharedLibraries = record(
omnibus = field([LinkedObject.type, None], None),
libraries = field({str.type: LinkedObject.type}, {}),
roots = field({"label": AnnotatedOmnibusRootProduct.type}, {}),
exclusion_roots = field(["label"]),
excluded = field(["label"]),
dispositions = field({"label": Disposition.type}),
)
def get_omnibus_graph(graph: LinkableGraph.type, roots: {"label": AnnotatedLinkableRoot.type}, excluded: {"label": None}) -> OmnibusGraph.type:
graph_nodes = graph.nodes.traverse()
nodes = {}
for node in filter(None, graph_nodes):
if node.linkable:
nodes[node.label] = node.linkable
for root, annotated in node.roots.items():
# When building ou graph, we prefer un-annotated roots. Annotations
# tell us if a root was discovered implicitly, but if was
# discovered explicitly (in which case it has no annotation) then
# we would rather record that, since the annotation wasn't
# necessary.
if annotated.annotation:
roots.setdefault(root, annotated)
else:
roots[root] = annotated
excluded.update(node.excluded)
return OmnibusGraph(nodes = nodes, roots = roots, excluded = excluded)
def get_roots(label: "label", deps: ["dependency"]) -> {"label": AnnotatedLinkableRoot.type}:
roots = {}
for dep in deps:
if LinkableRootInfo in dep:
roots[dep.label] = AnnotatedLinkableRoot(
root = dep[LinkableRootInfo],
annotation = LinkableRootAnnotation(dependent = label),
)
return roots
def get_excluded(deps: ["dependency"] = []) -> {"label": None}:
excluded_nodes = {}
for dep in deps:
dep_info = linkable_graph(dep)
if dep_info != None:
excluded_nodes[dep_info.label] = None
return excluded_nodes
def create_linkable_root(
ctx: "context",
link_infos: LinkInfos.type,
name: [str.type, None],
deps: ["dependency"],
graph: LinkableGraph.type,
create_shared_root: bool.type) -> LinkableRootInfo.type:
# Only include dependencies that are linkable.
deps = linkable_deps(deps)
def create_shared_root_impl():
env = ctx.attrs._omnibus_environment
if not env:
return (None, OmnibusPrivateRootProductCause(category = "no_omnibus_environment"))
env = env[OmnibusEnvironment]
prefer_stripped_objects = env.prefer_stripped_objects
if not create_shared_root:
return (None, OmnibusPrivateRootProductCause(category = "no_shared_root"))
omnibus_graph = get_omnibus_graph(graph, {}, {})
inputs = []
linker_info = get_cxx_toolchain_info(ctx).linker_info
linker_type = linker_info.type
inputs.append(LinkInfo(
pre_flags =
get_no_as_needed_shared_libs_flags(linker_type) +
get_ignore_undefined_symbols_flags(linker_type),
))
inputs.append(get_link_info_from_link_infos(
link_infos,
prefer_stripped = prefer_stripped_objects,
))
inputs.append(LinkInfo(linkables = [SharedLibLinkable(lib = env.dummy_omnibus)]))
env_excluded = _exclusions_from_env(env, omnibus_graph)
required_body = []
required_exclusions = []
for dep in _link_deps(omnibus_graph.nodes, deps):
node = omnibus_graph.nodes[dep]
actual_link_style = get_actual_link_style(
LinkStyle("shared"),
node.preferred_linkage,
)
if actual_link_style != LinkStyle("shared"):
inputs.append(
get_link_info(
node,
actual_link_style,
prefer_stripped = prefer_stripped_objects,
),
)
continue
is_excluded = dep in env_excluded or dep in omnibus_graph.excluded
is_root = dep in omnibus_graph.roots
if is_excluded or (_is_shared_only(node) and not is_root):
inputs.append(get_link_info(node, actual_link_style, prefer_stripped = prefer_stripped_objects))
required_exclusions.append(dep)
continue
if is_root:
dep_root = omnibus_graph.roots[dep].root.shared_root
if dep_root == None:
# If we know our dep is a root, but our dep didn't know
# that and didn't produce a shared root, then there is no
# point in producing anything a reusable root here since it
# wo'nt actually *be* reusable due to the root mismatch.
return (None, OmnibusPrivateRootProductCause(category = "dep_no_shared_root", label = dep))
inputs.append(LinkInfo(pre_flags = [
cmd_args(dep_root.product.shared_library.output),
]))
continue
required_body.append(dep)
output = ctx.actions.declare_output(
"omnibus/" + value_or(name, get_default_shared_library_name(linker_info, ctx.label)),
)
shared_library = cxx_link_shared_library(
ctx,
output,
name = name,
links = [LinkArgs(flags = env.shared_root_ld_flags), LinkArgs(infos = inputs)],
category_suffix = "omnibus_root",
identifier = name or output.short_path,
)
return (
SharedOmnibusRoot(
product = OmnibusRootProduct(
shared_library = shared_library,
global_syms = extract_global_syms(ctx, shared_library.output, prefer_local = False),
undefined_syms = extract_undefined_syms(ctx, shared_library.output, prefer_local = False),
private = None,
),
required_body = required_body,
required_exclusions = required_exclusions,
prefer_stripped_objects = prefer_stripped_objects,
linker_type = linker_type,
),
None,
)
(shared_root, no_shared_root_reason) = create_shared_root_impl()
return LinkableRootInfo(
name = name,
link_infos = link_infos,
deps = deps,
shared_root = shared_root,
no_shared_root_reason = no_shared_root_reason,
)
def _exclusions_from_env(env: OmnibusEnvironment.type, graph: OmnibusGraph.type):
excluded = [
label
for label, info in graph.nodes.items()
if _is_excluded_by_environment(label, env) and not _is_static_only(info)
]
return {label: None for label in excluded}
def _is_excluded_by_environment(label: "label", env: OmnibusEnvironment.type) -> bool.type:
return label.raw_target() in env.exclusions
def _omnibus_soname(ctx):
linker_info = get_cxx_toolchain_info(ctx).linker_info
return get_shared_library_name(linker_info, "omnibus")
def create_dummy_omnibus(ctx: "context", extra_ldflags: [""] = []) -> "artifact":
linker_info = get_cxx_toolchain_info(ctx).linker_info
output = ctx.actions.declare_output(get_shared_library_name(linker_info, "omnibus-dummy"))
cxx_link_shared_library(
ctx,
output,
name = _omnibus_soname(ctx),
links = [LinkArgs(flags = extra_ldflags)],
category_suffix = "dummy_omnibus",
)
return output
def _link_deps(
link_infos: {"label": LinkableNode.type},
deps: ["label"]) -> ["label"]:
"""
Return transitive deps required to link dynamically against the given deps.
This will following through deps of statically linked inputs and exported
deps of everything else (see https://fburl.com/diffusion/rartsbkw from v1).
"""
def find_deps(node: "label"):
return get_deps_for_link(link_infos[node], LinkStyle("shared"))
return breadth_first_traversal_by(link_infos, deps, find_deps)
def all_deps(
link_infos: {"label": LinkableNode.type},
roots: ["label"]) -> ["label"]:
"""
Return all transitive deps from following the given nodes.
"""
def find_transitive_deps(node: "label"):
return link_infos[node].deps + link_infos[node].exported_deps
all_deps = breadth_first_traversal_by(link_infos, roots, find_transitive_deps)
return all_deps
def _create_root(
ctx: "context",
spec: OmnibusSpec.type,
annotated_root_products,
root: LinkableRootInfo.type,
label: "label",
link_deps: ["label"],
omnibus: "artifact",
extra_ldflags: [""] = [],
prefer_stripped_objects: bool.type = False) -> OmnibusRootProduct.type:
"""
Link a root omnibus node.
"""
linker_info = get_cxx_toolchain_info(ctx).linker_info
linker_type = linker_info.type
if spec.body:
if root.shared_root != None:
# NOTE: This ignores ldflags. We rely on env.shared_root_ld_flags instead.
private = _requires_private_root(
root.shared_root,
linker_type,
prefer_stripped_objects,
spec,
)
if private == None:
return root.shared_root.product
else:
private = root.no_shared_root_reason
else:
private = OmnibusPrivateRootProductCause(category = "no_body")
inputs = []
# Since we're linking against a dummy omnibus which has no symbols, we need
# to make sure the linker won't drop it from the link or complain about
# missing symbols.
inputs.append(LinkInfo(
pre_flags =
get_no_as_needed_shared_libs_flags(linker_type) +
get_ignore_undefined_symbols_flags(linker_type),
))
# add native target link input
inputs.append(
get_link_info_from_link_infos(
root.link_infos,
prefer_stripped = prefer_stripped_objects,
),
)
# Link to Omnibus
if spec.body:
inputs.append(LinkInfo(linkables = [SharedLibLinkable(lib = omnibus)]))
# Add deps of the root to the link line.
for dep in link_deps:
node = spec.link_infos[dep]
actual_link_style = get_actual_link_style(
LinkStyle("shared"),
node.preferred_linkage,
)
# If this dep needs to be linked statically, then link it directly.
if actual_link_style != LinkStyle("shared"):
inputs.append(get_link_info(
node,
actual_link_style,
prefer_stripped = prefer_stripped_objects,
))
continue
# If this is another root.
if dep in spec.roots:
other_root = annotated_root_products[dep]
# TODO(cjhopman): This should be passing structured linkables
inputs.append(LinkInfo(pre_flags = [cmd_args(other_root.product.shared_library.output)]))
continue
# If this node is in omnibus, just add that to the link line.
if dep in spec.body:
continue
# At this point, this should definitely be an excluded node.
expect(dep in spec.excluded, str(dep))
# We should have already handled statically linked nodes above.
expect(actual_link_style == LinkStyle("shared"))
inputs.append(get_link_info(node, actual_link_style))
output = ctx.actions.declare_output(value_or(root.name, get_default_shared_library_name(
linker_info,
label,
)))
# link the rule
shared_library = cxx_link_shared_library(
ctx,
output,
name = root.name,
links = [LinkArgs(flags = extra_ldflags), LinkArgs(infos = inputs)],
category_suffix = "omnibus_root",
identifier = root.name or output.short_path,
# We prefer local execution because there are lot of cxx_link_omnibus_root
# running simultaneously, so while their overall load is reasonable,
# their peak execution load is very high.
prefer_local = True,
)
return OmnibusRootProduct(
shared_library = shared_library,
global_syms = extract_global_syms(
ctx,
shared_library.output,
# Same as above.
prefer_local = True,
),
undefined_syms = extract_undefined_syms(
ctx,
shared_library.output,
# Same as above.
prefer_local = True,
),
private = private,
)
def _requires_private_root(
candidate: SharedOmnibusRoot.type,
linker_type: str.type,
prefer_stripped_objects: bool.type,
spec: OmnibusSpec.type) -> [OmnibusPrivateRootProductCause.type, None]:
if candidate.linker_type != linker_type:
return OmnibusPrivateRootProductCause(category = "linker_type")
if candidate.prefer_stripped_objects != prefer_stripped_objects:
return OmnibusPrivateRootProductCause(category = "prefer_stripped_objects")
for required_body in candidate.required_body:
if not (required_body in spec.body and required_body not in spec.roots):
return OmnibusPrivateRootProductCause(
category = "required_body",
label = required_body,
disposition = spec.dispositions[required_body],
)
for required_exclusion in candidate.required_exclusions:
if not required_exclusion in spec.excluded:
return OmnibusPrivateRootProductCause(
category = "required_exclusion",
label = required_exclusion,
disposition = spec.dispositions[required_exclusion],
)
return None
def _extract_global_symbols_from_link_args(
ctx: "context",
name: str.type,
link_args: [["artifact", "resolved_macro", "cmd_args", str.type]],
prefer_local: bool.type = False) -> "artifact":
"""
Extract global symbols explicitly set in the given linker args (e.g.
`-Wl,--export-dynamic-symbol=<sym>`).
"""
# TODO(T110378137): This is ported from D24065414, but it might make sense
# to explicitly tell Buck about the global symbols, rather than us trying to
# extract it from linker flags (which is brittle).
output = ctx.actions.declare_output(name)
# We intentionally drop the artifacts referenced in the args when generating
# the argsfile -- we just want to parse out symbol name flags and don't need
# to materialize artifacts to do this.
argsfile, _ = ctx.actions.write(name + ".args", link_args, allow_args = True)
# TODO(T110378133): Make this work with other platforms.
param = "--export-dynamic-symbol"
pattern = "\\(-Wl,\\)\\?{}[,=]\\([^,]*\\)".format(param)
# Used sed/grep to filter the symbol name from the relevant flags.
# TODO(T110378130): As is the case in v1, we don't properly extract flags
# from argsfiles embedded in existing args.
script = (
"set -euo pipefail; " +
'cat "$@" | (grep -- \'{0}\' || [[ $? == 1 ]]) | sed \'s|{0}|\\2|\' | LC_ALL=C sort -S 10% -u > {{}}'
.format(pattern)
)
ctx.actions.run(
[
"/bin/bash",
"-c",
cmd_args(output.as_output(), format = script),
"",
argsfile,
],
category = "omnibus_global_symbol_flags",
prefer_local = prefer_local,
)
return output
def _create_global_symbols_version_script(
ctx: "context",
roots: [AnnotatedOmnibusRootProduct.type],
excluded: ["artifact"],
link_args: [["artifact", "resolved_macro", "cmd_args", str.type]]) -> "artifact":
"""
Generate a version script exporting symbols from from the given objects and
link args.
"""
# Get global symbols from roots. We set a rule to do this per-rule, as
# using a single rule to process all roots adds overhead to the critical
# path of incremental flows (e.g. that only update a single root).
global_symbols_files = [
root.product.global_syms
for root in roots
]
# TODO(T110378126): Processing all excluded libs together may get expensive.
# We should probably split this up and operate on individual libs.
if excluded:
global_symbols_files.append(extract_symbol_names(
ctx,
"__excluded_libs__.global_syms.txt",
excluded,
dynamic = True,
global_only = True,
category = "omnibus_global_syms_excluded_libs",
))
# Extract explicitly globalized symbols from linker args.
global_symbols_files.append(_extract_global_symbols_from_link_args(
ctx,
"__global_symbols_from_args__.txt",
link_args,
))
all_global_symbols_files = ctx.actions.write("__global_symbols__.symbols", global_symbols_files)
all_global_symbols_files = cmd_args(all_global_symbols_files).hidden(global_symbols_files)
return create_global_symbols_version_script(
actions = ctx.actions,
name = "__global_symbols__.vers",
category = "omnibus_version_script",
symbol_files = global_symbols_files,
)
def _is_static_only(info: LinkableNode.type) -> bool.type:
"""
Return whether this can only be linked statically.
"""
return info.preferred_linkage == Linkage("static")
def _is_shared_only(info: LinkableNode.type) -> bool.type:
"""
Return whether this can only use shared linking
"""
return info.preferred_linkage == Linkage("shared")
def _create_omnibus(
ctx: "context",
spec: OmnibusSpec.type,
annotated_root_products,
extra_ldflags: [""] = [],
prefer_stripped_objects: bool.type = False) -> LinkedObject.type:
inputs = []
# Undefined symbols roots...
non_body_root_undefined_syms = [
root.product.undefined_syms
for label, root in annotated_root_products.items()
if label not in spec.body
]
if non_body_root_undefined_syms:
argsfile = create_undefined_symbols_argsfile(
actions = ctx.actions,
name = "__undefined_symbols__.argsfile",
symbol_files = non_body_root_undefined_syms,
category = "omnibus_undefined_symbols",
)
inputs.append(LinkInfo(pre_flags = [
cmd_args(argsfile, format = "@{}"),
]))
# Process all body nodes.
deps = {}
global_symbols_link_args = []
for label in spec.body:
# If this body node is a root, add the it's output to the link.
if label in spec.roots:
root = annotated_root_products[label].product
# TODO(cjhopman): This should be passing structured linkables
inputs.append(LinkInfo(pre_flags = [cmd_args(root.shared_library.output)]))
continue
node = spec.link_infos[label]
# Otherwise add in the static input for this node.
actual_link_style = get_actual_link_style(
LinkStyle("static_pic"),
node.preferred_linkage,
)
expect(actual_link_style == LinkStyle("static_pic"))
body_input = get_link_info(
node,
actual_link_style,
prefer_stripped = prefer_stripped_objects,
)
inputs.append(body_input)
global_symbols_link_args.append(link_info_to_args(body_input))
# Keep track of all first order deps of the omnibus monolith.
for dep in node.deps + node.exported_deps:
if dep not in spec.body:
expect(dep in spec.excluded)
deps[dep] = None
# Now add deps of omnibus to the link
for label in _link_deps(spec.link_infos, deps.keys()):
node = spec.link_infos[label]
actual_link_style = get_actual_link_style(
LinkStyle("shared"),
node.preferred_linkage,
)
inputs.append(get_link_info(
node,
actual_link_style,
prefer_stripped = prefer_stripped_objects,
))
toolchain_info = get_cxx_toolchain_info(ctx)
linker_info = toolchain_info.linker_info
# Add global symbols version script.
# FIXME(agallagher): Support global symbols for darwin.
if linker_info.type != "darwin":
global_sym_vers = _create_global_symbols_version_script(
ctx,
# Extract symols from roots...
annotated_root_products.values(),
# ... and the shared libs from excluded nodes.
[
shared_lib.output
for label in spec.excluded
for shared_lib in spec.link_infos[label].shared_libs.values()
],
# Extract explicit global symbol names from flags in all body link args.
global_symbols_link_args,
)
inputs.append(LinkInfo(pre_flags = [
"-Wl,--version-script",
global_sym_vers,
]))
soname = _omnibus_soname(ctx)
hybrid = use_hybrid_links_for_libomnibus(ctx)
return cxx_link_into_shared_library(
ctx,
soname,
links = [LinkArgs(flags = extra_ldflags), LinkArgs(infos = inputs)],
category_suffix = "omnibus",
# TODO(T110378138): As with static C++ links, omnibus links are
# currently too large for RE, so run them locally for now (e.g.
# https://fb.prod.workplace.com/groups/buck2dev/posts/2953023738319012/).
# NB: We explicitly pass a value here to override
# the linker_info.link_libraries_locally that's used by `cxx_link_into_shared_library`.
# That's because we do not want to apply the linking behavior universally,
# just use it for omnibus.
prefer_local = False if hybrid else link_cxx_binary_locally(ctx, toolchain_info),
link_weight = linker_info.link_weight,
enable_distributed_thinlto = ctx.attrs.enable_distributed_thinlto,
identifier = soname,
force_full_hybrid_if_capable = hybrid,
)
def _build_omnibus_spec(
ctx: "context",
graph: OmnibusGraph.type) -> OmnibusSpec.type:
"""
Divide transitive deps into excluded, root, and body nodes, which we'll
use to link the various parts of omnibus.
"""
exclusion_roots = graph.excluded.keys() + _implicit_exclusion_roots(ctx, graph)
# Build up the set of all nodes that we have to exclude from omnibus linking
# (any node that is excluded will exclude all it's transitive deps).
excluded = {
label: None
for label in all_deps(
graph.nodes,
exclusion_roots,
)
}
# Finalized root nodes, after removing any excluded roots.
roots = {
label: root
for label, root in graph.roots.items()
if label not in excluded
}
# Find the deps of the root nodes. These form the roots of the nodes
# included in the omnibus link.
first_order_root_deps = []
for label in _link_deps(graph.nodes, flatten([r.root.deps for r in roots.values()])):
# We only consider deps which aren't *only* statically linked.
if _is_static_only(graph.nodes[label]):
continue
# Don't include a root's dep onto another root.
if label in roots:
continue
first_order_root_deps.append(label)
# All body nodes. These included all non-excluded body nodes and any non-
# excluded roots which are reachable by these body nodes (since they will
# need to be put on the link line).
body = {
label: None
for label in all_deps(graph.nodes, first_order_root_deps)
if label not in excluded
}
dispositions = {}
for node, info in graph.nodes.items():
if _is_static_only(info):
continue
if node in roots:
dispositions[node] = Disposition("root")
continue
if node in excluded:
dispositions[node] = Disposition("excluded")
continue
if node in body:
dispositions[node] = Disposition("body")
continue
fail("Node was not assigned: {}".format(node))
return OmnibusSpec(
excluded = excluded,
roots = roots,
body = body,
link_infos = graph.nodes,
exclusion_roots = exclusion_roots,
dispositions = dispositions,
)
def _implicit_exclusion_roots(ctx: "context", graph: OmnibusGraph.type) -> ["label"]:
env = ctx.attrs._omnibus_environment
if not env:
return []
env = env[OmnibusEnvironment]
return [
label
for label, info in graph.nodes.items()
if _is_excluded_by_environment(label, env) or (_is_shared_only(info) and (label not in graph.roots))
]
def _ordered_roots(
spec: OmnibusSpec.type) -> [("label", AnnotatedLinkableRoot.type, ["label"])]:
"""
Return information needed to link the roots nodes in topo-sorted order.
"""
# Calculate all deps each root node needs to link against.
link_deps = {}
for label, root in spec.roots.items():
link_deps[label] = _link_deps(spec.link_infos, root.root.deps)
# Used the link deps to create the graph of root nodes.
root_graph = {
node: [dep for dep in deps if dep in spec.roots]
for node, deps in link_deps.items()
}
ordered_roots = []
# Emit the root link info as a topo-sorted list, so that we generate root link
# rules for dependencies before their dependents.
for label in topo_sort(root_graph):
root = spec.roots[label]
deps = link_deps[label]
ordered_roots.append((label, root, deps))
return ordered_roots
def create_omnibus_libraries(
ctx: "context",
graph: OmnibusGraph.type,
extra_ldflags: [""] = [],
prefer_stripped_objects: bool.type = False) -> OmnibusSharedLibraries.type:
spec = _build_omnibus_spec(ctx, graph)
# Create dummy omnibus
dummy_omnibus = create_dummy_omnibus(ctx, extra_ldflags)
libraries = {}
root_products = {}
# Link all root nodes against the dummy libomnibus lib.
for label, annotated_root, link_deps in _ordered_roots(spec):
product = _create_root(
ctx,
spec,
root_products,
annotated_root.root,
label,
link_deps,
dummy_omnibus,
extra_ldflags,
prefer_stripped_objects,
)
if annotated_root.root.name != None:
libraries[annotated_root.root.name] = product.shared_library
root_products[label] = AnnotatedOmnibusRootProduct(
product = product,
annotation = annotated_root.annotation,
)
# If we have body nodes, then link them into the monolithic libomnibus.so.
omnibus = None
if spec.body:
omnibus = _create_omnibus(
ctx,
spec,
root_products,
extra_ldflags,
prefer_stripped_objects,
)
libraries[_omnibus_soname(ctx)] = omnibus
# For all excluded nodes, just add their regular shared libs.
for label in spec.excluded:
for name, lib in spec.link_infos[label].shared_libs.items():
libraries[name] = lib
return OmnibusSharedLibraries(
omnibus = omnibus,
libraries = libraries,
roots = root_products,
exclusion_roots = spec.exclusion_roots,
excluded = spec.excluded.keys(),
dispositions = spec.dispositions,
)
def is_known_omnibus_root(ctx: "context") -> bool.type:
env = ctx.attrs._omnibus_environment
if not env:
return False
env = env[OmnibusEnvironment]
if not env.enable_explicit_roots:
return False
if ctx.attrs.supports_python_dlopen != None:
return ctx.attrs.supports_python_dlopen
if ctx.label.raw_target() in env.roots:
return True
return False
def explicit_roots_enabled(ctx: "context") -> bool.type:
env = ctx.attrs._omnibus_environment
if not env:
return False
return env[OmnibusEnvironment].enable_explicit_roots
def use_hybrid_links_for_libomnibus(ctx: "context") -> bool.type:
env = ctx.attrs._omnibus_environment
if not env:
return False
return env[OmnibusEnvironment].force_hybrid_links
def omnibus_environment_attr():
default = select({
"DEFAULT": "fbcode//buck2/platform/omnibus:omnibus_environment",
"fbcode//buck2/platform/omnibus:do_not_inject_omnibus_environment": None,
})
# In open source, we don't want to use omnibus
default = None # @oss-enable
return attrs.option(attrs.dep(), default = default)

View file

@ -0,0 +1,16 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//utils:platform_flavors_util.bzl", "by_platform")
load(":cxx_context.bzl", "get_cxx_platform_info")
def cxx_by_platform(ctx: "context", xs: [(str.type, "_a")]) -> ["_a"]:
cxx_platform_info = get_cxx_platform_info(ctx)
platform_flavors = [cxx_platform_info.name]
if cxx_platform_info.deps_aliases:
platform_flavors.extend(cxx_platform_info.deps_aliases)
return by_platform(platform_flavors, xs)

View file

@ -0,0 +1,276 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//cxx:preprocessor.bzl",
"CPreprocessor",
"cxx_inherited_preprocessor_infos",
"cxx_merge_cpreprocessors",
)
load(
"@prelude//linking:link_groups.bzl",
"merge_link_group_lib_info",
)
load(
"@prelude//linking:link_info.bzl",
"LinkInfo",
"LinkInfos",
"LinkStyle",
"Linkage",
"LinkedObject",
"create_merged_link_info",
"get_actual_link_style",
"get_link_styles_for_linkage",
)
load(
"@prelude//linking:linkable_graph.bzl",
"create_linkable_graph",
"create_linkable_graph_node",
"create_linkable_node",
)
load(
"@prelude//linking:shared_libraries.bzl",
"SharedLibraryInfo",
"create_shared_libraries",
"merge_shared_libraries",
)
load("@prelude//utils:utils.bzl", "expect", "flatten_dict")
load(
":cxx_library_utility.bzl",
"cxx_inherited_link_info",
)
def _linkage(ctx: "context") -> Linkage.type:
"""
Construct the preferred linkage to use for the given prebuilt library.
"""
# If we have both shared and static libs, we support any linkage.
if (ctx.attrs.shared_link and
(ctx.attrs.static_link or ctx.attrs.static_pic_link)):
return Linkage("any")
# Otherwise, if we have a shared library, we only support shared linkage.
if ctx.attrs.shared_link:
return Linkage("shared")
# Otherwise, if we have a static library, we only support static linkage.
if ctx.attrs.static_link or ctx.attrs.static_pic_link:
return Linkage("static")
# Otherwise, header only libs use any linkage.
return Linkage("any")
def _parse_macro(
arg: str.type) -> [(str.type, str.type, str.type), None]:
"""
Parse a lib reference macro (e.g. `$(lib 0)`, `$(rel-lib libfoo.so)`) into
the format string used to format the arg, the name of the macro parsed, and
the argument passed to the macro.
"""
# TODO(T110378124): This is obviously not ideal and longer-term we should
# probably come up with a better UI for this rule or properly support these
# macros.
# If there's not macro, then there's nothing to do.
if "$(" not in arg:
return None
# Extract the macro name and it's arg out of the string. Also, create a
# format string with the remaining parts which can be used to format to an
# actual arg. This is pretty ugly, but we don't have too complex a case to
# support (e.g. a single macro with a single arg).
start, rest = arg.split("$(")
pos = rest.find(" ")
macro = rest[:pos]
rest = rest[pos + 1:]
pos = rest.find(")")
param = rest[:pos]
end = rest[pos + 1:]
return start + "{}" + end, macro, param
def _get_static_link_args(
libs: ["artifact"],
args: [str.type]) -> [""]:
"""
Format a pair of static link string args and static libs into args to be
passed to the link, by resolving macro references to libraries.
"""
link = []
for arg in args:
res = _parse_macro(arg)
if res != None:
# Macros in the static link line are indexes to the list of static
# archives.
fmt, macro, param = res
expect(macro == "lib")
link.append(cmd_args(libs[int(param)], format = fmt))
else:
link.append(arg)
return link
def _get_shared_link_args(
shared_libs: {str.type: "artifact"},
args: [str.type]) -> [""]:
"""
Format a pair of shared link string args and shared libs into args to be
passed to the link, by resolving macro references to libraries.
"""
link = []
for arg in args:
res = _parse_macro(arg)
if res != None:
# Macros in the shared link line are named references to the map
# of all shared libs.
fmt, macro, lib_name = res
expect(macro in ("lib", "rel-lib"))
shared_lib = shared_libs[lib_name]
if macro == "lib":
link.append(cmd_args(shared_lib, format = fmt))
elif macro == "rel-lib":
# rel-lib means link-without-soname.
link.append(cmd_args(shared_lib, format = "-L{}").parent())
link.append("-l" + shared_lib.basename.removeprefix("lib").removesuffix(shared_lib.extension))
else:
link.append(arg)
return link
# The `prebuilt_cxx_library_group` rule is meant to provide fine user control for
# how a group libraries of libraries are added to the link line and was added for
# `fbcode//third-party-buck/platform009/build/IntelComposerXE:mkl_lp64_iomp`, which
# includes libraries with dep cycles, and so must be linked together with flags
# like `--start-group`/`--end-group`.
#
# The link arguments for the various link styles are specified by pair of string
# arguments with macros referenceing a collection of libraries:
#
# - For static link styles, the string link args (e.g. specific in `static_link`)
# contain macros of the form `$(lib <number>)`, where the number is an index
# into the corresponding list of static libraries artifacts (e.g. specified in
# `static_libs`). For example:
#
# static_link = ["-Wl,--start-group", "$(lib 0)", "$(lib 1)", "-Wl,--end-group"],
# static_libs = ["libfoo1.a", "libfoo2.a"],
#
# - For shared linking, the string link args contain macros of the form
# `$(lib <name>)` or `$(rel-lib <name>)`, where the name is key for shared
# libraries specified in `shared_libs` or `provided_shared_libs`. The
# `lib` macro examples to the full path of the shared library, whereas the
# `rel-lib` macro expands to `-L<dirname> -l<name>` of the library and is
# meant to be used in situations where shared library does not contain an
# embedded soname. For example:
#
# shared_link = ["$(lib libfoo1.so)", "$(rel-lib libfoo2.so)"],
# shared_libs = {
# "libfoo1.so": "lib/libfoo1.so",
# "libfoo2.so": "lib/libfoo2.so",
# },
#
def prebuilt_cxx_library_group_impl(ctx: "context") -> ["provider"]:
providers = []
deps = ctx.attrs.deps
exported_deps = ctx.attrs.exported_deps
# Figure out preprocessor stuff
args = []
args.extend(ctx.attrs.exported_preprocessor_flags)
for inc_dir in ctx.attrs.include_dirs:
args += ["-isystem", inc_dir]
preprocessor = CPreprocessor(args = args)
inherited_pp_info = cxx_inherited_preprocessor_infos(exported_deps)
providers.append(cxx_merge_cpreprocessors(ctx, [preprocessor], inherited_pp_info))
# Figure out all the link styles we'll be building archives/shlibs for.
preferred_linkage = _linkage(ctx)
inherited_non_exported_link = cxx_inherited_link_info(ctx, deps)
inherited_exported_link = cxx_inherited_link_info(ctx, exported_deps)
# Gather link infos, outputs, and shared libs for effective link style.
outputs = {}
libraries = {}
solibs = {}
for link_style in get_link_styles_for_linkage(preferred_linkage):
outs = []
if link_style == LinkStyle("static"):
outs.extend(ctx.attrs.static_libs)
args = _get_static_link_args(ctx.attrs.static_libs, ctx.attrs.static_link)
elif link_style == LinkStyle("static_pic"):
outs.extend(ctx.attrs.static_pic_libs)
args = _get_static_link_args(ctx.attrs.static_pic_libs, ctx.attrs.static_pic_link)
else: # shared
outs.extend(ctx.attrs.shared_libs.values())
args = _get_shared_link_args(
flatten_dict([ctx.attrs.shared_libs, ctx.attrs.provided_shared_libs]),
ctx.attrs.shared_link,
)
solibs.update({n: LinkedObject(output = lib) for n, lib in ctx.attrs.shared_libs.items()})
outputs[link_style] = outs
# TODO(cjhopman): This is hiding static and shared libs in opaque
# linker args, it should instead be constructing structured LinkInfo
# instances
libraries[link_style] = LinkInfos(default = LinkInfo(
name = repr(ctx.label),
pre_flags = args,
))
# Collect per-link-style default outputs.
default_outputs = {}
for link_style in LinkStyle:
actual_link_style = get_actual_link_style(link_style, preferred_linkage)
default_outputs[link_style] = outputs[actual_link_style]
providers.append(DefaultInfo(default_outputs = default_outputs[LinkStyle("static")]))
# Provider for native link.
providers.append(create_merged_link_info(
ctx,
libraries,
preferred_linkage = preferred_linkage,
# Export link info from our (non-exported) deps (e.g. when we're linking
# statically).
deps = [inherited_non_exported_link],
# Export link info from our (exported) deps.
exported_deps = [inherited_exported_link],
))
# Propagate shared libraries up the tree.
providers.append(merge_shared_libraries(
ctx.actions,
create_shared_libraries(ctx, solibs),
filter(None, [x.get(SharedLibraryInfo) for x in deps + exported_deps]),
))
# Create, augment and provide the linkable graph.
linkable_graph = create_linkable_graph(
ctx,
node = create_linkable_graph_node(
ctx,
linkable_node = create_linkable_node(
ctx = ctx,
deps = deps,
exported_deps = exported_deps,
preferred_linkage = preferred_linkage,
link_infos = libraries,
shared_libs = solibs,
),
),
deps = deps + exported_deps,
)
providers.append(linkable_graph)
providers.append(merge_link_group_lib_info(deps = deps + exported_deps))
return providers

View file

@ -0,0 +1,363 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:paths.bzl", "paths")
load(
"@prelude//utils:utils.bzl",
"flatten",
"value_or",
)
load(":attr_selection.bzl", "cxx_by_language_ext")
load(":cxx_context.bzl", "get_cxx_toolchain_info")
load(
":headers.bzl",
"CHeader", # @unused Used as a type
"CxxHeadersLayout", # @unused Used as a type
"CxxHeadersNaming",
"HeaderStyle",
"HeadersAsRawHeadersMode",
"as_raw_headers",
"cxx_attr_exported_header_style",
"cxx_attr_exported_headers",
"cxx_attr_headers",
"prepare_headers",
)
load(":platform.bzl", "cxx_by_platform")
CPreprocessor = record(
# The arguments, [arglike things]
args = field([""], []),
# Header specs
headers = field([CHeader.type], []),
# Those should be mutually exclusive with normal headers as per documentation
raw_headers = field(["artifact"], []),
# Directories to be included via -I, [arglike things]
include_dirs = field(["label_relative_path"], []),
# Directories to be included via -isystem, [arglike things]
system_include_dirs = field(["label_relative_path"], []),
# Whether to compile with modules support
uses_modules = field(bool.type, False),
# Modular args to set when modules are in use, [arglike things]
modular_args = field([""], []),
modulemap_path = field("", None),
)
# Methods for transitive_sets must be declared prior to their use.
def _cpreprocessor_args(pres: [CPreprocessor.type]):
args = cmd_args()
for pre in pres:
args.add(pre.args)
return args
def _cpreprocessor_modular_args(pres: [CPreprocessor.type]):
args = cmd_args()
for pre in pres:
args.add(pre.modular_args)
return args
def _cpreprocessor_include_dirs(pres: [CPreprocessor.type]):
args = cmd_args()
for pre in pres:
for d in pre.include_dirs:
args.add("-I")
args.add(d)
for d in pre.system_include_dirs:
args.add("-isystem")
args.add(d)
return args
def _cpreprocessor_uses_modules(children: [bool.type], pres: [[CPreprocessor.type], None]):
if pres:
for pre in pres:
if pre.uses_modules:
return True
return any(children)
# Set of [CPreprocessor.type]. Most nodes have just a single value, but we
# allow > 1 for cxx compilation commands where it we do want > 1 (one for
# exported pp info and one for not-exported).
CPreprocessorTSet = transitive_set(
args_projections = {
"args": _cpreprocessor_args,
"include_dirs": _cpreprocessor_include_dirs,
"modular_args": _cpreprocessor_modular_args,
},
reductions = {
"uses_modules": _cpreprocessor_uses_modules,
},
)
CPreprocessorInfo = provider(fields = [
"set", # "CPreprocessorTSet"
])
# Defines the provider exposed by libraries to test targets,
# so that tests can have access to the private headers of
# the first order deps (for testing purposes).
CPreprocessorForTestsInfo = provider(fields = [
# [str.type] - list of targets in "tests"
"test_names", #
# CPreprocessor.type - the private preprocessor
# for the target which is _only_ exposed to any
# test targets defined in `test_names`
"own_non_exported_preprocessor",
])
# Preprocessor flags
def cxx_attr_preprocessor_flags(ctx: "context", ext: str.type) -> [""]:
return (
ctx.attrs.preprocessor_flags +
cxx_by_language_ext(ctx.attrs.lang_preprocessor_flags, ext) +
flatten(cxx_by_platform(ctx, ctx.attrs.platform_preprocessor_flags)) +
flatten(cxx_by_platform(ctx, cxx_by_language_ext(ctx.attrs.lang_platform_preprocessor_flags, ext)))
)
def cxx_attr_exported_preprocessor_flags(ctx: "context") -> [""]:
return (
ctx.attrs.exported_preprocessor_flags +
_by_language_cxx(ctx.attrs.exported_lang_preprocessor_flags) +
flatten(cxx_by_platform(ctx, ctx.attrs.exported_platform_preprocessor_flags)) +
flatten(cxx_by_platform(ctx, _by_language_cxx(ctx.attrs.exported_lang_platform_preprocessor_flags)))
)
def cxx_inherited_preprocessor_infos(first_order_deps: ["dependency"]) -> [CPreprocessorInfo.type]:
# We filter out nones because some non-cxx rule without such providers could be a dependency, for example
# cxx_binary "fbcode//one_world/cli/util/process_wrapper:process_wrapper" depends on
# python_library "fbcode//third-party-buck/$platform/build/glibc:__project__"
return filter(None, [x.get(CPreprocessorInfo) for x in first_order_deps])
def cxx_merge_cpreprocessors(ctx: "context", own: [CPreprocessor.type], xs: [CPreprocessorInfo.type]) -> "CPreprocessorInfo":
kwargs = {"children": [x.set for x in xs]}
if own:
kwargs["value"] = own
return CPreprocessorInfo(
set = ctx.actions.tset(CPreprocessorTSet, **kwargs),
)
def cxx_exported_preprocessor_info(ctx: "context", headers_layout: CxxHeadersLayout.type, extra_preprocessors: [CPreprocessor.type] = []) -> CPreprocessor.type:
"""
This rule's preprocessor info which is both applied to the compilation of
its source and propagated to the compilation of dependent's sources.
"""
# Modular libraries will provide their exported headers via a symlink tree
# using extra_preprocessors, so should not be put into a header map.
if getattr(ctx.attrs, "modular", False):
exported_headers = []
else:
exported_headers = cxx_attr_exported_headers(ctx, headers_layout)
# Add any headers passed in via constructor params
for pre in extra_preprocessors:
exported_headers += pre.headers
exported_header_map = {
paths.join(h.namespace, h.name): h.artifact
for h in exported_headers
}
raw_headers = []
include_dirs = []
system_include_dirs = []
style = cxx_attr_exported_header_style(ctx)
# If headers-as-raw-headers is enabled, convert exported headers to raw
# headers, with the appropriate include directories.
raw_headers_mode = _attr_headers_as_raw_headers_mode(ctx)
inferred_inc_dirs = as_raw_headers(ctx, exported_header_map, raw_headers_mode)
if inferred_inc_dirs != None:
raw_headers.extend(exported_header_map.values())
if style == HeaderStyle("local"):
include_dirs.extend(inferred_inc_dirs)
else:
system_include_dirs.extend(inferred_inc_dirs)
exported_header_map.clear()
# Add in raw headers and include dirs from attrs.
raw_headers.extend(value_or(ctx.attrs.raw_headers, []))
include_dirs.extend([ctx.label.path.add(x) for x in ctx.attrs.public_include_directories])
system_include_dirs.extend([ctx.label.path.add(x) for x in ctx.attrs.public_system_include_directories])
header_root = prepare_headers(ctx, exported_header_map, "buck-headers")
# Process args to handle the `$(cxx-header-tree)` macro.
args = []
for arg in cxx_attr_exported_preprocessor_flags(ctx):
if _needs_cxx_header_tree_hack(arg):
if header_root == None or header_root.symlink_tree == None:
fail("No headers")
arg = _cxx_header_tree_hack_replacement(header_root.symlink_tree)
args.append(arg)
# Propagate the exported header tree.
if header_root != None:
inc_flag = _header_style_flag(style)
args.extend([inc_flag, header_root.include_path])
# Embed raw headers as hidden artifacts in our args. This means downstream
# cases which use these args don't also need to know to add raw headers.
if raw_headers:
# NOTE(agallagher): It's a bit weird adding an "empty" arg, but this
# appears to do the job (and not e.g. expand to `""`).
args.append(cmd_args().hidden(raw_headers))
modular_args = []
# Append any extra preprocessor info passed in via the constructor params
for pre in extra_preprocessors:
args.extend(pre.args)
modular_args.extend(pre.modular_args)
return CPreprocessor(
args = args,
headers = exported_headers,
raw_headers = raw_headers,
include_dirs = include_dirs,
system_include_dirs = system_include_dirs,
modular_args = modular_args,
)
def cxx_private_preprocessor_info(
ctx: "context",
headers_layout: CxxHeadersLayout.type,
raw_headers: ["artifact"] = [],
extra_preprocessors: [CPreprocessor.type] = [],
non_exported_deps: ["dependency"] = [],
is_test: bool.type = False) -> (CPreprocessor.type, [CPreprocessor.type]):
private_preprocessor = _cxx_private_preprocessor_info(ctx, headers_layout, raw_headers, extra_preprocessors)
test_preprocessors = []
if is_test:
for non_exported_dep in non_exported_deps:
preprocessor_for_tests = non_exported_dep.get(CPreprocessorForTestsInfo)
if preprocessor_for_tests and ctx.label.name in preprocessor_for_tests.test_names:
test_preprocessors.append(preprocessor_for_tests.own_non_exported_preprocessor)
return (private_preprocessor, test_preprocessors)
def _cxx_private_preprocessor_info(
ctx: "context",
headers_layout: CxxHeadersLayout.type,
raw_headers: ["artifact"],
extra_preprocessors: [CPreprocessor.type]) -> CPreprocessor.type:
"""
This rule's preprocessor info which is only applied to the compilation of
its source, and not propagated to dependents.
"""
headers = cxx_attr_headers(ctx, headers_layout)
# `apple_*` rules allow headers to be included via only a basename if those
# are headers (private or exported) from the same target.
if headers_layout.naming == CxxHeadersNaming("apple"):
headers.extend(
_remap_headers_to_basename(
headers + cxx_attr_exported_headers(ctx, headers_layout),
),
)
uses_modules = False
# Include any headers provided via constructor params and determine whether
# to use modules
for pp in extra_preprocessors:
headers += pp.headers
uses_modules = uses_modules or pp.uses_modules
header_map = {paths.join(h.namespace, h.name): h.artifact for h in headers}
all_raw_headers = []
include_dirs = []
# If headers-as-raw-headers is enabled, convert exported headers to raw
# headers, with the appropriate include directories.
raw_headers_mode = _attr_headers_as_raw_headers_mode(ctx)
inferred_inc_dirs = as_raw_headers(ctx, header_map, raw_headers_mode)
if inferred_inc_dirs != None:
all_raw_headers.extend(header_map.values())
include_dirs.extend(inferred_inc_dirs)
header_map.clear()
# Add in raw headers and include dirs from attrs.
all_raw_headers.extend(raw_headers)
include_dirs.extend([ctx.label.path.add(x) for x in ctx.attrs.include_directories])
# Create private header tree and propagate via args.
args = []
header_root = prepare_headers(ctx, header_map, "buck-private-headers")
if header_root != None:
args.extend(["-I", header_root.include_path])
# Embed raw headers as hidden artifacts in our args. This means downstream
# cases which use these args don't also need to know to add raw headers.
if all_raw_headers:
# NOTE(agallagher): It's a bit weird adding an "empty" arg, but this
# appears to do the job (and not e.g. expand to `""`).
args.append(cmd_args().hidden(all_raw_headers))
return CPreprocessor(
args = args,
headers = headers,
raw_headers = all_raw_headers,
include_dirs = include_dirs,
uses_modules = uses_modules,
)
def _by_language_cxx(x: {"": ""}) -> [""]:
return cxx_by_language_ext(x, ".cpp")
def _header_style_flag(style: HeaderStyle.type) -> str.type:
if style == HeaderStyle("local"):
return "-I"
if style == HeaderStyle("system"):
return "-isystem"
fail("unsupported header style: {}".format(style))
def _attr_headers_as_raw_headers_mode(ctx: "context") -> HeadersAsRawHeadersMode.type:
"""
Return the `HeadersAsRawHeadersMode` setting to use for this rule.
"""
mode = get_cxx_toolchain_info(ctx).headers_as_raw_headers_mode
# If the platform hasn't set a raw headers translation mode, we don't do anything.
if mode == None:
return HeadersAsRawHeadersMode("disabled")
# Otherwise use the rule-specific setting, if provided (not available on prebuilt_cxx_library).
if getattr(ctx.attrs, "headers_as_raw_headers_mode", None) != None:
return HeadersAsRawHeadersMode(ctx.attrs.headers_as_raw_headers_mode)
# Fallback to platform default.
return mode
def _needs_cxx_header_tree_hack(arg: "") -> bool.type:
# The macro $(cxx-header-tree) is used in exactly once place, and its a place which isn't very
# Buck v2 compatible. We replace $(cxx-header-tree) with HACK-CXX-HEADER-TREE at attribute time,
# then here we substitute in the real header tree.
return "HACK-CXX-HEADER-TREE" in repr(arg)
def _cxx_header_tree_hack_replacement(header_tree: "artifact") -> "cmd_args":
# Unfortunately, we can't manipulate flags very precisely (for good reasons), so we rely on
# knowing the form it takes.
# The source is: -fmodule-map-file=$(cxx-header-tree)/module.modulemap
return cmd_args(header_tree, format = "-fmodule-map-file={}/module.modulemap")
# Remap the given headers to be includable via their basenames (for use with
# "apple" style header naming).
def _remap_headers_to_basename(headers: [CHeader.type]) -> [CHeader.type]:
remapped_headers = []
for header in headers:
if not header.named:
remapped_headers.append(CHeader(
artifact = header.artifact,
name = paths.basename(header.name),
namespace = "",
named = False,
))
return remapped_headers

View file

@ -0,0 +1,176 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(":cxx_context.bzl", "get_cxx_toolchain_info")
def extract_symbol_names(
ctx: "context",
name: str.type,
objects: ["artifact"],
category: str.type,
identifier: [str.type, None] = None,
undefined_only: bool.type = False,
dynamic: bool.type = False,
prefer_local: bool.type = False,
local_only: bool.type = False,
global_only: bool.type = False) -> "artifact":
"""
Generate a file with a sorted list of symbol names extracted from the given
native objects.
"""
if not objects:
fail("no objects provided")
cxx_toolchain = get_cxx_toolchain_info(ctx)
nm = cxx_toolchain.binary_utilities_info.nm
output = ctx.actions.declare_output(name)
# -A: Prepend all lines with the name of the input file to which it
# corresponds. Added only to make parsing the output a bit easier.
# -P: Generate portable output format
nm_flags = "-AP"
if global_only:
nm_flags += "g"
if undefined_only:
nm_flags += "u"
# darwin objects don't have dynamic symbol tables.
if dynamic and cxx_toolchain.linker_info.type != "darwin":
nm_flags += "D"
script = (
"set -euo pipefail; " +
'"$1" {} "${{@:2}}"'.format(nm_flags) +
# Grab only the symbol name field.
' | cut -d" " -f2 ' +
# Strip off ABI Version (@...) when using llvm-nm to keep compat with buck1
" | cut -d@ -f1 " +
# Sort and dedup symbols. Use the `C` locale and do it in-memory to
# make it significantly faster. CAUTION: if ten of these processes
# run in parallel, they'll have cumulative allocations larger than RAM.
" | LC_ALL=C sort -S 10% -u > {}"
)
ctx.actions.run(
[
"/bin/bash",
"-c",
cmd_args(output.as_output(), format = script),
"",
nm,
] +
objects,
category = category,
identifier = identifier,
prefer_local = prefer_local,
local_only = local_only,
)
return output
def extract_undefined_syms(ctx: "context", output: "artifact", prefer_local: bool.type) -> "artifact":
return extract_symbol_names(
ctx,
output.short_path + ".undefined_syms.txt",
[output],
dynamic = True,
global_only = True,
undefined_only = True,
category = "omnibus_undefined_syms",
identifier = output.basename,
prefer_local = prefer_local,
)
def extract_global_syms(ctx: "context", output: "artifact", prefer_local: bool.type) -> "artifact":
return extract_symbol_names(
ctx,
output.short_path + ".global_syms.txt",
[output],
dynamic = True,
global_only = True,
category = "omnibus_global_syms",
identifier = output.basename,
prefer_local = prefer_local,
)
def _create_symbols_file_from_script(
actions: "actions",
name: str.type,
script: str.type,
symbol_files: ["artifact"],
category: [str.type, None] = None,
prefer_local: bool.type = False) -> "artifact":
"""
Generate a version script exporting symbols from from the given objects and
link args.
"""
all_symbol_files = actions.write(name + ".symbols", symbol_files)
all_symbol_files = cmd_args(all_symbol_files).hidden(symbol_files)
output = actions.declare_output(name)
cmd = [
"/bin/bash",
"-c",
script,
"",
all_symbol_files,
output.as_output(),
]
actions.run(
cmd,
category = category,
prefer_local = prefer_local,
)
return output
def create_undefined_symbols_argsfile(
actions: "actions",
name: str.type,
symbol_files: ["artifact"],
category: [str.type, None] = None,
prefer_local: bool.type = False) -> "artifact":
"""
Combine files with sorted lists of symbols names into an argsfile to pass
to the linker to mark these symbols as undefined (e.g. `-m`).
"""
return _create_symbols_file_from_script(
actions = actions,
name = name,
script = (
"set -euo pipefail; " +
'xargs cat < "$1" | LC_ALL=C sort -S 10% -u -m | sed "s/^/-u/" > $2'
),
symbol_files = symbol_files,
category = category,
prefer_local = prefer_local,
)
def create_global_symbols_version_script(
actions: "actions",
name: str.type,
symbol_files: ["artifact"],
category: [str.type, None] = None,
prefer_local: bool.type = False) -> "artifact":
"""
Combine files with sorted lists of symbols names into an argsfile to pass
to the linker to mark these symbols as undefined (e.g. `-m`).
"""
return _create_symbols_file_from_script(
actions = actions,
name = name,
script = """\
set -euo pipefail
echo "{" > "$2"
echo " global:" >> "$2"
xargs cat < "$1" | LC_ALL=C sort -S 10% -u -m | awk '{print " \\""$1"\\";"}' >> "$2"
echo " local: *;" >> "$2"
echo "};" >> "$2"
""",
symbol_files = symbol_files,
category = category,
prefer_local = prefer_local,
)

View file

@ -0,0 +1,21 @@
load(":defs.bzl", "cxx_hacks")
prelude = native
prelude.python_bootstrap_binary(
name = "make_comp_db",
main = "make_comp_db.py",
visibility = ["PUBLIC"],
)
prelude.python_bootstrap_binary(
name = "makefile_to_dep_file",
main = "makefile_to_dep_file.py",
visibility = ["PUBLIC"],
)
# Required to support the $(cxx-header-tree) macro
cxx_hacks(
name = "cxx_hacks",
visibility = ["PUBLIC"],
)

View file

@ -0,0 +1,19 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
def _cxx_hacks_impl(_ctx):
return [DefaultInfo(), TemplatePlaceholderInfo(
unkeyed_variables = {
"cxx-header-tree": "/dev/null/HACK-CXX-HEADER-TREE",
"output-dwo-dir": "/dev/null/HACK-OUTPUT-DWO-DIR",
},
)]
cxx_hacks = rule(
impl = _cxx_hacks_impl,
attrs = {},
)

View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
"""
Utility to create compilation DBs
$ make_comp_db.py gen --output=entry.json foo.cpp -- g++ -c -fPIC
$ make_comp_db.py gen --output=entry2.json foo2.cpp -- g++ -c -fPIC
$ make_comp_db.py merge --output=comp_db.json entry.json entry2.json
"""
# pyre-unsafe
import argparse
import json
import shlex
import sys
def gen(args):
"""
Generate a single compilation command in JSON form.
"""
entry = {}
entry["file"] = args.directory + "/" + args.filename
entry["directory"] = "."
arguments = []
for arg in args.arguments:
if arg.startswith("@"):
with open(arg[1:]) as argsfile:
for line in argsfile:
# The argsfile's arguments are separated by newlines; we
# don't want those included in the argument list.
arguments.append(" ".join(shlex.split(line)))
else:
arguments.append(arg)
entry["arguments"] = arguments
json.dump(entry, args.output, indent=2)
def merge(args):
"""
Merge multiple compilation DB commands into a single DB.
"""
entries = []
for entry in args.entries:
with open(entry) as f:
entries.append(json.load(f))
json.dump(entries, args.output, indent=2)
def main(argv):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser_gen = subparsers.add_parser("gen")
parser_gen.add_argument("--output", type=argparse.FileType("w"), default=sys.stdout)
parser_gen.add_argument("filename")
parser_gen.add_argument("directory")
parser_gen.add_argument("arguments", nargs="*")
parser_gen.set_defaults(func=gen)
parser_merge = subparsers.add_parser("merge")
parser_merge.add_argument(
"--output", type=argparse.FileType("w"), default=sys.stdout
)
parser_merge.add_argument("entries", nargs="*")
parser_merge.set_defaults(func=merge)
args = parser.parse_args(argv[1:])
args.func(args)
sys.exit(main(sys.argv))

View file

@ -0,0 +1,126 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
# pyre-unsafe
import os
import subprocess
import sys
def rewrite_dep_file(src_path, dst_path):
"""
Convert a makefile to a depfile suitable for use by Buck2. The files we
rewrite look like P488268797.
"""
here = os.getcwd().replace("\\", "/") + "/"
with open(src_path) as f:
body = f.read()
parts = body.split(": ", 1)
body = parts[1] if len(parts) == 2 else ""
# Escaped newlines are not meaningful so remove them.
body = body.replace("\\\n", "")
# Now, recover targets. They are space separated, but we need to ignore
# spaces that are escaped.
pos = 0
deps = []
current_parts = []
def push_slice(s):
if s:
current_parts.append(s)
def flush_current_dep():
if current_parts:
deps.append("".join(current_parts))
current_parts.clear()
while True:
next_pos = body.find(" ", pos)
# If we find the same character we started at, this means we started on
# a piece of whitespace. We know this cannot be escaped, because if we
# started here that means we stopped at the previous character, which
# means it must have been whitespace as well.
if next_pos == pos:
flush_current_dep()
pos += 1
continue
# No more whitespace, so this means that whatever is left from our
# current position to the end is the last dependency (assuming there is
# anything).
if next_pos < 0:
push_slice(body[pos:-1])
break
# Check if this was escaped by looking at the previous character. If it
# was, then insert the part before the escape, and then push a space.
# If it wasn't, then we've reached the end of a dependency.
if next_pos > 0 and body[next_pos - 1] == "\\":
push_slice(body[pos : next_pos - 1])
push_slice(" ")
else:
push_slice(body[pos:next_pos])
flush_current_dep()
pos = next_pos + 1
flush_current_dep()
# Now that we've parsed deps, we need to normalize them.
normalized_deps = []
for dep in deps:
# The paths we get sometimes include "../" components, so get rid
# of those because we want ForwardRelativePath here.
dep = os.path.normpath(dep).replace("\\", "/")
if os.path.isabs(dep):
if dep.startswith(here):
# The dep file included a path inside the build root, but
# expressed an absolute path. In this case, rewrite it to
# be a relative path.
dep = dep[len(here) :]
else:
# The dep file included a path to something outside the
# build root. That's bad (actions shouldn't depend on
# anything outside the build root), but that dependency is
# therefore not tracked by Buck2 (which can only see things
# in the build root), so it cannot be represented as a
# dependency and therefore we don't include it (event if we
# could include it, this could never cause a miss).
continue
normalized_deps.append(dep)
with open(dst_path, "w") as f:
for dep in normalized_deps:
f.write(dep)
f.write("\n")
def main():
"""
Expects the src dep file to be the first argument, dst dep file to be the
second argument, and the command to follow.
"""
ret = subprocess.call(sys.argv[3:])
if ret == 0:
rewrite_dep_file(sys.argv[1], sys.argv[2])
sys.exit(ret)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,88 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:attributes.bzl", "Linkage", "Traversal")
load(
"@prelude//cxx:groups.bzl",
"compute_mappings",
)
load(
"@prelude//cxx:link_groups.bzl",
"LinkGroupInfo",
"parse_link_group_definitions",
)
load(
"@prelude//linking:link_groups.bzl",
"LinkGroupLibInfo",
)
load(
"@prelude//linking:link_info.bzl",
"MergedLinkInfo",
)
load(
"@prelude//linking:linkable_graph.bzl",
"LinkableGraph",
"create_linkable_graph",
"get_linkable_graph_node_map_func",
)
load(
"@prelude//linking:shared_libraries.bzl",
"SharedLibraryInfo",
)
load("@prelude//user:rule_spec.bzl", "RuleRegistrationSpec")
def _v1_attrs(optional_root: bool.type = False):
attrs_root = attrs.dep(providers = [
LinkGroupLibInfo,
LinkableGraph,
MergedLinkInfo,
SharedLibraryInfo,
])
if optional_root:
attrs_root = attrs.option(attrs_root)
return attrs.list(
attrs.tuple(
attrs.string(),
attrs.list(
attrs.tuple(
attrs_root,
attrs.enum(Traversal),
attrs.option(attrs.string()),
attrs.option(attrs.enum(Linkage)),
),
),
),
)
def link_group_map_attr():
v2_attrs = attrs.dep(providers = [LinkGroupInfo])
return attrs.option(attrs.one_of(v2_attrs, _v1_attrs(optional_root = True)), default = None)
def _impl(ctx: "context") -> ["provider"]:
link_groups = parse_link_group_definitions(ctx.attrs.map)
linkable_graph = create_linkable_graph(
ctx,
children = [
mapping.root.node.linkable_graph
for group in link_groups
for mapping in group.mappings
],
)
linkable_graph_node_map = get_linkable_graph_node_map_func(linkable_graph)()
mappings = compute_mappings(groups = link_groups, graph_map = linkable_graph_node_map)
return [
DefaultInfo(),
LinkGroupInfo(groups = link_groups, groups_hash = hash(str(link_groups)), mappings = mappings),
]
registration_spec = RuleRegistrationSpec(
name = "link_group_map",
impl = _impl,
attrs = {
"map": _v1_attrs(),
},
)

View file

@ -0,0 +1,57 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load(
"@prelude//cxx:compile.bzl",
"CxxSrcWithFlags", # @unused Used as a type
)
def cxx_populate_xcode_attributes(
ctx,
srcs: [CxxSrcWithFlags.type],
argsfiles_by_ext: {str.type: "artifact"},
product_name: str.type) -> {str.type: ""}:
converted_srcs = {}
for src in srcs:
file_properties = _get_artifact_owner(src.file)
if src.flags:
# List of resolved_macros will encode as:
# [['\"-some-flag\"'], ['\"-another-flag\"']]
#
# Convert it to a string and rip-out the quotes
# so it appears as ["-some-flag", "-another-flag"]
file_properties["flags"] = [str(flag).replace('\"', "") for flag in src.flags]
converted_srcs[src.file] = file_properties
data = {
"argsfiles_by_ext": {
# Enum types cannot be encoded by our JSON API.
# Use the str representation.
repr(ext).replace('\"', ""): artifact
for ext, artifact in argsfiles_by_ext.items()
},
"headers": _get_artifacts_with_owners(ctx.attrs.headers),
"product_name": product_name,
"srcs": converted_srcs,
}
if hasattr(ctx.attrs, "exported_headers"):
data["exported_headers"] = _get_artifacts_with_owners(ctx.attrs.exported_headers)
return data
def _get_artifacts_with_owners(files: "") -> {"artifact": {str.type: "label"}}:
if type(files) == "dict":
return {artifact: _get_artifact_owner(artifact) for _, artifact in files.items()}
else:
return {file: _get_artifact_owner(file) for file in files}
def _get_artifact_owner(file: "artifact") -> {str.type: "label"}:
if file.owner:
return {"target": file.owner}
else:
return {}