Bazel: Add custom rules

Dilip Kumar
5 min readJan 9, 2025

--

What are rules in Bazel?

Writing new rules in Bazel involves defining custom build rules using Starlark, Bazel’s extension language.

Example#1 Helloworld

This example demonstrates how to create a custom rule that generates a file with a specific content.

Step 1: Create the BUILD file

load("//:custom_rules.bzl", "generate_file")

generate_file(
name = "example_file",
content = "Hello, Bazel!",
output = "output.txt",
)

Step 2: Define the custom rule in custom_rules.bzl

def _generate_file_impl(ctx):
output = ctx.actions.declare_file(ctx.attr.output)
ctx.actions.write(output, ctx.attr.content)
return [DefaultInfo(files = depset([output]))]

generate_file = rule(
implementation = _generate_file_impl,
attrs = {
"content": attr.string(mandatory = True),
"output": attr.string(mandatory = True),
},
)

Step 3: Build the target

Run the following command to build the target:

$ bazel build //:example_file

This will generate a file named output.txt with the content Hello, Bazel!.

Understand the syntax

  • rule() function: This function, imported from @bazel_skylib//:bzl_library.bzl, defines the structure of your rule.
  • implementation function: This is the core of your rule. It defines the actions (commands) that Bazel should execute to build the outputs.
  • attrs (attributes): This dictionary specifies the inputs your rule accepts. You define the type of each input (e.g., label, string, int, bool, label_list).
  • outputs (optional): You can explicitly define the output files your rule produces.
  • provides (optional): This is used to declare providers that are produced by this rule.

Example#2: Custom Rule to Compile a Simple Markdown File to HTML

This example shows how to create a custom rule that compiles a Markdown file to HTML using an external tool like pandoc.

Step 1: Create the BUILD file

load("//:custom_rules.bzl", "markdown_to_html")

markdown_to_html(
name = "example_md",
src = "example.md",
out = "example.html",
)

Step 2: Define the custom rule in custom_rules.bzl

def _markdown_to_html_impl(ctx):
src = ctx.file.src
out = ctx.actions.declare_file(ctx.attr.out)
ctx.actions.run(
inputs = [src],
outputs = [out],
executable = "pandoc",
arguments = [src.path, "-o", out.path],
)
return [DefaultInfo(files = depset([out]))]

markdown_to_html = rule(
implementation = _markdown_to_html_impl,
attrs = {
"src": attr.label(allow_single_file = True, mandatory = True),
"out": attr.string(mandatory = True),
},
)

Step 3: Build the target

$ bazel build //:example_md

This will generate an HTML file named example.html from the Markdown file example.md.

Example #3: Custom Rule to create zip file

This example demonstrates how to create a custom rule that aggregates multiple files into a single archive.

Step 1: Create the BUILD file

load("//:custom_rules.bzl", "aggregate_files")

aggregate_files(
name = "example_archive",
srcs = ["file1.txt", "file2.txt"],
out = "archive.zip",
)

Step 2: Define the custom rule in custom_rules.bzl

def _aggregate_files_impl(ctx):
out = ctx.actions.declare_file(ctx.attr.out)
ctx.actions.run_shell(
inputs = ctx.files.srcs,
outputs = [out],
command = "zip {out} {srcs}".format(
out = out.path,
srcs = " ".join([f.path for f in ctx.files.srcs]),
),
)
return [DefaultInfo(files = depset([out]))]

aggregate_files = rule(
implementation = _aggregate_files_impl,
attrs = {
"srcs": attr.label_list(allow_files = True, mandatory = True),
"out": attr.string(mandatory = True),
},
)

Step 3: Build the target

$ bazel build //:example_archive

This will generate a ZIP archive named archive.zip containing file1.txt and file2.txt.

aspect.bzl

In Bazel, Aspects are a powerful feature that allows you to traverse the dependency graph of targets and collect or analyze information across the entire build. The aspect.bzl file is used to define custom aspects, which can be applied to existing rules to extend their behavior without modifying the rules themselves.

Example 1 : Using aspect.bzl to Write a Custom Aspect

Let’s create a custom aspect that collects all transitive source files from a target and its dependencies.

Step 1: Define the Aspect in aspect.bzl

def _source_files_aspect_impl(target, ctx):
# Collect all source files from the target
source_files = []
if hasattr(ctx.rule.attr, "srcs"):
for src in ctx.rule.attr.srcs:
source_files.extend(src.files.to_list())

# Collect source files from dependencies
for dep in ctx.rule.attr.deps:
source_files.extend(dep[OutputGroupInfo].source_files.to_list())

# Return the collected source files in an OutputGroupInfo provider
return [OutputGroupInfo(source_files = depset(source_files))]

