> ## 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 — native.define_rule, attribute types, the implementation function, and the full ctx API

rbs is SDK-first: the language rules that ship with it are written in the
same `.rbs`-language API you use. A rule is defined with `native.define_rule()`,
usually in a module that build files `load()`.

```python theme={null}
# rules.rbs
def _hello_impl(ctx):
    dirs = ctx.bin.create_dirs()
    out = dirs.output + "/" + ctx.attr.name + ".txt"
    ctx.file.write(out, "hello from " + ctx.attr.name + "\n")
    return out

hello_file = native.define_rule(
    name = "hello_file",
    kind = "custom",
    implementation = _hello_impl,
    attrs = {
        "srcs": attr.label_list(doc = "Input files"),
    },
)
```

```python theme={null}
# BUILD.rbs
load("//rules.rbs", "hello_file")

hello_file(name = "greeting")
```

## native.define\_rule()

```python theme={null}
native.define_rule(name, kind = "custom", implementation = None, attrs = {},
                   toolchain = "", fragments = [], outputs = None, actions = None)
```

Registers a rule and **returns the rule function**, so it can be assigned
to a module-level name and exported through `load()`.

| Parameter        | Type           | Description                                                                                                                                                                                                                                                 |
| ---------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`           | `string`       | The rule's name (e.g. `"py_binary"`). Required.                                                                                                                                                                                                             |
| `kind`           | `string`       | Rule category: `"binary"`, `"library"`, `"test"`, or any custom string (default `"custom"`). Generic commands key off it — `rbs run` recognizes binaries, `rbs test` recognizes tests. Recorded on every target as the reserved `_rbs_rule_kind` attribute. |
| `implementation` | `callable`     | Function of one argument (`ctx`), executed at **build time** — the recommended form.                                                                                                                                                                        |
| `attrs`          | `dict`         | Attribute schema: name → `attr.*(...)` definition.                                                                                                                                                                                                          |
| `toolchain`      | `string`       | Name of a registered toolchain; exposed to the implementation as `ctx.toolchain`.                                                                                                                                                                           |
| `fragments`      | `list[string]` | Configuration fragments the rule reads via `ctx.fragments`.                                                                                                                                                                                                 |
| `outputs`        | `callable`     | Legacy form: function of `ctx` returning a list of output paths. Ignored when `implementation` is set.                                                                                                                                                      |
| `actions`        | `callable`     | Legacy form: function of `ctx` returning a list of [action dicts](#legacy-form-outputs--actions). Ignored when `implementation` is set.                                                                                                                     |

For `kind = "binary"` and `"library"` the target's output is its directory
under `.rbs/bin/<platform>/<package>/<name>` — the same tree `ctx.bin`
points at — so caching and `rbs run` work without per-rule wiring.

## The attr module

`attr.*` constructors describe a rule's attributes inside `attrs = {...}`.

| Constructor          | Attribute type                         |
| -------------------- | -------------------------------------- |
| `attr.string()`      | A string.                              |
| `attr.int()`         | An integer.                            |
| `attr.bool()`        | A boolean.                             |
| `attr.list()`        | A list (optionally typed with `of =`). |
| `attr.string_list()` | A list of strings.                     |
| `attr.dict()`        | A dictionary.                          |
| `attr.string_dict()` | A string → string dictionary.          |
| `attr.dict_list()`   | A list of dictionaries.                |
| `attr.label()`       | A reference to another target.         |
| `attr.label_list()`  | A list of target references.           |

Shared parameters (all optional, all keyword):

| Parameter             | Type     | Applies to                      | Description                                                                                                                                                                       |
| --------------------- | -------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mandatory`           | `bool`   | all                             | The caller must set it.                                                                                                                                                           |
| `optional`            | `bool`   | all except `list`               | Explicitly optional.                                                                                                                                                              |
| `default`             | value    | all                             | Default value (lists default to `[]`, dicts to `{}`, `bool` to `False`, `int` to `0`).                                                                                            |
| `doc` / `description` | `string` | all                             | Documentation (`description` wins if both are given).                                                                                                                             |
| `values`              | `list`   | `attr.string`                   | Allowed values.                                                                                                                                                                   |
| `sensitive`           | `bool`   | `attr.string`                   | Marks the value sensitive.                                                                                                                                                        |
| `of`                  | value    | `attr.list`                     | Element type.                                                                                                                                                                     |
| `is_dep`              | `bool`   | `attr.label`, `attr.label_list` | When `True`, the referenced targets become **build dependencies** (edges in the graph, built first). Defaults to `False` — labels are treated as plain strings unless you opt in. |

