Skip to main content

Basic How-Tos

Writing a BXL

To create a BXL, first, create a script somewhere in the repository ending in .bxl. (Note that you can define a single bxl per file, or multiple BXLs per file like in Starlark rules).

In it, define a BXL function as follows:

def _your_implementation(ctx):
# ...
pass

your_function_name = bxl_main(
impl = _your_implementation,
cli_args = {
# cli args that you want to receive from the command line
"bool_arg": cli_args.bool(),
# cli_args will be converted to snakecase. e.g. for this case, passed as --list-type, accessed via ctx.cli_args.list_type
"list-type": cli_args.list(cli_args.int()),
"optional": cli_args.option(cli_args.string()),
"target": cli_args.target_label(),
},
)

This exposes your_function_name as a function, with whatever arguments you defined it, so that on the command line you can invoke:

buck2 bxl //myscript.bxl:your_function_name -- --bool_arg true --list-type 1 --list-type 2 --target //foo:bar`

The implementation function takes a single context as parameter (see the documentation for bxl.Context). Using it, you'll be able to access functions that enable you to perform queries, analysis, builds, and even create your own actions within BXL to build artifacts as part of a BXL function.

Running a BXL

To run a BXL function, invoke the buck2 command:

buck2 bxl <bxl function> -- <function args>

Where <bxl function> is of the form <cell path to function>:<function name>, and <function args> are the arguments that the function accepts from the command line.

The documentation for a BXL function can be seen by running:

 buck2 bxl <bxl function> -- --help

Note that this is different from buck2 bxl --help, which generates the help for the buck2 command instead of the function.

Return information from BXL

The primary method to return information from BXL is to either print them, or build some artifact (for details, see the bxl.OutputStream documentation, available as part of ctx.output). At high level, ctx.output.print(..) prints results to stdout, and ctx.output.ensure(artifact) marks artifacts as to be materialized into buck-out by the end of the BXL function, returning an object that lets you print the output path via ctx.output.print(ensured).

Passing in and using CLI args

A BXL function can accept a cli_args attribute where args names and types are specified to use within your script, as shown in the following example:

Example:

def _impl_example(ctx):
# ...
pass

example = bxl_main(
impl = _impl_example,
cli_args = {
# cli args that you want to receive from the command line
"bool_arg": cli_args.bool(),
"list_type": cli_args.list(cli_args.int()),
"optional": cli_args.option(cli_args.string()),
"target": cli_args.target_label(),
},
)

On the command line, you can invoke the arguments as follows:

buck2 bxl //myscript.bxl:example -- --bool_arg true --list_type 1 --list_type 2 --target //foo:bar

For BXL functions, to read the arguments, use them as attributes from the cli_args attribute on the BXL ctx object, as follows:

def _impl_example(ctx):
my_bool_arg = ctx.cli_args.bool_arg

Running actions

You can create actions within BXL via the actions_factory. This is called once globally then used on demand:

def _impl_example(ctx):
actions = ctx.bxl_actions().actions # call once, reuse wherever needed
output = actions.write("my_output", "out")

You will need to have execution platforms enabled for your project, or else you will get an error. You can specify the execution platform resolution by setting named parameters when instantiating bxl_actions:

  • exec_deps - These are dependencies you wish to access as executables for creating the action. This is usually the same set of targets one would pass to rule's attr.exec_dep. Accepts a list of strings, subtarget labels, target labels, or target nodes.
  • toolchains - The set of toolchains needed for the actions you intend to create. Accepts a list of strings, subtarget labels, target labels, or target nodes.
  • target_platform - The intended target platform for your toolchains. Accepts a string or target label.
  • exec_compatible_with - Explicit list of configuration nodes (like platforms or constraints) that these actions are compatible with. This is the exec_compatible_with attribute of a target. Accepts a list of strings, target labels, or target nodes.

If you specify exec_deps or toolchains, you can access the resolved dependency objects on the bxl_actions object. The bxl_actions object will have exec_deps and toolchains attributes, which are dicts where the keys are the unconfigured subtarget labels, and the values are the configured/resolved dependency objects.

Note that the keys of exec_deps and toolchains must be unconfigured subtarget labels (StarlarkProvidersLabel), and not unconfigured target labels. You can use ctx.unconfigured_sub_targets(...) or with_sub_target() on target_label to create the label.

