Bazel: Add custom rules
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 :-)