```python theme={null}
attrs = {
    "srcs": attr.label_list(doc = "Source files"),
    "deps": attr.label_list(is_dep = True, doc = "Targets built before this one"),
    "mode": attr.string(values = ["debug", "release"], default = "debug"),
    "port": attr.int(default = 8080),
}
```

### How targets instantiate

When a build file calls your rule:

* `name` is required, and `srcs` / `deps` get first-class handling (`deps`
  become graph edges; entries starting with `@external://` are filtered out
  of the graph and left for the implementation to resolve).
* Any attribute declared with `is_dep = True` also contributes `:name` and
  `//pkg:name` labels as graph edges.
* `env` and `target_compatible_with` are accepted by every rule
  automatically.
* All keyword arguments — declared or not — are recorded on the target and
  visible to the implementation as `ctx.attr.<key>`.

## The implementation function

The implementation receives one argument, `ctx`, and runs at **build
time** (not while the build file loads), only when the target is requested
and its action cache key misses.

```python theme={null}
def _impl(ctx):
    name = ctx.attr.name
    srcs = ctx.attr.srcs if hasattr(ctx.attr, "srcs") and ctx.attr.srcs else []

    dirs = ctx.bin.create_dirs()
    for src in srcs:
        ctx.file.copy(ctx.src.path(src), dirs.run_files + "/" + src)
    ...
```

<Note>
  Read optional attributes defensively — `hasattr(ctx.attr, "srcs")` — since
  an attribute the caller never set may be absent from `ctx.attr`.
</Note>

## The ctx API

### Identity and attributes

| Member        | Type           | Description                                                                                     |
| ------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| `ctx.name`    | `string`       | The target's name.                                                                              |
| `ctx.package` | `string`       | The target's package path (`""` for the root package).                                          |
| `ctx.attr`    | module         | Every attribute the caller passed, plus `name`. Access as `ctx.attr.srcs`, `ctx.attr.mode`, ... |
| `ctx.srcs`    | `list[string]` | The `srcs` values, as workspace-relative paths.                                                 |
| `ctx.deps`    | `list[string]` | The `deps` labels (external `@external://` entries removed).                                    |

### Output paths: ctx.bin

`ctx.bin` is the hermetic, package-isolated output layout for the current
target — the recommended place for everything the target produces:

| Member                                    | Value                                                                                                                                      |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `ctx.bin.output`                          | `.rbs/bin/<platform>/<package>/<name>` — the target's output directory (absolute).                                                         |
| `ctx.bin.run_files`                       | `<output>/runfiles` — runtime files.                                                                                                       |
| `ctx.bin.toolchains`                      | `<output>/toolchains` — toolchains copied in.                                                                                              |
| `ctx.bin.data`                            | `<output>/data` — data files.                                                                                                              |
| `ctx.bin.executable`                      | `<output>/<name>` — the conventional main executable path.                                                                                 |
| `ctx.bin.platform`                        | The target platform name.                                                                                                                  |
| `ctx.bin.package`                         | The package path.                                                                                                                          |
| `ctx.bin.create_dirs()`                   | Creates `output`, `run_files`, `toolchains` and `data`, returning them as a struct.                                                        |
| `ctx.bin.local_dep(dep)`                  | Resolves a local dependency label (`":lib"`, `"//pkg:lib"`, `"pkg:lib"`) to that target's output directory. Rejects `@external://` labels. |
| `ctx.bin.external_dep(package, language)` | Resolves an external package to its resolved directory for the given ecosystem, e.g. `ctx.bin.external_dep("requests", "python")`.         |

```python theme={null}
dirs = ctx.bin.create_dirs()
dep_out = ctx.bin.local_dep(":greeter")     # another target's output dir
```

### Source paths: ctx.src

| Member               | Description                                                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.src.dir`        | The package directory (where the `BUILD.rbs` lives).                                                                                          |
| `ctx.src.workspace`  | The workspace root.                                                                                                                           |
| `ctx.src.package`    | The package path.                                                                                                                             |
| `ctx.src.path(path)` | Resolves a path: relative paths resolve against the package directory, `//`-prefixed paths against the workspace root. Returns the full path. |

```python theme={null}
for src in ctx.attr.srcs:
    full = ctx.src.path(src)
    ctx.file.copy(full, dirs.run_files + "/" + src)
```

### ctx.actions

| Function                      | Signature                                                                                                                                                                                 |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.actions.run`             | `run(executable, arguments = [], inputs = [], outputs = [], mnemonic = "", progress_message = "")` — executes a binary/tool, returns its combined output.                                 |
| `ctx.actions.run_shell`       | `run_shell(command, inputs = [], outputs = [], mnemonic = "", progress_message = "", description = "")` — runs a `bash -c` command, returns its output. Fails the build on non-zero exit. |
| `ctx.actions.write`           | `write(output, content, is_executable = False)` — writes a file (mode `0755` when executable).                                                                                            |
| `ctx.actions.expand_template` | `expand_template(template, output, substitutions = {}, is_executable = False)` — reads a template and replaces `{key}` / `{{key}}` placeholders.                                          |

```python theme={null}
ctx.actions.write(
    output = ctx.bin.executable,
    content = "#!/bin/bash\nexec python3 \"$(dirname \"$0\")/main.py\" \"$@\"\n",
    is_executable = True,
)
ctx.actions.run_shell(command = "cd " + main_dir + " && go build -o app .")
```

Commands run inside the action's hermetic environment when one is active:
exactly the declared env vars (the ambient process environment is not
inherited), with the exec root as working directory.

### ctx.file

| Function              | Signature                                                                                                                                                                    |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.file.read`       | `read(filepath)` → file content as a string. Relative paths resolve against the workspace root. The content is hashed into the target's cache key.                           |
| `ctx.file.write`      | `write(filepath, content, permission = 0o644)` — parent directories are created; the file always lands on a fresh inode (safe to rewrite a script a process may be running). |
| `ctx.file.exists`     | `exists(filepath)` → `bool`.                                                                                                                                                 |
| `ctx.file.copy`       | `copy(src, dst)` — clone-first copy (APFS clonefile / Linux reflink when possible); tracked as a cache input.                                                                |
| `ctx.file.copy_batch` | `copy_batch(files, concurrency = 10)` — `files` is a list of `{"src": ..., "dst": ...}` dicts, copied in parallel.                                                           |
| `ctx.file.copy_tree`  | `copy_tree(src, dst, concurrency = 20)` — recursive directory copy; follows a symlinked root. Returns the file count.                                                        |
| `ctx.file.symlink`    | `symlink(target, link_path)` — idempotent symlink on a fresh inode; will not clobber a non-empty real directory.                                                             |
| `ctx.file.sha256`     | `sha256(text)` → hex digest of a string (for deriving stable staging keys).                                                                                                  |

### ctx.dir

| Function               | Signature                                                                                                                                                                                                                                |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.dir.create`       | `create(dirpath)` — `mkdir -p`.                                                                                                                                                                                                          |
| `ctx.dir.list`         | `list(dirpath)` → list of entry names.                                                                                                                                                                                                   |
| `ctx.dir.exists`       | `exists(dirpath)` → `bool` (true only for directories).                                                                                                                                                                                  |
| `ctx.dir.remove`       | `remove(dirpath)` — deletes a tree; **refuses any path outside the workspace's `.rbs` output tree**. Use it to rebuild derived state (stale staged files otherwise survive renames and deletions).                                       |
| `ctx.dir.link_entries` | `link_entries(src, dst)` — symlinks every top-level entry of `src` into `dst`, skipping names already present. Returns the number of links created. The entry set is tracked as a cache input.                                           |
| `ctx.dir.publish`      | `publish(src, dst)` — atomically promotes a fully staged directory to a shared location by rename. Returns `True` if `src` became `dst`; `False` when a concurrent builder won (your `src` is discarded and the winner's `dst` is used). |

```python theme={null}
if ctx.dir.exists(runfiles_dir):
    ctx.dir.remove(runfiles_dir)     # runfiles is derived state — rebuild it
