Skip to main content
RBS is fully extensible. You can define custom build rules for any language, tool, or workflow using native.define_rule. Custom rules are written in the RBS DSL and can be shared across projects.

Defining a rule

Use native.define_rule to create a new rule type:
def my_binary_impl(ctx):
    """Build a custom binary."""
    dirs = ctx.bin.create_dirs()
    ctx.runfiles(files = ctx.srcs)

    native.create_launcher(
        output_path = ctx.bin.executable,
        executable = "python",
        args = [ctx.attr.main],
        workdir = "$SCRIPT_DIR/runfiles/_main",
    )

native.define_rule(
    name = "my_binary",
    kind = "binary",
    implementation = my_binary_impl,
    attrs = {
        "srcs": attr.list(of = attr.string),
        "main": attr.string(mandatory = True),
        "deps": attr.list(of = attr.string, default = []),
    },
)

# Now use it like any built-in rule
my_binary(
    name = "app",
    srcs = ["main.py", "utils.py"],
    main = "main.py",
)

Convenience wrappers

# For binary rules
native.define_binary_rule(name = "my_binary", implementation = impl, attrs = {...})

# For library rules
native.define_library_rule(name = "my_library", implementation = impl, attrs = {...})

Parameters

ParameterTypeRequiredDescription
namestringYesRule name — exported as a function in BUILD files
kindstringNoClassifier (e.g., "binary", "library", "test")
implementationfunctionNoThe build logic function receiving ctx
toolchainstringNoToolchain type this rule uses
attrsdictNoAttribute schema for the rule
fragmentslistNoRequired configuration fragments

Attribute types

Define the schema for rule attributes using the attr module:
FunctionDescription
attr.string()String attribute
attr.string_list()List of strings
attr.string_dict()Dictionary of strings
attr.bool()Boolean attribute
attr.int()Integer attribute
attr.label()Reference to another target
attr.label_list()List of target references
attr.dict()Generic dictionary
attr.list()Generic list

Common parameters

All attribute types support:
ParameterTypeDefaultDescription
mandatoryboolFalseWhether the attribute is required
defaultanyNoneDefault value if not specified
docstring""Documentation string

The is_dep parameter

For attr.label() and attr.label_list(), use is_dep = True to mark the attribute as a build dependency. This ensures the referenced target is built before the current rule.
native.define_rule(
    name = "oci_image",
    implementation = _oci_image_impl,
    attrs = {
        "binary": attr.label(is_dep = True, doc = "Binary to package"),
        "files": attr.label_list(is_dep = True, doc = "Additional files"),
        "config_template": attr.label(doc = "Optional config (not a dep)"),
    },
)

The context object (ctx)

The implementation function receives a ctx object with access to build inputs, outputs, and actions.

Basic information

def my_rule_impl(ctx):
    ctx.name         # Target name
    ctx.package      # Package path
    ctx.srcs         # List of source files
    ctx.deps         # List of dependencies
    ctx.attr.main    # Access custom attributes

ctx.actions — Execute commands

def my_rule_impl(ctx):
    # Run an executable
    ctx.actions.run(
        executable = "python",
        arguments = ["compile.py", "input.txt"],
        inputs = ["compile.py", "input.txt"],
        outputs = ["output.txt"],
        mnemonic = "Compile",
        progress_message = "Compiling...",
    )

    # Run a shell command
    ctx.actions.run_shell(
        command = "cat input.txt | sort > sorted.txt",
        outputs = ["sorted.txt"],
        description = "Sort input",
    )

    # Write a file
    ctx.actions.write(
        output = "config.json",
        content = '{"version": "1.0"}',
        is_executable = False,
    )

    # Expand a template
    ctx.actions.expand_template(
        template = "template.conf",
        output = "app.conf",
        substitutions = {"APP_NAME": ctx.name, "VERSION": "1.0"},
    )
Provides per-target, package-isolated output paths for hermetic builds:
def my_rule_impl(ctx):
    dirs = ctx.bin.create_dirs()

    dirs.output       # .rbs/bin/{platform}/{package}/{name}
    dirs.run_files    # .rbs/bin/{platform}/{package}/{name}/runfiles
    dirs.toolchains   # .rbs/bin/{platform}/{package}/{name}/toolchains
    dirs.data         # .rbs/bin/{platform}/{package}/{name}/data

    ctx.bin.executable  # Path to the main executable
    ctx.bin.platform    # e.g., "darwin-arm64"
    ctx.bin.package     # e.g., "services/api"

ctx.bin.local_dep — Resolve local dependency paths

lib_path = ctx.bin.local_dep(":my_lib")                # Same package
utils_path = ctx.bin.local_dep("//libs/common:utils")  # Different package

ctx.bin.external_dep — Resolve external dependency paths

requests_path = ctx.bin.external_dep("requests", "python")
express_path = ctx.bin.external_dep("express", "nodejs")

ctx.runfiles — Manage runtime files

def my_rule_impl(ctx):
    main_dir = ctx.runfiles(files = ctx.srcs)
    # Creates runfiles/_main directory with source files

ctx.tools — Copy toolchains

tools_dir = ctx.tools.copy(
    toolchain = "python3",
    destination = ctx.outputs.dir + "/tools",
)

