> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing Macros

> Learn how to write Bazel macros to compose existing rules, share BUILD file boilerplate, and provide ergonomic syntax sugar without breaking compatibility.

Goal: by the end of this section, you'll know how to make more readable `BUILD` files by using a simple code-sharing technique provided by Bazel.

## Macros

Bazel Macros are like pre-processor definitions, which compose existing rules in a novel way and
provide "syntax sugar" to developers who call them from `BUILD` files.

<Note>
  Macro/Rule Isomorphism:

  At a `BUILD` file usage site, you cannot distinguish macro from rule. This is to allow a rule to be wrapped with a macro without a breaking change.
</Note>

Thanks to this design, we can start by imagining the right way for a user to express their "bare facts" in the `BUILD` file, then write Starlark code that supports it.
We can start with a macro as they are much easier, but we can always introduce a custom rule when the requirements make it necessary.

## Symbolic Macros

Symbolic macros were introduced in Bazel 8, and use a dedicated new `macro` constructor call in a `.bzl` file with a separate implementation function:

```python theme={null}
def _my_macro_impl(name, visibility, srcs, **kwargs):
  some_rule(name = name, visibility = visibility, srcs = srcs, **kwargs)

my_macro = macro(implementation = _my_macro_impl, attrs = {
  "srcs": ctx.attr.label_list(),
})
```

Symbolic macros have some advantages:

* providing types for their attributes makes better errors when misused
* targets they create are private by default, so they don’t accidentally expose their internals
* they enforce targets are created using the naming schema, to avoid collisions when the macro is called more than once in a package