ctx.dir.create(runfiles_dir + "/_main")
```

### ctx.json, ctx.archive, ctx.http

| Function                                         | Signature                                                                             |
| ------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `ctx.json.parse`                                 | `parse(json_string)` → dict/list/values.                                              |
| `ctx.json.stringify`                             | `stringify(object)` → JSON string.                                                    |
| `ctx.archive.extract`                            | `extract(archive_path, extract_dir)` — tar/zip extraction.                            |
| `ctx.archive.extract_batch`                      | `extract_batch(archives, concurrency = 5)` — list of `{"archive": ..., "dest": ...}`. |
| `ctx.http.get`                                   | `get(url)` → response body as a string.                                               |
| `ctx.http.download`                              | `download(url, filepath)` — downloads to a path.                                      |
| `ctx.http.get_batch` / `ctx.http.download_batch` | Parallel variants taking a list of dicts and a concurrency limit.                     |

### Tools and toolchains

| Member                                   | Description                                                                                                  |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `ctx.tool.run(tool, args = [])`          | Executes a tool, returns combined output; fails the build on error.                                          |
| `ctx.tool.exec(tool, args = [])`         | Alias of `run`.                                                                                              |
| `ctx.tool.path(tool)`                    | The tool's invocation path.                                                                                  |
| `ctx.tool.platform()`                    | Current target platform name (e.g. `"darwin-arm64"`).                                                        |
| `ctx.toolchain`                          | Struct of the rule's declared toolchain attributes (present when the rule set `toolchain =`).                |
| `ctx.toolchains`                         | Struct of every registered toolchain; each entry carries its attributes plus `binary` (path) and `run(...)`. |
| `ctx.tools.copy(toolchain, destination)` | Copies a registered toolchain (binary + files) into a destination directory.                                 |

### External dependencies

| Function                                                                 | Description                                                              |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `ctx.external_deps.copy(dependencies, destination, language = "python")` | Copies resolved external packages (list of names/refs) into a directory. |
| `ctx.external_deps.get(...)` / `list(...)` / `exists(...)`               | Query resolved packages.                                                 |

### Text and HTML helpers

| Function                                                                                                 | Description                                      |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `ctx.text.template(path, vars = {})`                                                                     | Loads a template file and substitutes variables. |
| `ctx.text.replace` / `join` / `lines` / `dedent`                                                         | String utilities.                                |
| `ctx.html.inject`, `inject_script`, `inject_style`, `inject_importmap`, `set_attribute`, `rewrite_paths` | HTML manipulation for web rules.                 |

### Specialized modules

| Module                                      | Purpose (members)                                                                                                                                                                                                |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.oci`                                   | Container image building: `create_layer`, `create_layer_from`, `write_image`, `write_docker_tar`, `image_builder`, `pull`, `load_base`, `binary_platform`.                                                       |
| `ctx.test`                                  | Test SDK: `register_kind`, `register_size`, `register_coverage_format`, `set_defaults`, `is_test_kind`, `get_timeout_for_size`, `coverage_config`, `post_process_command`, `write_config`, and `list_*` queries. |
| `ctx.watch`                                 | Watch-mode config: `write_config`, `default_ignore`, and per-language extension helpers.                                                                                                                         |
| `ctx.lint`                                  | Linting: `run`, `fix`, `check`, `list`, `get_linter`, `run_all`, `fix_all`.                                                                                                                                      |
| `ctx.job`                                   | Job scheduling: `create`, `requirements`, `submit`.                                                                                                                                                              |
| `ctx.fragments`                             | Configuration fragments the rule requested via `fragments = [...]`, e.g. `ctx.fragments.python.version`.                                                                                                         |
| `ctx.output_path`                           | Workspace output dirs (`bin`, `out`, `testlogs`, `toolchains`, `external_deps`, plus `host_toolchains` / `host_external_deps`).                                                                                  |
| `ctx.outputs` / `ctx.dirs` / `ctx.runfiles` | Older path helpers that do **not** include the package in the path. Prefer `ctx.bin` for new rules — it is package-isolated and what the cache restores.                                                         |

