# 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_context.bzl", "get_cxx_toolchain_info")
load("@prelude//cxx:cxx_toolchain_types.bzl", "PicBehavior")
load(
"@prelude//cxx:link.bzl",
"cxx_link_shared_library",
)
load(
"@prelude//cxx:link_types.bzl",
"link_options",
)
load(
"@prelude//haskell:compile.bzl",
"PackagesInfo",
"get_packages_info",
)
load(
"@prelude//haskell:library_info.bzl",
"HaskellLibraryInfo",
"HaskellLibraryProvider",
)
load(
"@prelude//haskell:toolchain.bzl",
"HaskellToolchainInfo",
)
load("@prelude//haskell:util.bzl", "attr_deps", "get_artifact_suffix")
load("@prelude//linking:execution_preference.bzl", "LinkExecutionPreference")
load(
"@prelude//linking:link_info.bzl",
"LinkArgs",
"LinkInfo",
"LinkStyle",
"get_lib_output_style",
"set_linkable_link_whole",
"to_link_strategy",
)
load(
"@prelude//linking:linkable_graph.bzl",
"LinkableGraph",
"LinkableRootInfo",
"create_linkable_graph",
"get_deps_for_link",
"get_link_info",
)
load(
"@prelude//linking:shared_libraries.bzl",
"SharedLibraryInfo",
"create_shlib_symlink_tree",
"traverse_shared_library_info",
"with_unique_str_sonames",
)
load("@prelude//linking:types.bzl", "Linkage")
load(
"@prelude//utils:graph_utils.bzl",
"depth_first_traversal",
"depth_first_traversal_by",
)
load("@prelude//utils:utils.bzl", "flatten")
GHCiPreloadDepsInfo = record(
preload_symlinks = dict[str, Artifact],
preload_deps_root = Artifact,
)
USER_GHCI_PATH = "user_ghci_path"
BINUTILS_PATH = "binutils_path"
GHCI_LIB_PATH = "ghci_lib_path"
CC_PATH = "cc_path"
CPP_PATH = "cpp_path"
CXX_PATH = "cxx_path"
GHCI_PACKAGER = "ghc_pkg_path"
GHCI_GHC_PATH = "ghc_path"
HaskellOmnibusData = record(
omnibus = Artifact,
so_symlinks_root = Artifact,
)
def _write_final_ghci_script(
ctx: AnalysisContext,
omnibus_data: HaskellOmnibusData,
packages_info: PackagesInfo,
packagedb_args: cmd_args,
prebuilt_packagedb_args: cmd_args,
iserv_script: Artifact,
start_ghci_file: Artifact,
ghci_bin: Artifact,
haskell_toolchain: HaskellToolchainInfo,
ghci_script_template: Artifact,
enable_profiling: bool) -> Artifact:
srcs = " ".join(
[
paths.normalize(
paths.join(
paths.relativize(str(ctx.label.path), "fbcode"),
s,
),
)
for s in ctx.attrs.srcs
],
)
# Collect compiler flags
compiler_flags = cmd_args(
# TODO(gustavoavena): do I need to filter these flags?
filter(lambda x: x == "-O", haskell_toolchain.compiler_flags),
delimiter = " ",
)
compiler_flags.add([
"-fPIC",
"-fexternal-dynamic-refs",
])
if enable_profiling:
compiler_flags.add([
"-prof",
"-osuf p_o",
"-hisuf p_hi",
])
compiler_flags.add(ctx.attrs.compiler_flags)
omnibus_so = omnibus_data.omnibus
final_ghci_script = _replace_macros_in_script_template(
ctx,
script_template = ghci_script_template,
haskell_toolchain = haskell_toolchain,
ghci_bin = ghci_bin,
exposed_package_args = packages_info.exposed_package_args,
packagedb_args = packagedb_args,
prebuilt_packagedb_args = prebuilt_packagedb_args,
start_ghci = start_ghci_file,
iserv_script = iserv_script,
squashed_so = omnibus_so,
compiler_flags = compiler_flags,
srcs = srcs,
output_name = ctx.label.name,
)
return final_ghci_script
def _build_haskell_omnibus_so(ctx: AnalysisContext) -> HaskellOmnibusData:
link_style = LinkStyle("static_pic")
if False:
# TODO(nga): typechecker raises issue here.
def unknown():
pass
link_style = unknown()
# pic_behavior = PicBehavior("always_enabled")
pic_behavior = PicBehavior("supported")
preload_deps = ctx.attrs.preload_deps
all_deps = attr_deps(ctx) + preload_deps + ctx.attrs.template_deps
linkable_graph_ = create_linkable_graph(
ctx,
deps = all_deps,
)
# Keep only linkable nodes
graph_nodes = {
n.label: n.linkable
for n in linkable_graph_.nodes.traverse()
if n.linkable
}
# Map node label to its dependencies' labels
dep_graph = {
nlabel: get_deps_for_link(n, to_link_strategy(link_style), pic_behavior)
for nlabel, n in graph_nodes.items()
}
all_direct_deps = []
for dep in all_deps:
graph = dep.get(LinkableGraph)
if graph:
all_direct_deps.append(graph.label)
dep_graph[ctx.label] = all_direct_deps
# Need to exclude all transitive deps of excluded deps
all_nodes_to_exclude = depth_first_traversal(
dep_graph,
[dep[LinkableGraph].label for dep in preload_deps if LinkableGraph in dep],
)
# Body nodes should support haskell omnibus (e.g. cxx_library)
# and can't be prebuilt tp dependencies
body_nodes = {}
# Prebuilt (i.e. third-party) nodes shouldn't be statically linked on
# the omnibus, but we need to keep track of them because they're a
# dependency of it and are linked dynamically.
prebuilt_so_deps = {}
# Helper to get body nodes and prebuilt dependencies of the
# omnibus SO (which should dynamically linked) during BFS traversal
def find_deps_for_body(node_label: Label):
deps = dep_graph[node_label]
final_deps = []
for node_label in deps:
node = graph_nodes[node_label]
# We process these libs even if they're excluded, as they need to
# be added to the link line.
if "prebuilt_so_for_haskell_omnibus" in node.labels:
# If the library is marked as force-static, then it won't provide
# shared libs and we'll have to link is statically.
if node.preferred_linkage == Linkage("static"):
body_nodes[node_label] = None
else:
prebuilt_so_deps[node_label] = None
if node_label in all_nodes_to_exclude:
continue
if "supports_haskell_omnibus" in node.labels and "prebuilt_so_for_haskell_omnibus" not in node.labels:
body_nodes[node_label] = None
final_deps.append(node_label)
return final_deps
# This is not the final set of body nodes, because it still includes
# nodes that don't support omnibus (e.g. haskell_library nodes)
depth_first_traversal_by(
dep_graph,
[ctx.label],
find_deps_for_body,
)
# After collecting all the body nodes, get all their linkables (e.g. `.a`
# files) that will be part of the omnibus SO.
body_link_infos = {}
for node_label in body_nodes.keys():
node = graph_nodes[node_label]
node_target = node_label.raw_target()
if (node_target in body_link_infos):
# Not skipping these leads to duplicate symbol errors
continue
output_style = get_lib_output_style(
to_link_strategy(link_style),
node.preferred_linkage,
pic_behavior = pic_behavior,
)
li = get_link_info(node, output_style)
linkables = [
# All symbols need to be included in the omnibus so, even if
# they're not being referenced yet, so we should enable
# link_whole which passes the `--whole-archive` linker flag.
set_linkable_link_whole(linkable)
for linkable in li.linkables
]
new_li = LinkInfo(
name = li.name,
pre_flags = li.pre_flags,
post_flags = li.post_flags,
linkables = linkables,
external_debug_info = li.external_debug_info,
)
body_link_infos[node_target] = new_li
# Handle third-party dependencies of the omnibus SO
tp_deps_shared_link_infos = {}
prebuilt_shlibs = []
for node_label in prebuilt_so_deps.keys():
node = graph_nodes[node_label]
output_style = get_lib_output_style(
to_link_strategy(LinkStyle("shared")),
node.preferred_linkage,
pic_behavior = pic_behavior,
)
shared_li = node.link_infos.get(output_style, None)
if shared_li != None:
tp_deps_shared_link_infos[node_label] = shared_li.default
prebuilt_shlibs.extend(node.shared_libs.libraries)
# Create symlinks to the TP dependencies' SOs
so_symlinks_root_path = ctx.label.name + ".so-symlinks"
so_symlinks_root = create_shlib_symlink_tree(
actions = ctx.actions,
out = so_symlinks_root_path,
shared_libs = prebuilt_shlibs,
)
linker_info = get_cxx_toolchain_info(ctx).linker_info
soname = "libghci_dependencies.so"
extra_ldflags = [
"-rpath",
"$ORIGIN/{}".format(so_symlinks_root_path),
]
link_result = cxx_link_shared_library(
ctx,
soname,
opts = link_options(
links = [
LinkArgs(flags = extra_ldflags),
LinkArgs(infos = body_link_infos.values()),
LinkArgs(infos = tp_deps_shared_link_infos.values()),
],
category_suffix = "omnibus",
link_weight = linker_info.link_weight,
identifier = soname,
link_execution_preference = LinkExecutionPreference("any"),
),
)
omnibus = link_result.linked_object.output
return HaskellOmnibusData(
omnibus = omnibus,
so_symlinks_root = so_symlinks_root,
)
# Use the script_template_processor.py script to generate a script from a
# script template.
def _replace_macros_in_script_template(
ctx: AnalysisContext,
script_template: Artifact,
haskell_toolchain: HaskellToolchainInfo,
# Optional artifacts
ghci_bin: Artifact | None = None,
start_ghci: Artifact | None = None,
iserv_script: Artifact | None = None,
squashed_so: Artifact | None = None,
# Optional cmd_args
exposed_package_args: [cmd_args, None] = None,
packagedb_args: [cmd_args, None] = None,
prebuilt_packagedb_args: [cmd_args, None] = None,
compiler_flags: [cmd_args, None] = None,
# Optional string args
srcs: [str, None] = None,
output_name: [str, None] = None,
ghci_iserv_path: [Artifact, None] = None,
preload_libs: [str, None] = None) -> Artifact:
toolchain_paths = {
BINUTILS_PATH: haskell_toolchain.ghci_binutils_path,
GHCI_LIB_PATH: haskell_toolchain.ghci_lib_path.get(DefaultInfo).default_outputs[0],
CC_PATH: haskell_toolchain.ghci_cc_path,
CPP_PATH: haskell_toolchain.ghci_cpp_path,
CXX_PATH: haskell_toolchain.ghci_cxx_path,
GHCI_PACKAGER: haskell_toolchain.ghci_packager.get(DefaultInfo).default_outputs[0],
GHCI_GHC_PATH: haskell_toolchain.ghci_ghc_path.get(DefaultInfo).default_outputs[0],
}
if ghci_bin != None:
toolchain_paths[USER_GHCI_PATH] = ghci_bin.short_path
final_script = ctx.actions.declare_output(
script_template.basename if not output_name else output_name,
)
script_template_processor = haskell_toolchain.script_template_processor[RunInfo]
replace_cmd = cmd_args(script_template_processor)
replace_cmd.add(cmd_args(script_template, format = "--script_template={}"))
for name, path in toolchain_paths.items():
if path:
replace_cmd.add(cmd_args(path, format = "--{}={{}}".format(name)))
replace_cmd.add(cmd_args(
final_script.as_output(),
format = "--output={}",
))
replace_cmd.add(cmd_args(
ctx.label.name,
format = "--target_name={}",
))
exposed_package_args = exposed_package_args if exposed_package_args != None else ""
replace_cmd.add(cmd_args(
cmd_args(exposed_package_args, delimiter = " "),
format = "--exposed_packages={}",
))
if packagedb_args != None:
replace_cmd.add(cmd_args(
packagedb_args,
format = "--package_dbs={}",
))
if prebuilt_packagedb_args != None:
replace_cmd.add(cmd_args(
prebuilt_packagedb_args,
format = "--prebuilt_package_dbs={}",
))
# Tuple containing orig value (for null check), macro value and flag name
optional_flags = [
(
start_ghci,
start_ghci.short_path if start_ghci != None else "",
"--start_ghci",
),
(iserv_script, "iserv", "--iserv_path"),
(
squashed_so,
squashed_so.short_path if squashed_so != None else "",
"--squashed_so",
),
(compiler_flags, compiler_flags, "--compiler_flags"),
(srcs, srcs, "--srcs"),
(ghci_iserv_path, ghci_iserv_path, "--ghci_iserv_path"),
(preload_libs, preload_libs, "--preload_libs"),
]
for (orig_val, macro_value, flag) in optional_flags:
if orig_val != None:
replace_cmd.add(cmd_args(
macro_value,
format = flag + "={}",
))
ctx.actions.run(
replace_cmd,
category = "replace_template_{}".format(
script_template.basename.replace("-", "_"),
),
local_only = True,
)
return final_script
def _write_iserv_script(
ctx: AnalysisContext,
preload_deps_info: GHCiPreloadDepsInfo,
haskell_toolchain: HaskellToolchainInfo,
enable_profiling: bool) -> Artifact:
ghci_iserv_template = haskell_toolchain.ghci_iserv_template
if (not ghci_iserv_template):
fail("ghci_iserv_template missing in haskell_toolchain")
preload_libs = ":".join(
[paths.join(
"${DIR}",
preload_deps_info.preload_deps_root.short_path,
so,
) for so in sorted(preload_deps_info.preload_symlinks)],
)
if enable_profiling:
ghci_iserv_path = haskell_toolchain.ghci_iserv_prof_path
else:
ghci_iserv_path = haskell_toolchain.ghci_iserv_path
iserv_script_name = "iserv"
if enable_profiling:
iserv_script_name += "-prof"
iserv_script = _replace_macros_in_script_template(
ctx,
script_template = ghci_iserv_template,
output_name = iserv_script_name,
haskell_toolchain = haskell_toolchain,
ghci_iserv_path = ghci_iserv_path.get(DefaultInfo).default_outputs[0],
preload_libs = preload_libs,
)
return iserv_script
def _build_preload_deps_root(
ctx: AnalysisContext,
haskell_toolchain: HaskellToolchainInfo) -> GHCiPreloadDepsInfo:
preload_deps = ctx.attrs.preload_deps
preload_symlinks = {}
preload_libs_root = ctx.label.name + ".preload-symlinks"
for preload_dep in preload_deps:
if SharedLibraryInfo in preload_dep:
slib_info = preload_dep[SharedLibraryInfo]
shlib = traverse_shared_library_info(slib_info)
for soname, shared_lib in with_unique_str_sonames(shlib).items():
preload_symlinks[soname] = shared_lib.lib.output
# TODO(T150785851): build or get SO for direct preload_deps
# TODO(T150785851): find out why the only SOs missing are the ones from
# the preload_deps themselves, even though the ones from their deps are
# already there.
if LinkableRootInfo in preload_dep:
linkable_root_info = preload_dep[LinkableRootInfo]
preload_so_name = linkable_root_info.name
linkables = map(lambda x: x.objects, linkable_root_info.link_infos.default.linkables)
object_file = flatten(linkables)[0]
preload_so = ctx.actions.declare_output(preload_so_name)
link = cmd_args(haskell_toolchain.linker)
link.add(haskell_toolchain.linker_flags)
link.add(ctx.attrs.linker_flags)
link.add("-o", preload_so.as_output())
link.add(
"-shared",
"-dynamic",
"-optl",
"-Wl,-soname",
"-optl",
"-Wl," + preload_so_name,
)
link.add(object_file)
ctx.actions.run(
link,
category = "haskell_ghci_link",
identifier = preload_so_name,
)
preload_symlinks[preload_so_name] = preload_so
preload_deps_root = ctx.actions.symlinked_dir(preload_libs_root, preload_symlinks)
return GHCiPreloadDepsInfo(
preload_deps_root = preload_deps_root,
preload_symlinks = preload_symlinks,
)
# Symlink the ghci binary that will be used, e.g. the internal fork in Haxlsh
def _symlink_ghci_binary(ctx, haskell_toolchain: HaskellToolchainInfo, ghci_bin: Artifact):
ghci_bin_dep = ctx.attrs.ghci_bin_dep
if not ghci_bin_dep:
ghci_bin_dep = haskell_toolchain.ghci_ghc_path
# NOTE: In the buck1 version we'd symlink the binary only if a custom one
# was provided, but in buck2 we're always setting `ghci_bin_dep` (i.e.
# to default one if custom wasn't provided).
src = ghci_bin_dep[DefaultInfo].default_outputs[0]
ctx.actions.symlink_file(ghci_bin.as_output(), src)
def _first_order_haskell_deps(
ctx: AnalysisContext,
enable_profiling: bool) -> list[HaskellLibraryInfo]:
libs = []
for dep in ctx.attrs.deps:
if HaskellLibraryProvider in dep:
if enable_profiling:
libs.append(dep[HaskellLibraryProvider].prof_lib.values())
else:
libs.append(dep[HaskellLibraryProvider].lib.values())
return dedupe(flatten(libs))
# Creates the start.ghci script used to load the packages during startup
def _write_start_ghci(
ctx: AnalysisContext,
script_file: Artifact,
enable_profiling: bool):
start_cmd = cmd_args()
# Reason for unsetting `LD_PRELOAD` env var obtained from D6255224:
# "Certain libraries (like allocators) cannot be loaded after the process
# has started. When needing to use these libraries, send them to a
# user-supplied script for handling them appropriately. Running the real
# iserv with these libraries under LD_PRELOAD accomplishes this.
# To ensure the LD_PRELOAD env doesn't make it to subsequently forked
# processes, the very first action of start.ghci is to unset the variable."
start_cmd.add("System.Environment.unsetEnv \"LD_PRELOAD\"")
set_cmd = cmd_args(":set", delimiter = " ")
first_order_deps = list(map(
lambda dep: dep.name + "-" + dep.version,
_first_order_haskell_deps(ctx, enable_profiling),
))
deduped_deps = {pkg: 1 for pkg in first_order_deps}.keys()
package_list = cmd_args(
deduped_deps,
format = "-package {}",
delimiter = " ",
)
set_cmd.add(package_list)
set_cmd.add("\n")
start_cmd.add(set_cmd)
header_ghci = ctx.actions.declare_output("header.ghci")
ctx.actions.write(header_ghci.as_output(), start_cmd)
if ctx.attrs.ghci_init:
append_ghci_init = cmd_args()
append_ghci_init.add(
["sh", "-c", 'cat "$1" "$2" > "$3"', "--", header_ghci, ctx.attrs.ghci_init, script_file.as_output()],
)
ctx.actions.run(append_ghci_init, category = "append_ghci_init")
else:
ctx.actions.copy_file(script_file, header_ghci)
def haskell_ghci_impl(ctx: AnalysisContext) -> list[Provider]:
haskell_toolchain = ctx.attrs._haskell_toolchain[HaskellToolchainInfo]
enable_profiling = ctx.attrs.enable_profiling
start_ghci_file = ctx.actions.declare_output("start.ghci")
_write_start_ghci(ctx, start_ghci_file, enable_profiling)
ghci_bin = ctx.actions.declare_output(ctx.attrs.name + ".bin/ghci")
_symlink_ghci_binary(ctx, haskell_toolchain, ghci_bin)
preload_deps_info = _build_preload_deps_root(ctx, haskell_toolchain)
ghci_script_template = haskell_toolchain.ghci_script_template
if (not ghci_script_template):
fail("ghci_script_template missing in haskell_toolchain")
iserv_script = _write_iserv_script(
ctx,
preload_deps_info,
haskell_toolchain,
enable_profiling,
)
link_style = LinkStyle("static_pic")
packages_info = get_packages_info(
ctx,
link_style,
specify_pkg_version = True,
enable_profiling = enable_profiling,
)
# Create package db symlinks
package_symlinks = []
package_symlinks_root = ctx.label.name + ".packages"
packagedb_args = cmd_args(delimiter = " ")
prebuilt_packagedb_args_set = {}
for lib in packages_info.transitive_deps.traverse():
if lib.is_prebuilt:
prebuilt_packagedb_args_set[lib.db] = None
else:
lib_symlinks_root = paths.join(
package_symlinks_root,
lib.name,
)
lib_symlinks = {
"packagedb": lib.db,
}
for prof, import_dir in lib.import_dirs.items():
artifact_suffix = get_artifact_suffix(link_style, prof)
lib_symlinks["hi-" + artifact_suffix] = import_dir
for o in lib.libs:
lib_symlinks[o.short_path] = o
symlinked_things = ctx.actions.symlinked_dir(
lib_symlinks_root,
lib_symlinks,
)
package_symlinks.append(symlinked_things)
packagedb_args.add(
paths.join(
lib_symlinks_root,
"packagedb",
),
)
prebuilt_packagedb_args = cmd_args(prebuilt_packagedb_args_set.keys(), delimiter = " ")
script_templates = []
for script_template in ctx.attrs.extra_script_templates:
final_script = _replace_macros_in_script_template(
ctx,
script_template = script_template,
haskell_toolchain = haskell_toolchain,
ghci_bin = ghci_bin,
exposed_package_args = packages_info.exposed_package_args,
packagedb_args = packagedb_args,
prebuilt_packagedb_args = prebuilt_packagedb_args,
)
script_templates.append(final_script)
omnibus_data = _build_haskell_omnibus_so(ctx)
final_ghci_script = _write_final_ghci_script(
ctx,
omnibus_data,
packages_info,
packagedb_args,
prebuilt_packagedb_args,
iserv_script,
start_ghci_file,
ghci_bin,
haskell_toolchain,
ghci_script_template,
enable_profiling,
)
outputs = [
start_ghci_file,
ghci_bin,
preload_deps_info.preload_deps_root,
iserv_script,
omnibus_data.omnibus,
omnibus_data.so_symlinks_root,
final_ghci_script,
]
outputs.extend(package_symlinks)
outputs.extend(script_templates)
# As default output (e.g. used in `$(location )` buck macros), the rule
# should output a directory containing symlinks to all scripts and resources
# (e.g. shared objects, package configs)
output_artifacts = {o.short_path: o for o in outputs}
root_output_dir = ctx.actions.symlinked_dir(
"__{}__".format(ctx.label.name),
output_artifacts,
)
run = cmd_args(final_ghci_script, hidden = outputs)
return [
DefaultInfo(default_outputs = [root_output_dir]),
RunInfo(args = run),
]