<Callout icon="book">
  Read more: [https://bazel.build/versions/8.0.0/rules/macro-tutorial](https://bazel.build/versions/8.0.0/rules/macro-tutorial)
</Callout>

## Legacy Macros

Starting in Bazel 8, the construct which had been referred to as a “Macro” is now a “Legacy Macro”. However it’s still useful to learn it because it can do things a Symbolic macro cannot, and it’s simpler.

A legacy macro is just a function definition in a `.bzl` file which composes some existing rules.

```python theme={null}
def my_legacy_macro(name, srcs, **kwargs):
    some_rule(
        name = name,
        srcs = srcs,
        **kwargs
    )

```

The `run_binary` rule introduced earlier is a great candidate for the `some_rule` here.

<Callout icon="book">
  Read more: [genrule bestrule](/blog/genrule-bestrule)
</Callout>

Legacy macros can:

* Allow polymorphic attribute types, and vary their behavior based on the type. For example `rules_oci` uses this to permit an attribute that’s either a label of a file, or a string value that should be written into a file.
* Use `native.glob` to infer a default value for missing attributes based on what files exist.

However, legacy macros are untyped. To provide a good user experience when developers make a mistake, it’s necessary to check the types manually.

<Warning>
  Leaky Abstraction
  If you `bazel print` (Aspect CLI only) which is a syntactic operation on the BUILD file, you see the macro as it was called.
  However, macros are expanded during the loading phase, so if you run a `bazel query` you'll see the result of the macro evaluation.

  If the macro is named differently from the underlying rule, this can be confusing for users and also affect usability, for example `--test_lang_filters` applies to the underlying rule's name.
</Warning>

### Example 1

This example just wraps a single `run_binary` rule, in this case it's a third-party tool called "mocha"
which was fetched from npm.

[Usage](https://github.com/aspect-build/rules_js/blob/933c1e6b7605289cdf0c2d72fd1d3ac77ba4a50b/examples/macro/BUILD.bazel):

```python theme={null}
load("//examples/macro:mocha.bzl", "mocha_test")

mocha_test(
    name = "test",
    srcs = ["test.js"],
)

```

[Definition](https://github.com/aspect-build/rules_js/blob/v1.6.8/examples/macro/mocha.bzl):

```python theme={null}
"Example macro wrapping the mocha CLI"

load("@npm//examples/macro:mocha/package_json.bzl", "bin")

def mocha_test(name, srcs, args = [], data = [], env = {}, **kwargs):
    bin.mocha_test(
        name = name,
        args = [
            "--reporter",
            "mocha-multi-reporters",
            "--reporter-options",
            "configFile=$(location //examples/macro:mocha_reporters.json)",
            native.package_name() + "/*test.js",
        ] + args,
        data = data + srcs + [
            "//examples/macro:mocha_reporters.json",
            "//examples/macro:node_modules/mocha-multi-reporters",
            "//examples/macro:node_modules/mocha-junit-reporter",
        ],
        env = dict(env, **{
            # Add environment variable so that mocha writes its test xml
            # to the location Bazel expects.
            "MOCHA_FILE": "$$XML_OUTPUT_FILE",
        }),
        **kwargs
    )

```

### Example 2

This example composes a few building blocks from bazel\_skylib and aspect\_bazel\_lib.

[Usage](https://github.com/aspect-build/rules_ts/blob/3212673615ba508a8061cc54dc908b070d5c1533/examples/root_dir/BUILD.bazel#L19):

```python theme={null}
ts_project(
    name = "strip",
    tsconfig = {
        # Demonstrating that rootDir compilerOption works the same as the
        # root_dir attribute.
        "compilerOptions": {
            "rootDir": "subdir",
        },
    },
)

assert_outputs(
    name = "strip_test",
    actual = "strip",
    expected = [
        "examples/root_dir/a.js",
        "examples/root_dir/deep/subdir/b.js",
    ],
)

```

[Definition](https://github.com/bazel-contrib/bazel-lib/blob/main/lib/testing.bzl):

```python theme={null}
"helpers for test assertions"

load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@bazel_skylib//lib:types.bzl", "types")
load("@aspect_bazel_lib//lib:params_file.bzl", "params_file")

def assert_outputs(name, actual, expected):
    """Assert that the default outputs of actual are the expected ones

    Args:
        name: name of the resulting diff_test
        actual: string of the label to check the outputs
        expected: expected outputs
    """

    if not types.is_list(expected):
        fail("expected should be a list of strings")
    params_file(
        name = "_actual_" + name,
        data = [actual],
        args = ["$(rootpaths {})".format(actual)],
        out = "_{}_outputs.txt".format(name),
    )
    write_file(
        name = "_expected_ " + name,
        content = expected,
        out = "_expected_{}.txt".format(name),
    )
    diff_test(
        name = name,
        file1 = "_expected_ " + name,
        file2 = "_actual_" + name,
    )

```

### Example 3

This example creates a macro wrapping a repository rule rather than a build rule.
(Actually, it uses `alias` which is even shorter than a macro, it passes all attributes through.)

It uses `select` to get a binary for the host platform, bypassing the need for toolchains which are
a tricky part of Custom rules.

<Danger>
  If you run an un-configured build (for example with `bazel query`) then select will eagerly load every label on the right-hand-side. This causes an eager fetch of tools which don't run on the host platform and wastes the developers time.
  This is a good reason to get in the habit of always using `bazel cquery` instead, so that the build is configured.
</Danger>

Usage:

```python theme={null}
http_archive(
    name = "terraform_macos_aarch64",
    build_file_content = "exports_files([\\"terraform\\"])",
    sha256 = "ff92cd79b01d39a890314c2df91355c0b6d6815fbc069ccaee9da5d8b9ff8580",
    urls = ["<https://releases.hashicorp.com/terraform/{0}/terraform_{0}_darwin_arm64.zip>".format(version)],
)

```

```python theme={null}
alias(
    name = "terraform_binary",
    actual = select({
        "//platforms/config:linux_x86_64": "@terraform_linux_x86_64//:terraform",
        "//platforms/config:macos_aarch64": "@terraform_macos_aarch64//:terraform",
        "//platforms/config:macos_x86_64": "@terraform_macos_x86_64//:terraform",
    }),
)

```

### When a Macro isn't enough

Rules create actions, which transform inputs to outputs.

Using `ts_project` as an example, this couldn't be a macro for several reasons:

1. It creates a tree of actions, which might use one tool to transpile `.js` outputs, and a
   different tool for producing TypeScript types (`.d.ts` files).
2. It requires that `srcs` have a `JsInfo` provider so that it can understand their structure.
3. It produces a `JsInfo` provider for inter-op with downstream rules that depend on it.

<Note>
  Even when Providers get in your way of "just using a macro", you can often write a tiny adapter rule and then put most of your logic in a more easily understood macro.
  For example, [this code](https://github.com/mrmeku/codelabs/blob/fbf0eef162882a297869d49855b3f7cb60ac85cf/proto/ts_proto_library.bzl) adapts a `ProtoInfo` on its sources to a `DefaultInfo` output.
</Note>

## Try it: write a macro

Add any macro in your repository, even a trivial one.

Then change one of your `BUILD` files to call the macro.
