Style guide and best practices for defining Bazel rules, macros, and functions

Overview

This page contains Fuchsia-specific style and best practices for writing rules, macros, and functions. These are always defined in .bzl files, and the guidance for those applies here as well. In addition, guidance related to targets in BUILD.bazel files applies to targets defined by rules and macros.

This page is part of the Bazel style guide and best practices.

Consider whether new macros and functions are appropriate

As in the Bazel style guide, Fuchsia prefers DAMP (Descriptive and Meaningful Phrases) BUILD files over DRY (Don't Repeat Yourself).

In particular, the Fuchsia build uses area-specific macros and functions much less frequently in Bazel than it used such templates in GN.

Use shared, static, lists where things MUST be kept in sync (e.g. common dependencies), instead of writing area-specific macros.

While area-specific templates can reduce typing at the initial creation of targets, they become a maintenance and migration issue, especially for automated tooling, as the individual targets are hidden from view by the area-specific macros. Consult with the Build Team if you think you would strongly benefit from the use of area-specific macros and functions (the bar is high, based on past experience).

Prefer using existing macros and rules where possible

The importance of this increases with the potential extent of the use of such a macro or rule.

Especially if your needs are limited to the following, consider whether they can be accomplished using existing macro(s) and/or rule(s):

  • Generating metadata

  • Enforcing attribute values

  • Minimizing, for example, the number of attributes callers must specify

In the first two cases, especially, consider whether a Bazel query, SHAC rule, test, or some other mechanism can satisfy your needs. Bazel queries can be run on dependency trees or all targets defined in a directory tree. Can you write a test that runs a query and checks the results? If your use case requires an extra attribute, consider using tags.

Reasons to use existing rules and macros include:

  • Easier to understand and less ramp-up time for developers

    • A developer familiar with Bazel can jump in and understand targets.

    • Developers in one part of the team can understand targets in other parts of the code base.

  • AI is more likely to understand and produce Bazel files that use common rules and macros

  • Fewer load() statements required

    • Built-in identifiers do not require any load statements.

    • Others may already be loaded for other targets in the file.

  • Less opportunity for bugs

    • For example, aspects only work correctly when they traverse all relevant targets. A custom macro or rule is an opportunity to introduce a path that won't be followed. See Use standard attribute names.

Prefer symbolic macros to legacy macros

Prefer writing symbolic macros, which are defined using macro(), over legacy macros (Python-like functions defined with def that create targets). Symbolic macros provide clearer documentation of attributes and their types, perform type checking on attributes, and ensure labels are evaluated at the call site. They also support attribute inheritance (via inherit_attrs). For further context, see Why you shouldn't use legacy macros.

Never specify default values for arguments in a symbolic macro's implementation function as the default value is defined by the attrs entry, and a value will be provided for every attribute.

There are some cases where using a legacy macro wrapper around a symbolic macro is necessary, but these should be very rare for most developers.

Visibility and access checks for targets defined within macros

Overview

For the purposes of visibility, think of legacy macros as if they are expanded inline wherever they are instantiated. When used in BUILD.bazel files, the targets defined by a legacy macro are effectively defined in that file. As a result:

  • The defined targets' default visibility is the same as the instantiating package, including the package default_visibility if specified.

  • The macro can use (e.g., add to deps) any target that is visible to the instantiating package.

    • The location of the .bzl file defining the legacy macro is irrelevant.

However, for the purposes of visibility, think of targets defined by symbolic macros as if they are defined in a BUILD.bazel file in the same package (directory) as the .bzl file defining the macro. As a result:

  • The defined targets' default visibility is the package (directory) containing the .bzl file.

  • The macro can use (e.g., add to deps) any target that is visible to the package (directory) containing the .bzl file.

    • This can be useful for, for example, FIDL bindings support libraries added by fidl_library().

    • But it is problematic for things such as an HLCPP support library allowlist.

    • See issue 446911800 for details.

Specifying visibility for targets defined in macros

Public targets

Forward the visibility attribute passed to the macro to the main target defined by the macro - the one to which name is passed. The visibility attribute may also be forwarded to other defined public targets mentioned in the macro's doc string as appropriate.

Private targets

In symbolic macros, the visibility of all other targets defined will default to ["//visibility:private"]. For legacy macros, however, you must Specify visibility for all targets defined by legacy macros.

Specify visibility for all targets defined by legacy macros

Specify visibility = ["//visibility:private"] for all targets that do not use the visibility attribute passed to the macro. This is necessary to prevent them from defaulting to the package's default_visibility if specified.

Use standard attribute names

Use standard attribute names in macros and rules. For example, use "deps", "data", or even "tools" rather than "images" or "scripts". See some generally applicable attributes.

Reasons for this include:

  • For readability and other reasons similar to those in Prefer using existing macros and rules where possible.

  • Aspects only work correctly when they traverse all relevant targets, and macros will generally be configured to be applied to "deps", and other common attributes as appropriate. However, they are unlikely to be aware of, for example, "rust_deps", and failing to be applied to that attribute could exclude targets relevant to the aspect.

Require named arguments

Public functions and legacy macros (a special category of function) should generally declare keyword-only arguments (all arguments are declared after *,). Exceptions may be made for functions with at most a few arguments where the arguments are clear from the symbol name, and the arguments will not be mistakenly used in the wrong position (including due to refactoring/reordering). Boolean arguments and arguments with default values should always appear after the *.

While this is most important for symbols meant to be used by other parts of the codebase, it also applies to public symbols in all .bzl files.

This helps enforce the Bazel .bzl Style Guide's guidance that "When calling a macro, use only keyword arguments. This is consistent with rules, and greatly improves readability."

Declare all arguments used by a macro

If a macro (optionally) uses an argument, explicitly declare that argument in the macro implementation's parameters list rather than extracting it from kwargs. For example, avoid the following:

# Do NOT do this:
testonly = kwargs.get("testonly", False),

Declaring the arguments makes it clearer which arguments are relevant to the macro implementation (vs., for example, macros it calls) and provides a single place to see the default value. For legacy macros, it also ensures that the arguments are documented. (This is mostly relevant for the Fuchsia Bazel SDK.)

As with any other argument, but especially Attributes common to all build rules and Attributes common to all test rules (_test), you must be sure to pass the argument to all macros and rules that support it since these will not be in **kwargs.

Comments

  • Provide function-level comments for all public functions (those that do not begin with an underscore).

  • Provide doc strings for all rules and macros.

  • Provide doc strings for all rule and macro attributes.

    • doc strings may be omitted for private attributes (those that begin with an underscore) where the meaning is obvious from the default value.

doc strings

  • In the Fuchsia platform, doc strings are meant to be read in the source file rather than in some generated documentation. Thus, prefer optimizing for that rather than how some generated documentation might look.

  • Long doc strings:

    • Prefer writing a top-level single sentence description entirely on the same line as doc= where reasonable.

    • There is no strict line length limit in Bazel.

    • When multiple lines are necessary, write multiline strings using triple quotes.

    • Avoid appending regular strings.

  • Multiline doc strings

    • Begin multiline strings on the same line as the doc argument (doc = """Begin the comment...).

    • Start subsequent lines under the d in doc, similar to how Python comments start the next line under the first ".

    • This optimizes for consistency and readability while accepting that it would not be ideal for generated text.

    • End multiline strings with triple quotes (""") on a separate line aligned with doc.

Rule and macro implementation function names

Name implementation functions for rule() and macro() instances using a leading underscore, followed by the name of the rule or function, and ending with _impl.

Use variables when referencing target names within macros

If a macro defines a target then depends on it in another target, define a variable with the former target's name and use that for its name attribute and in the deps of the latter target.

This unambiguously links the two targets, especially in complex cases where there are multiple levels of target names based on other target names, and is easier to highlight and search for.

Do not use variables for targets not used internally unless it enhances readability, such as when all target names are defined in one place.

Wrapping built-in and common rules, macros, and functions

When writing a macro to be used in place of a common Bazel rule, macro, or function within the Fuchsia platform codebase, prefix the wrapped name with fx_. For example, fx_cc_library() is to be used instead of cc_library(). "Common" includes symbols built into Bazel (including native.*) as well as those in common repositories such as rules_cc. Also use this prefix for other conflicts, such as fx_package(), which is unrelated to package. Do not use a fuchsia_ prefix as fuchsia_... files and symbols are in the Fuchsia Bazel SDK.

Avoid caching or remoting large artifacts

See Avoiding caching for large artifacts.