tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_brand

rules_brand

Wear a brand without building one — consume a .brando package in any Bazel repo, no Python required

Latest0.1.1
Versions2
CategoryBazel rules
Compat level1
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_brand/
Sourcegithub.com/mattmarshall/rules_brand
MODULE.bazelstarlark
bazel_dep(name = "rules_brand", version = "0.1.1")

View source & releases on GitHub ↗

Wear a brand. Don’t build one.

bazel_dep(name = "rules_brand", version = "0.1.1")

brand = use_extension("@rules_brand//brand:extensions.bzl", "brand")
brand.from_archive(name = "leangres", archive = "//third_party:leangres.brando")
use_repo(brand, "leangres")
filegroup(name = "site_assets", srcs = [
    "@leangres//:theme.css",
    "@leangres//:favicon.ico",
    "@leangres//:fonts",
])

That is the whole API.

What this is for

A brand is produced by a pipeline: geometry in Python, CSG, rasterization, gradient math, font subsetting, LaTeX templating. A repo that merely wears one needs none of that — it needs a stylesheet, a favicon and a couple of SVGs.

Before this module, the two were the same thing. Every repo in our fleet that used a brand also had to be a brand pipeline: shapely, Pillow, a pip hub, a Python toolchain, a copied raster.py, and a hand-maintained outs list carrying # keep in sync with gen_mark.py. Two of those repos had no pip hub at all and simply could not build. Others gave up and hardcoded the hex codes in TypeScript — three separate copies, which is how a palette drifts without anyone editing it.

rules_brand reads a .brando package — a manifest plus content-addressed artifacts — and generates the targets. It depends on nothing.

Targets it generates

By logical name, exactly as the producing brand declared them:

@leangres//:theme.json   @leangres//:theme.css   @leangres//:mark.svg
@leangres//:favicon.ico  @leangres//:icon-512.png ...

And by brando.v1.ArtifactKind, which is the same vocabulary for every brand — so a BUILD file that consumes one brand consumes any of them:

:mark  :icons  :wordmark  :theme  :mdbook_theme  :latex_class
:office  :guidelines  :terminal  :social  :fonts  :all

Groups a brand doesn’t ship are emitted empty rather than omitted, so referring to :terminal before a brand has a terminal theme builds to nothing instead of breaking the consumer.

Plus the manifest itself — :manifest (brando.v1.BrandPackage binpb) and :manifest_json — for anything that wants the spec: positioning, voice, font licensing and provenance, not just the pixels.

The palette in Starlark

load("@leangres//:brand.bzl", "LIGHT", "DARK", "DISPLAY_NAME", "ASSETS")

oci_image(labels = {"org.opencontainers.image.vendor": DISPLAY_NAME})

Reading a hex from a JSON file needs a build action. Reading it from a .bzl needs a load. Consumers routinely want a colour at loading time — a chart value, an OCI label, a badge URL — and every one that wanted it before hardcoded it.

Pinning

from_archive takes a vendored file. from_url fetches a published one and requires integrity — not optional. A brand is what an organisation looks like, and packages are served from GitHub Releases, whose assets can be replaced. Immutability therefore comes from the pin, not from the host: the hash in the filename keeps two builds from colliding, and the integrity is what makes the URL mean one thing. Without it, a swapped asset restyles every consumer at once and nothing reports it.

Why the archive carries its manifest twice

A .brando holds brand.binpb and brand.json. Generating a target per asset means reading the manifest in a repository rule, which runs during loading — before any toolchain is resolved, with no protobuf runtime and no way to acquire one. Starlark does have json.decode. Both encodings come from one textproto in the producing build, so they cannot disagree; the JSON is a convenience encoding of the proto, never a second source of truth.

Acceptance tests

e2e/ is a module that declares rules_brand and nothing else — no toolchain, no rules_python, no bazel_skylib. If @brando//:theme.css builds there, the seam holds. Adding a dependency to that module weakens the test.

e2e_url/ covers from_url, the path a real consumer writes: a fetch, an extraction, and an integrity check. It is a separate module because it hits the network, and folding it into e2e/ would quietly falsify that module’s one claim. Verified in both directions — a tampered integrity is rejected with Checksum was … but wanted ….

It also gates the one divergence that is otherwise invisible: the palette rules_brand projects into Starlark must equal the palette in the artifact the brand actually ships. Nothing crashes when those drift — the site renders one accent and the LaTeX class another, and someone notices in a screenshot weeks later.

See also

brando — the producing side: the marklib geometry library, the Bazel rules, and brand_package, which emits the .brando this module consumes.

Conformance#

No gate findings. 8 contested atoms. See how gating works or the full report.

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
bazel_skylib 1.8.2 1.9.0 ×2
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
protobuf 33.4 34.0.bcr.1 ×2
rules_jvm_external 6.7 6.8 ×4
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1

Versions#

2 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.1.1 latest nE2z/82tYnF9AycD… tag archive ↗
0.1.0 HcNmJo5+ltBOz+T+… tag archive ↗

← All modules