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

# Custom Rules

> Define your own build rules using native.define_rule, the context API, attribute types, and launcher scripts

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:

```python theme={null}
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

```python theme={null}
# 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

| Parameter        | Type     | Required | Description                                          |
| ---------------- | -------- | -------- | ---------------------------------------------------- |
| `name`           | string   | Yes      | Rule name — exported as a function in BUILD files    |
| `kind`           | string   | No       | Classifier (e.g., `"binary"`, `"library"`, `"test"`) |
| `implementation` | function | No       | The build logic function receiving `ctx`             |
| `toolchain`      | string   | No       | Toolchain type this rule uses                        |
| `attrs`          | dict     | No       | Attribute schema for the rule                        |
| `fragments`      | list     | No       | Required configuration fragments                     |

## Attribute types

Define the schema for rule attributes using the `attr` module:

| Function             | Description                 |
| -------------------- | --------------------------- |
| `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:

| Parameter   | Type   | Default | Description                       |
| ----------- | ------ | ------- | --------------------------------- |
| `mandatory` | bool   | `False` | Whether the attribute is required |
| `default`   | any    | `None`  | Default value if not specified    |
| `doc`       | string | `""`    | 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.

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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"},
    )
```

### `ctx.bin` — Hermetic output paths (recommended)

Provides per-target, package-isolated output paths for hermetic builds:

```python theme={null}
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

```python theme={null}
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

```python theme={null}
requests_path = ctx.bin.external_dep("requests", "python")
express_path = ctx.bin.external_dep("express", "nodejs")
```

### `ctx.runfiles` — Manage runtime files

```python theme={null}
def my_rule_impl(ctx):
    main_dir = ctx.runfiles(files = ctx.srcs)
    # Creates runfiles/_main directory with source files
```

### `ctx.tools` — Copy toolchains

```python theme={null}
tools_dir = ctx.tools.copy(
    toolchain = "python3",
    destination = ctx.outputs.dir + "/tools",
)
```

### `ctx.external_deps` — Manage external packages

```python theme={null}
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

```python theme={null}
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

```python theme={null}
ctx.dir.create("output/subdir")
files = ctx.dir.list("input_dir")
if ctx.dir.exists("optional_dir"):
    pass
```

### `ctx.json` — JSON operations

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

### `ctx.http` — HTTP operations

```python theme={null}
content = ctx.http.get("https://api.example.com/data")
ctx.http.download("https://example.com/file.txt", "downloaded.txt")
```

### `ctx.archive` — Archive operations

```python theme={null}
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

```python theme={null}
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

| Parameter         | Type   | Required | Description                               |
| ----------------- | ------ | -------- | ----------------------------------------- |
| `output_path`     | string | Yes      | Path to write the launcher script         |
| `executable`      | string | No       | Command to execute                        |
| `args`            | list   | No       | Arguments for the executable              |
| `env`             | dict   | No       | Environment variables                     |
| `path_env_vars`   | dict   | No       | Path-based env vars with separators       |
| `toolchain_paths` | list   | No       | Glob patterns for toolchain bin dirs      |
| `workdir`         | string | No       | Working directory                         |
| `pre_commands`    | list   | No       | Shell commands to run before exec         |
| `condition`       | string | No       | Shell condition for conditional execution |
| `fallback_cmd`    | string | No       | Command if condition fails                |

### Path-based environment variables

Different languages use different path variables. The `path_env_vars` parameter handles this:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    path_env_vars = {
        "PYTHONPATH": {
            "paths": ["$SCRIPT_DIR/runfiles/_main", "$SCRIPT_DIR/runfiles"],
            "separator": ":",
            "append": True,
        },
    }
    ```
  </Tab>

  <Tab title="Java">
    ```python theme={null}
    path_env_vars = {
        "CLASSPATH": {
            "paths": ["$SCRIPT_DIR/runfiles/_main/classes", "$SCRIPT_DIR/runfiles/_main/libs/*"],
            "separator": ":",
            "append": False,
        },
    }
    ```
  </Tab>

  <Tab title="Node.js">
    ```python theme={null}
    path_env_vars = {
        "NODE_PATH": {
            "paths": ["$SCRIPT_DIR/runfiles/_main/node_modules"],
            "separator": ":",
            "append": True,
        },
    }
    ```
  </Tab>
</Tabs>

### Conditional execution

```python theme={null}
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

```python theme={null}
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:

```python theme={null}
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

<CardGroup cols={2}>
  <Card title="Language SDKs" icon="code" href="/sdks/python">
    See built-in rules for Python, Java, Node.js, and more.
  </Card>

  <Card title="Container Building" icon="box" href="/advanced/containers">
    Learn to build OCI container images with custom rules.
  </Card>
</CardGroup>
