Skip to main content
External rule packages are how a workspace uses rules that are not built into rbs — without forking it. A package is a git repository (or archive) laid out like rbs’s own embedded ruleset. You declare it in ext.rbs under a namespace you choose, and load its modules as @<namespace>//<path>.rbs.

The ext.rbs manifest

ext.rbs lives at the workspace root. It is evaluated like any other .rbs file, but ext() is the only available function — load() is deliberately disallowed. A dependency list that could compute itself could not be read without running it, and resolution must be answerable before any rule code executes. (This is also what makes nested packages cheap: reading a fetched package’s manifest is parsing, not evaluation.)

ext()

Each entry must name exactly one source: git, http, or rule.
The rule = "org/name" coordinate form is resolved through a control plane. On a ReasonOS node this is zero-config — nodes launch with RBS_CONTROL_PLANE_URL set — and the control plane answers with a clone URL and the commit its version tag points at. Off a node, set the same variable or use git =.

Loading package modules

Once declared, a package’s .rbs files load by namespace:
The path after @<namespace>// is a file path inside the package repository; the .rbs extension is appended if missing. Resolution happens before any build-language code runs: the whole external graph is settled first, then evaluation reads it — a failed fetch fails the build up front rather than surfacing later as “unknown rule”.

The rbs ext CLI

The manifest is the source of truth; rbs ext edits it and resolves, the way go get edits go.mod. rbs ext add flags:

Pinning and updating

A ref resolves to a commit, and the commit is recorded in rbs.lock (under the ext ecosystem). After that, the pin answers:
  • Builds are reproducible — a moved tag cannot change your rules silently.
  • A warm workspace resolves offline; no remote lookups per build.
  • rbs ext update is the only operation that re-reads a moving ref.
Fetched packages are cached in .rbs/ext/git/<commit>, keyed by commit rather than namespace — two packages depending on the same rules at the same commit share one copy and one fetch. The typical update flow:
To move to a new version explicitly, edit ref = in ext.rbs (or re-run rbs ext add with the new --ref), then build.

Namespaces are private

A rule package carries its own ext.rbs for its own dependencies, and those are resolved too. Every namespace lookup is answered from the table of the repository doing the load:
Inside java-rules, @base means jvm-base. In your workspace, @base means whatever you declared — or nothing at all. Neither can shadow the other, so a published package’s internal naming never reaches its consumers. Two packages may even use the same namespace for different things. Dependency cycles between packages resolve fine: identity is content, so a package reached twice is the same package and the walk stops.

Authoring a package

A package repository mirrors the embedded ruleset layout — directories of .rbs modules that define rules with the rule SDK and export them:
Tag a release, and consumers pick it up:
Remember that load() bindings are file-local: a package module that wants to re-export symbols it loaded from a sibling module must rebind them (load("impl.rbs", _java_binary = "java_binary") then java_binary = _java_binary).

Scaffolds

A package can ship project scaffolds alongside its rules, addressed the same way and served from the same pinned commit: