# 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", "LinkStrategy")
load("@prelude//rust:build_params.bzl", "MetadataKind")
load("@prelude//rust:link_info.bzl", "RustLinkInfo")
load("@prelude//rust/rust-analyzer:provider.bzl", "RustAnalyzerInfo")
load("@prelude//utils:type_defs.bzl", "is_list")
TargetInfo = dict[str, typing.Any]
MacroOutput = record(
actual = TargetLabel,
dylib = Artifact,
)
ExpandedAndResolved = record(
expanded_targets = list[TargetLabel],
queried_proc_macros = dict[TargetLabel, MacroOutput],
resolved_deps = dict[TargetLabel, TargetInfo],
)
def materialize(
ctx: bxl.Context,
target: bxl.ConfiguredTargetNode) -> Artifact:
analysis = ctx.analysis(target)
sources = analysis.providers()[DefaultInfo].sub_targets["sources"][DefaultInfo].default_outputs[0]
return sources
def _get_nullable_attr(attrs, key: str) -> typing.Any:
nullable = getattr(attrs, key, None)
return nullable.value() if nullable != None else None
def _process_target_config(
ctx: bxl.Context,
target: bxl.ConfiguredTargetNode,
analysis: bxl.AnalysisResult,
in_workspace: bool) -> TargetInfo:
target = target.unwrap_forward()
providers = analysis.providers()
ra_info = providers[RustAnalyzerInfo]
# convert all source paths to absolute paths
resolved_attrs = target.resolved_attrs_eager(ctx)
# Using srcs instead of .sources() gives the resolved artifacts if provided with a buck rule as a src label.
# For example, this is used in cxx powered crates internally
srcs = []
for src in resolved_attrs.srcs:
srcs.append(src)
# remove the configured platform from the deps. for example,
# `fbsource//third-party/rust:tracing (ovr_config//platform/linux:x86_64-fbcode-platform010-clang-9f23200ddcddc3cb)`
# becomes `fbsource//third-party/rust:tracing`.
deps = [dep.label.raw_target() for dep in ra_info.rust_deps]
# Grab only the values that the the gen-rules are being mapped to.
mapped_srcs = {}
for key, v in resolved_attrs.mapped_srcs.items():
mapped_srcs[v] = key
# remove the configured platform from named deps.
if is_list(resolved_attrs.named_deps):
named_deps_names = providers[DefaultInfo].sub_targets["named_deps"][DefaultInfo].default_outputs[0]
named_deps = [named_deps_names]
for _alias, dep in resolved_attrs.named_deps:
named_deps.append(dep.label.raw_target())
else:
named_deps = {}
for dep, alias in resolved_attrs.named_deps.items():
named_deps[dep] = alias.label.raw_target()
# remove the configured platform for tests
tests = []
for test in resolved_attrs.tests:
tests.append(test.raw_target())
env = {k: cmd_args(v, delimiter = "") for k, v in ra_info.env.items()}
# copy over the absolute paths and raw targets into the output
attrs = target.attrs_eager()
return {
"crate": ra_info.crate.simple,
"crate_dynamic": ra_info.crate.dynamic,
"crate_root": ra_info.crate_root,
"deps": deps,
"edition": _get_nullable_attr(attrs, "edition"),
"env": env,
"features": resolved_attrs.features,
"in_workspace": in_workspace,
"kind": target.rule_type,
"label": target.label.raw_target(),
"mapped_srcs": mapped_srcs,
"name": resolved_attrs.name,
"named_deps": named_deps,
"proc_macro": _get_nullable_attr(attrs, "proc_macro"),
"project_relative_buildfile": ctx.fs.project_rel_path(target.buildfile_path),
"rustc_flags": _get_nullable_attr(attrs, "rustc_flags"),
"source_folder": materialize(ctx, target), # Always generate the source folder. Let rust-project resolve whether or not to use it
"srcs": srcs,
"tests": tests,
}
def _select_assume_default(value):
"""Unwrap select() values, assuming the DEFAULT value is always used.
"""
if isinstance(value, bxl.SelectConcat):
items = []
for raw_item in value.select_iter():
if isinstance(raw_item, list):
items.extend(raw_item)
else:
unwrapped = _select_assume_default(raw_item)
if isinstance(unwrapped, list):
items.extend(unwrapped)
return items
if isinstance(value, bxl.SelectDict):
return value.get_select_entry("DEFAULT")
return value
def _get_attr_no_select(target: bxl.UnconfiguredTargetNode, attr: str) -> typing.Any:
return _select_assume_default(target.get_attr(attr))
def gather_unconfigured_deps(ctx: bxl.Context, targets: list[TargetLabel]) -> dict[TargetLabel, TargetInfo]:
"""Make a best-effort attempt to gather target and dependency information for
the targets given.
This intended for targets where we cannot use the configured target nodes,
typically because they have compatible_with constraints that do not match the
current machine.
"""
out = {}
unconfigured_targets = ctx.unconfigured_targets(targets)
for target in unconfigured_targets:
deps = []
for dep in _get_attr_no_select(target, "deps") or []:
deps.append(dep.raw_target())
raw_env = _get_attr_no_select(target, "env") or {}
env = {k: cmd_args(v, delimiter = "") for k, v in raw_env.items()}
srcs = [src.short_path for src in _get_attr_no_select(target, "srcs") or []]
mapped_srcs = {}
raw_mapped_srcs = _get_attr_no_select(target, "mapped_srcs") or {}
for t, t_srcs in raw_mapped_srcs.items():
mapped_srcs[t.raw_target()] = t_srcs
abs_buildfile_path = ctx.root() + "/" + target.buildfile_path.cell + "/" + target.buildfile_path.path
source_folder = abs_buildfile_path.removesuffix("/TARGETS").removesuffix("/BUCK")
crate_root = _get_attr_no_select(target, "crate_root")
if not crate_root:
for src in srcs:
if src.endswith("/lib.rs") or src.endswith("/main.rs"):
crate_root = src
break
if not crate_root:
continue
tests = []
for test in _get_attr_no_select(target, "tests") or []:
tests.append(test.raw_target())
info = {
"crate": _get_attr_no_select(target, "crate"),
"crate_dynamic": _get_attr_no_select(target, "crate_dynamic"),
"crate_root": crate_root,
"deps": deps,
"edition": _get_attr_no_select(target, "edition"),
"env": env,
"features": _get_attr_no_select(target, "features"),
"in_workspace": False,
"kind": target.rule_type,
"label": target.label,
"mapped_srcs": mapped_srcs,
"name": target.get_attr("name"),
"named_deps": {},
"proc_macro": _get_attr_no_select(target, "proc_macro"),
"project_relative_buildfile": ctx.fs.project_rel_path(target.buildfile_path),
"rustc_flags": _get_attr_no_select(target, "rustc_flags"),
"source_folder": source_folder,
"srcs": srcs,
"tests": tests,
}
out[target.label] = info
return out
def gather_deps(
ctx: bxl.Context,
target_analysis: dict[Label, bxl.AnalysisResult],
workspaces: list[TargetLabel]) -> dict[TargetLabel, TargetInfo]:
targets = set()
for _target, analysis in target_analysis.items():
info = analysis.providers().get(RustAnalyzerInfo)
if info:
for target_set in info.transitive_target_set:
targets.add(target_set)
#TODO(romanp) support set as target_universe arg
outputs = ctx.target_universe(list(targets)).target_set()
out = {}
# Eagerly analyze targets
analysis = ctx.analysis(outputs)
for target in outputs:
attrs = target.attrs_lazy()
label = target.label.with_sub_target()
in_workspace = label in target_analysis
candidate_workspaces = attrs.get("_workspaces")
if candidate_workspaces:
for candidate_workspace in candidate_workspaces.value():
if candidate_workspace.raw_target() in workspaces:
in_workspace = True
break
target_info = _process_target_config(
ctx = ctx,
target = target,
analysis = analysis[target.label.with_sub_target()],
in_workspace = in_workspace,
)
out[target.label.raw_target()] = target_info
return out
def expand_proc_macros(
ctx: bxl.Context,
target_analysis: dict[Label, bxl.AnalysisResult]) -> dict[TargetLabel, MacroOutput]:
macros = set()
for _target, analysis in target_analysis.items():
info = analysis.providers().get(RustAnalyzerInfo)
if info:
macros.update([d.label for d in info.available_proc_macros])
proc_macro_analysis = ctx.analysis(list(macros))
out = {}
for target, analysis in proc_macro_analysis.items():
rlib = analysis.providers()[RustLinkInfo].strategies[LinkStrategy("shared")].outputs[MetadataKind("link")]
label = target.raw_target()
out[label] = MacroOutput(
actual = label,
dylib = rlib,
)
return out
# Returns a list of all the expanded targets including any workspaces, followed by just the workspaces
def expand_targets(
ctx: bxl.Context,
targets: list[TargetLabel],
exclude_workspaces: bool) -> (dict[Label, bxl.AnalysisResult], list[TargetLabel]):
target_universe = ctx.target_universe(targets).target_set()
kind_target_list = ctx.cquery().kind("^(rust_binary|rust_library|rust_test|alias)$", target_universe)
# Allow targets to opt-in to being treated as rust-analyzer-compatible.
# This is used for cross-compilation targets that apply Buck transitions to Rust rules.
labeled_target_list = ctx.cquery().attrfilter("labels", "rust_analyzer_target", target_universe)
expanded_targets = {t.label.raw_target(): t for t in kind_target_list + labeled_target_list}
# Map of potential workspaces to a list of the targets that name these as potential workspaces
possible_workspaces = {}
if not exclude_workspaces:
for label, t in expanded_targets.items():
workspaces = t.attrs_lazy().get("_workspaces")
if workspaces:
for workspace in workspaces.value():
if not ctx.target_exists(str(workspace.raw_target())):
continue
possible_workspaces.setdefault(workspace.raw_target(), []).append(label)
workspace_analysis = ctx.analysis(ctx.target_universe(possible_workspaces.keys()).target_set())
active_workspaces = {}
for workspace_label, analysis in workspace_analysis.items():
workspace = workspace_label.raw_target()
candidate_deps = possible_workspaces[workspace]
workspace_info = analysis.providers().get(RustAnalyzerInfo)
if workspace_info:
workspace_deps = {t.raw_target(): () for t in workspace_info.transitive_target_set}
else:
workspace_deps = {}
for d in candidate_deps:
if d in workspace_deps:
active_workspaces[workspace] = ()
# Remove the target from the expanded targets. This is correct because we know
# that the target will reappear later as a dep of the workspace. To understand why
# it's necessary, consider the case where the target is a proc macro: Later doing
# cquery deps(proc_macro + workspace) will result in the proc macro appearing twice,
# once in its exec configuration and once in its target configuration
# FIXME: Add a test for this. It's currently a bit hard to test because proc macros
# in the prelude are a bit hard in general
expanded_targets.pop(d, None)
target_set = ctx.target_universe(expanded_targets.keys() + active_workspaces.keys()).target_set()
target_analysis = ctx.analysis(target_set)
return target_analysis, sorted(possible_workspaces.keys())
def resolve_targets_impl(ctx: bxl.Context) -> None:
# equivalent of `flat_map`ing
targets = [target for sublist in ctx.cli_args.targets for target in sublist]
actions = ctx.bxl_actions().actions
target_analysis, workspaces = expand_targets(ctx, targets, ctx.cli_args.exclude_workspaces)
queried_proc_macros = expand_proc_macros(ctx, target_analysis)
resolved_deps = gather_deps(ctx, target_analysis, workspaces)
expanded_targets = sorted([t.raw_target() for t in target_analysis.keys()])
if not expanded_targets:
# ctx.analysis() drops targets that are incompatible with the current buck
# configuration (see the `skip_incompatible` keyword argument).
#
# If we have no expanded targets at all, try to find sufficient metadata about
# the requested targets using unconfigured buck targets.
expanded_targets = targets
resolved_deps = gather_unconfigured_deps(ctx, targets)
artifact = actions.declare_output("resolve_targets.json")
artifacts = actions.write_json(
artifact,
ExpandedAndResolved(
expanded_targets = expanded_targets,
queried_proc_macros = queried_proc_macros,
resolved_deps = resolved_deps,
),
with_inputs = True,
absolute = True,
pretty = ctx.cli_args.pretty,
)
ctx.output.ensure_multiple(artifacts)
ctx.output.print(ctx.output.ensure(artifact).abs_path())
def resolve_owning_buildfile_impl(ctx: bxl.Context) -> None:
# depending on the input, determine the initial set of targets
if ctx.cli_args.files:
targets = ctx.uquery().owner(ctx.cli_args.files)
elif ctx.cli_args.buildfiles:
targets = [ctx.uquery().targets_in_buildfile(buildfile) for buildfile in ctx.cli_args.buildfiles]
# equivalent of `flat_map`ing
targets = [target for sublist in targets for target in sublist]
targets = ctx.uquery().kind("^(rust_binary|rust_library|rust_test)$", targets)
elif ctx.cli_args.targets:
# equivalent of `flat_map`ing
targets = [target for sublist in ctx.cli_args.targets for target in sublist]
targets = ctx.unconfigured_targets(targets)
else:
fail("Neither `--files`, `--targets`, nor `--buildfiles` were specified; this is a bug")
# group targets by their buildfile
targets_by_buildfile = {}
for target in targets:
buildfile_path = ctx.fs.abs_path_unsafe(target.buildfile_path)
if buildfile_path not in targets_by_buildfile:
targets_by_buildfile[buildfile_path] = utarget_set()
targets_by_buildfile[buildfile_path] += utarget_set([target])
# collect extra targets from each buildfile
extra_targets_by_buildfile = {}
for buildfile_path in targets_by_buildfile:
extra_targets = ctx.uquery().targets_in_buildfile("{}".format(buildfile_path))
extra_targets = ctx.uquery().kind("^(rust_binary|rust_library|rust_test)$", extra_targets)
# Exclude targets with the rustc_do_no_check label from the extra targets. This
# label is used for foo@symbol targets (generated by rust_linkable_symbols), which
# are slow to build and never a direct dependencies of rust targets.
extra_targets -= ctx.uquery().attrfilter(
"labels",
"rustc_do_not_check",
extra_targets,
)
# explicitly included targets aren't "extra"
extra_targets -= targets_by_buildfile[buildfile_path]
extra_targets_by_buildfile[buildfile_path] = extra_targets
# add as many extra targets as we can according to max_extra_targets.
# note that which extra targets we add is arbitrary since it depends on the
# iteration order of the dict and the target_set.
remaining_extra_targets = ctx.cli_args.max_extra_targets
for buildfile_path, extra_targets in extra_targets_by_buildfile.items():
extra_targets = utarget_set(list(extra_targets)[:remaining_extra_targets])
targets_by_buildfile[buildfile_path] += extra_targets
remaining_extra_targets -= len(extra_targets)
if remaining_extra_targets <= 0:
break
# output just the target labels by buildfile
out = {}
for buildfile_path, targets in targets_by_buildfile.items():
out[buildfile_path] = [target.label for target in targets]
ctx.output.print_json(out)
# Writes a json file as an artifact and returns the absolute path to that artifact to stdout.
resolve_targets = bxl_main(
impl = resolve_targets_impl,
cli_args = {
"exclude_workspaces": cli_args.bool(default = False),
"pretty": cli_args.bool(default = False),
"targets": cli_args.list(cli_args.target_expr()),
},
)
resolve_owning_buildfile = bxl_main(
impl = resolve_owning_buildfile_impl,
cli_args = {
# while buildfiles, files, and targets can all be passed, only files will be used.
# this file is driven primarily by rust-project's needs and is a private implementation
# detail.
"buildfiles": cli_args.option(cli_args.list(cli_args.string())),
"files": cli_args.option(cli_args.list(cli_args.string())),
"max_extra_targets": cli_args.int(),
"targets": cli_args.option(cli_args.list(cli_args.target_expr())),
},
)