source_files_aspect = aspect(
implementation = _source_files_aspect_impl,
attr_aspects = ["deps"], # Propagate the aspect to dependencies
)

Step 2: Use the Aspect in a BUILD File

Assume you have a BUILD file with a cc_binary target:

cc_binary(
name = "my_binary",
srcs = ["main.cc"],
deps = [":my_library"],
)

cc_library(
name = "my_library",
srcs = ["lib.cc"],
hdrs = ["lib.h"],
)

You can apply the aspect to the my_binary target to collect all transitive source files:

$ bazel build //:my_binary --aspects=//:aspect.bzl%source_files_aspect \
--output_groups=source_files

Step 3: Inspect the Output

The aspect will collect all source files (main.cc, lib.cc, lib.h) and make them available in the source_files output group. You can inspect the output using:

bazel aquery //:my_binary --aspects=//:aspect.bzl%source_files_aspect \
--output_groups=source_files

Example 2: Custom Aspect to Validate Sources with license header

Let’s create an aspect that validates that all source files in a target and its dependencies have a license header.

Step 1: Define the Aspect in aspect.bzl

def _license_header_aspect_impl(target, ctx):
missing_license_files = []
for src in target.files.to_list():
if not src.path.endswith(".h") and not src.path.endswith(".cc"):
continue
content = ctx.actions.read(src)
if not content.startswith("// Copyright"):
missing_license_files.append(src)

if missing_license_files:
fail("Missing license header in files: " + str(missing_license_files))

return []

license_header_aspect = aspect(
implementation = _license_header_aspect_impl,
attr_aspects = ["deps"], # Propagate the aspect to dependencies
)

Step 2: Apply the Aspect in a BUILD File

$ bazel build //:my_binary --aspects=//:aspect.bzl%license_header_aspect

If any source file lacks the license header, the build will fail with an error message.

Example #3: Define aspect within BUILD file

Instead of manually passing the aspect to the bazel build command, you can define a custom rule that applies the aspect to its dependencies.

Let’s say you have an aspect that collects all transitive source files (as shown in the previous example). You can create a custom rule that applies this aspect to its dependencies and outputs the collected files.

Step 1: Define the Aspect in aspect.bzl

def _source_files_aspect_impl(target, ctx):
# Collect all source files from the target
source_files = []
if hasattr(ctx.rule.attr, "srcs"):
for src in ctx.rule.attr.srcs:
source_files.extend(src.files.to_list())

# Collect source files from dependencies
for dep in ctx.rule.attr.deps:
source_files.extend(dep[OutputGroupInfo].source_files.to_list())

# Return the collected source files in an OutputGroupInfo provider
return [OutputGroupInfo(source_files = depset(source_files))]

source_files_aspect = aspect(
implementation = _source_files_aspect_impl,
attr_aspects = ["deps"], # Propagate the aspect to dependencies
)

Step 2: Define a Custom Rule That Applies the Aspect

def _collect_source_files_impl(ctx):
# The aspect is applied to the deps attribute
source_files = depset()
for dep in ctx.attr.deps:
source_files = depset(transitive = [source_files, dep[OutputGroupInfo].source_files])

# Write the list of source files to an output file
output = ctx.actions.declare_file(ctx.attr.name + "_sources.txt")
ctx.actions.write(output, "\n".join([f.path for f in source_files.to_list()]))
return [DefaultInfo(files = depset([output]))]

collect_source_files = rule(
implementation = _collect_source_files_impl,
attrs = {
"deps": attr.label_list(aspects = [source_files_aspect]), # Apply the aspect to deps
},
)

Step 3: Use the Custom Rule in a BUILD File

Now, you can use the collect_source_files rule in your BUILD file to collect all transitive source files from a target and its dependencies:

load("//:aspect.bzl", "collect_source_files")

cc_binary(
name = "my_binary",
srcs = ["main.cc"],
deps = [":my_library"],
)

cc_library(
name = "my_library",
srcs = ["lib.cc"],
hdrs = ["lib.h"],
)

collect_source_files(
name = "collect_sources",
deps = [":my_binary"],
)

In summary

  • aspect.bzl files define aspects, which are a way to traverse and analyze the dependency graph in Bazel.
  • Aspects are used for tasks like code generation, linting, dependency analysis, and custom build actions that need to operate on a target’s dependencies.
  • They are a powerful tool for extending Bazel’s capabilities beyond what standard rules can achieve.

Happy learning :-)

--

--

Dilip Kumar
Dilip Kumar

Written by Dilip Kumar

With 18+ years of experience as a software engineer. Enjoy teaching, writing, leading team. Last 4+ years, working at Google as a backend Software Engineer.

No responses yet