def _impl_example(ctx):
my_exec_dep = ctx.unconfigured_sub_targets("foo//bar:baz") # has some provider that you would use in the action
bxl_actions = ctx.bxl_actions(exec_deps = [my_exec_dep]) # call once, reuse wherever needed
output = bxl_actions.actions.run(
[
"python3",
bxl_actions.exec_deps[my_exec_dep][RunInfo], # access resolved exec_deps on the `bxl_actions`
out.as_output(),
],
category = "command",
local_only = True,
)
ctx.output.ensure(output)

Getting providers from an analysis

After calling analysis(), you can get the providers collection from providers():

def _impl_example(ctx):
my_providers = ctx.analysis(my_target).providers()

Get a specific provider from an analysis

After calling analysis(), you can also get the providers collection from providers() then grab whatever specific provider you need:

def _impl_example(ctx):
default_info = ctx.analysis(my_target).providers()[DefaultInfo]
ctx.output.print(default_info)

Get a specific subtarget from an analysis

Once you have a provider, you can get its subtargets by using the sub_targets attribute on the struct to get a dict of provider labels to provider collections:

def _impl_example(ctx):
subtarget = ctx.analysis(my_target).providers()[DefaultInfo].sub_targets["my_subtarget"]
ctx.output.print(subtarget)

Building a target/subtarget without blocking

ctx.build is synchronous and should only be used when the result of the build is needed inline during the bxl execution. To execute builds without blocking the script, retrieve the DefaultInfo from the target's providers and use the ctx.output.ensure_multiple api.

Example:

ctx.output.ensure_multiple(ctx.analysis(label).providers()[DefaultInfo])

Accessing Unconfigured/Configured Target Node Attributes

BXL provides a unified API for accessing attributes on both unconfigured and configured target nodes.

For special attributes like rule_kind, we get them directly from node:

node.rule_kind

Deprecated apis

The following attribute access api are not recommended and will be deprecated

For ConfiguredTargetNode:

For UnconfiguredTargetNode:

Example

def _impl_example(ctx):
my_configured_node = ctx.configured_targets(":foo")

# get an attribute named "foo", if not exist return None
foo_attr = my_configured_node.get_attr("foo")

# get all attributes, it returns a dict mapping from attribute name to attribute
all_attrs = my_configured_node.get_attrs()

# check if "foo" attribute exists on node
foo_exist = my_configured_node.has_attr("foo")

# access special attribute `rule_type`
rule_type = my_configured_node.rule_type

# same for UnconfiguredTargetNode

Inspecting a struct

You can use dir(my_struct) to inspect a struct. You can also use getattr(my_struct, "my_attr") to grab individual attributes, which is equivalent to my_struct.my_attr.

These are available as part of the Starlark language spec.

Set addition/subtraction on a target_set

There are a few BXL actions that return a target_set (such as a cquery eval()). The target_set supports set subtraction and addition (you can use - and + directly in Starlark).

Initializing configured/unconfigured target_set

You can use following apis to initialize target_set

def bxl.utarget_set(nodes: None | list[bxl.UnconfiguredTargetNode]) -> bxl.UnconfiguredTargetSet
def bxl.ctarget_set(nodes: None | list[bxl.ConfiguredTargetNode]) -> bxl.ConfiguredTargetSet

Profiling, Testing, and Debugging a BXL script

You can use buck2 bxl profiler, with various measurements, to determine where the script is least efficient.

To time individual pieces of the script, you can use BXL’s timestamp methods:

def _impl_example(_ctx):
start = now() # call once and reuse wherever is necessary
# do something time intensive here
end1 = start.elapsed_millis()
# do something else time intensive here
end2 = start.elapsed_millis()
  • Debug - the common way to debug a BXL script is with print statements (print(), pprint() and ctx.output.print()).
  • Test - BXL does not have a robust testing framework for mocking. The main method to test a BXL script is to actually invoke it with required inputs then verify the outputs.

Getting the path of an artifact as a string

The starlark artifact type encapsulates source artifacts, declared artifacts, and build artifacts. It can be dangerous to access paths and use them in further BXL computations. For example, if you are trying to use absolute paths for something and end up passing it into a remotely executed action, the absolute path may not exist on the remote machine. Or, if you are working with paths and expecting the artifact to already have been materialized in further BXL computations, that would also result in errors.

However, if you are not making any assumptions about the existence of these artifacts, you can use use get_path_without_materialization(), which accepts source, declared, or build aritfacts. It does not accept ensured artifacts (also see What do I need to know about ensured artifacts).

For getting paths of cmd_args() inputs, you can use get_paths_without_materialization(), but note this is risky because the inputs could contain tsets, which, when expanded, could be very large. Use these methods at your own risk.