ctx.external_deps — Manage external packages

deps_dir = ctx.external_deps.copy(
    dependencies = ["@external://requests", "@external://flask"],
    destination = ctx.outputs.dir + "/dependencies",
    language = "python",
)

if ctx.external_deps.exists(name = "requests", ecosystem = "python"):
    dep_info = ctx.external_deps.get(name = "requests", ecosystem = "python")
    print("Cache dir:", dep_info.cache_dir)

ctx.file — File operations

content = ctx.file.read("input.txt")
ctx.file.write("output.txt", "Hello World")
if ctx.file.exists("optional.txt"):
    ctx.file.copy("optional.txt", "output/optional.txt")
ctx.file.copy_tree("src/", "dest/")

ctx.dir — Directory operations

ctx.dir.create("output/subdir")
files = ctx.dir.list("input_dir")
if ctx.dir.exists("optional_dir"):
    pass

ctx.json — JSON operations

data = ctx.json.parse('{"key": "value"}')
json_str = ctx.json.stringify({"result": "success"})

ctx.http — HTTP operations

content = ctx.http.get("https://api.example.com/data")
ctx.http.download("https://example.com/file.txt", "downloaded.txt")

ctx.archive — Archive operations

ctx.archive.extract("archive.zip", "extracted/")

Launcher scripts — native.create_launcher()

Generate cross-platform launcher scripts for binaries and tests. This is the recommended way to create executable wrappers.

Basic usage

native.create_launcher(
    output_path = ctx.bin.executable,
    executable = "python",
    args = ["main.py"],
    toolchain_paths = ["$SCRIPT_DIR/toolchains/python*/bin"],
    workdir = "$SCRIPT_DIR/runfiles/_main",
)

Parameters

ParameterTypeRequiredDescription
output_pathstringYesPath to write the launcher script
executablestringNoCommand to execute
argslistNoArguments for the executable
envdictNoEnvironment variables
path_env_varsdictNoPath-based env vars with separators
toolchain_pathslistNoGlob patterns for toolchain bin dirs
workdirstringNoWorking directory
pre_commandslistNoShell commands to run before exec
conditionstringNoShell condition for conditional execution
fallback_cmdstringNoCommand if condition fails

Path-based environment variables

Different languages use different path variables. The path_env_vars parameter handles this:
path_env_vars = {
    "PYTHONPATH": {
        "paths": ["$SCRIPT_DIR/runfiles/_main", "$SCRIPT_DIR/runfiles"],
        "separator": ":",
        "append": True,
    },
}

Conditional execution

native.create_launcher(
    output_path = ctx.bin.executable,
    executable = "python",
    args = ["-m", "pytest", "test_main.py", "-v"],
    condition = 'python -c "import pytest" 2>/dev/null',
    fallback_cmd = "python -m unittest test_main",
)

Complete example — Python binary rule

def _py_binary_impl(ctx):
    dirs = ctx.bin.create_dirs()

    # Copy toolchain into target's output
    ctx.file.copy_tree(
        ctx.output_path.toolchains + "/python3.11",
        dirs.toolchains + "/python3.11",
    )

    # Copy source files
    ctx.runfiles(files = ctx.srcs)

    # Copy external dependencies
    for dep in ctx.attr.deps:
        dep_path = ctx.bin.external_dep(dep, "python")
        if ctx.dir.exists(dep_path):
            ctx.file.copy_tree(dep_path, dirs.run_files + "/" + dep)

    # Create launcher
    native.create_launcher(
        output_path = ctx.bin.executable,
        executable = "python",
        args = [ctx.attr.main],
        path_env_vars = {
            "PYTHONPATH": {
                "paths": ["$SCRIPT_DIR/runfiles/_main", "$SCRIPT_DIR/runfiles"],
                "separator": ":",
                "append": True,
            },
        },
        toolchain_paths = ["$SCRIPT_DIR/toolchains/python*/bin"],
        workdir = "$SCRIPT_DIR/runfiles/_main",
    )

native.define_binary_rule(
    name = "py_binary",
    implementation = _py_binary_impl,
    attrs = {
        "srcs": attr.list(of = attr.string),
        "main": attr.string(mandatory = True),
        "deps": attr.list(of = attr.string, default = []),
    },
)

External dependency resolvers

Define custom resolvers to manage how packages are downloaded and cached:
def python_pip_resolver(package, version, cache_dir, context):
    metadata_url = "https://pypi.org/pypi/{}/{}/json".format(package, version)
    metadata = context.json.parse(context.http.get(metadata_url))

    for file_info in metadata["urls"]:
        if file_info["filename"].endswith(".whl"):
            wheel_path = cache_dir + "/package.whl"
            context.http.download(file_info["url"], wheel_path)
            context.archive.extract(wheel_path, cache_dir + "/site-packages")
            return {"success": True}
    return {"success": False}

native.define_external_dep_resolver(
    name = "python_pip",
    ecosystem = "python",
    implementation = python_pip_resolver,
    library_dirs = ["site-packages", "lib"],
    file_extensions = [".whl", ".tar.gz"],
    cache_structure = "python/{package}/{version}",
)

Next steps