rules_brand
Wear a brand without building one — consume a .brando package in any Bazel repo, no Python required
| Latest | 0.1.1 |
|---|---|
| Versions | 2 |
| Category | Bazel rules |
| Compat level | 1 |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/rules_brand/ |
| Source | github.com/mattmarshall/rules_brand |
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.
| Atom | Resolved here | Elsewhere |
|---|---|---|
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.
| Version | Integrity (sha256) | Source archive |
|---|---|---|
0.1.1 latest | nE2z/82tYnF9AycD… | tag archive ↗ |
0.1.0 | HcNmJo5+ltBOz+T+… | tag archive ↗ |