## Outputs and caching

* For `kind = "binary"` / `"library"`, the target's recorded output is its
  `ctx.bin` output directory; the action cache stores and restores that
  tree, and `rbs run` finds the executable there.
* `ctx.file.read`, `ctx.file.copy`, `ctx.file.copy_tree` and
  `ctx.dir.link_entries` register what they touch as **cache inputs**, so
  editing a source file invalidates the target even though your
  implementation is a build-language function rather than a command line.
* Treat staged trees (runfiles, `node_modules`, dist bundles) as **derived
  state**: `ctx.dir.remove` then rebuild, or deleted sources live on in the
  old tree and keep getting compiled.

## Sharing data between rules

rbs has no Bazel-style provider objects. The convention — used by the
embedded language rules — is to write a metadata file into the target's
output directory and have dependents read it:

```python theme={null}
def _lib_impl(ctx):
    dirs = ctx.bin.create_dirs()
    ctx.file.write(dirs.output + "/" + ctx.attr.name + "_deps.json",
                   ctx.json.stringify({
                       "name": ctx.attr.name,
                       "srcs": ctx.attr.srcs,
                       "runfiles": [dirs.run_files],
                   }))

def _bin_impl(ctx):
    for dep in ctx.attr.deps:
        dep_out = ctx.bin.local_dep(dep)
        meta = ctx.json.parse(ctx.file.read(dep_out + "/" + dep.replace(":", "") + "_deps.json"))
        ...
```

## Legacy form: outputs + actions

Instead of `implementation`, a rule may declare two functions evaluated at
load time:

```python theme={null}
native.define_rule(
    name = "concat",
    kind = "custom",
    outputs = lambda ctx: [output_path.out + "/" + ctx.name + ".txt"],
    actions = lambda ctx: [{
        "name": "concat_" + ctx.name,
        "command": ["sh", "-c", "cat " + " ".join(ctx.srcs) + " > out.txt"],
        "inputs": ctx.srcs,
        "outputs": [output_path.out + "/" + ctx.name + ".txt"],
        "description": "Concatenate sources",
        "mnemonic": "Concat",
    }],
)
```

Each action dict may carry: `name`, `command` (argv list) **or** `fn` (a
build-language callable), `inputs`, `outputs`, `working_dir`, `description`,
`mnemonic`, `toolchain`. Prefer `implementation` for new rules.

## Testing rules

### Unit tests: rbs rules test

```bash theme={null}
rbs rules test              # run all *_test.rbs files
rbs rules test genrule      # run tests matching "genrule"
rbs rules test lang/        # run tests in a directory
```

Test files use the `assert` functions:

```python theme={null}
assert.equals(actual, expected, message = "optional")
assert.true(condition, message = "optional")
assert.false(condition, message = "optional")
assert.contains(haystack, needle, message = "optional")
assert.fails(function, message = "optional")
```

### Integration tests: rbs rules integration

```bash theme={null}
rbs rules integration            # run all *_integration_test.rbs files
rbs rules integration python     # filter by pattern
```

Integration tests spin up a real temporary workspace and run rbs against
it:

```python theme={null}
integration.create_workspace(workspace_content = "...", build_content = "...")
integration.copy_rules("embedded/rules/python")
result = integration.run_rbs("build //...")
integration.assert_output(result, contains = "expected output", exit_code = 0)
integration.assert_file("path/to/file", exists = True, contains = "expected text")
integration.write_file("path/to/file", "content")
integration.read_file("path/to/file")
integration.cleanup()
```
