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

rules_lean

Bazel rules for Lean 4 with Lake integration (rules_lean). Reuses Lake's mathlib cache via lake_workspace repository rule.

Latest0.6.2
Versions24
CategoryBazel rules
Compat level1
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_lean/
Sourcegithub.com/tomato-bazel/rules_lean
MODULE.bazelstarlark
bazel_dep(name = "rules_lean", version = "0.6.2")

View source & releases on GitHub ↗

Bazel rules for Lean 4, with native Lake integration that reuses Mathlib’s upstream Reservoir cache instead of forcing each consumer to self-host a multi-gigabyte olean tarball.

  • rules: lean_test, lean_axiom_test, lean_emit, lean_library, lean_binary, lean_prebuilt_library, lean_toolchain — see docs/lean.md.
  • proof gates (0.7.0): a sorry fails the build (forbid_sorry, default True), and lean_axiom_test fails it when a theorem’s transitive axiom dependencies leave an allowlist. See Gating a proof.
  • lake integration: lake_workspace repository rule + lake module extension — see docs/lake.md.
  • RulesLean Lean library (lean/lib/): structured introspection of .olean files (RulesLean.Olean) and Lake workspaces (RulesLean.Workspace). Internal helpers under RulesLean.Internal.* are unstable; treat them as opt-in and expect API churn between releases. See lean/lib/RulesLean.lean for the entry-point doc.
  • lake_imports_manifest target (opt-in): set emit_imports_manifest = True on your lake.workspace and lake_workspace builds the RulesLean library + oleanImports CLI and runs it over every olean in the workspace. Result lands at @<your-lake-deps>//:lake_imports_manifest — a TSV of <path>\t<imported-module> edges (~5MB / 42k edges for full mathlib), for import-graph analysis, tree-shaking, dead-code detection. Off by default since 0.6.0: compiling the CLI leaves a ~149MB .lake/build/ behind (measured, darwin/arm64) in every lake_workspace, whether or not anything reads the manifest. The target still exists when off — it is just empty.

Install

Add the registry to your .bazelrc:

common --registry=https://registry.fastverk.com/
common --registry=https://bcr.bazel.build/

In your MODULE.bazel:

bazel_dep(name = "rules_lean", version = "0.3.0")

lake = use_extension("@rules_lean//lean:lake.bzl", "lake")
lake.workspace(
    name = "lake_deps",
    lean_toolchain  = "//:lean-toolchain",
    lakefile        = "//:lakefile.lean",
    lake_manifest   = "//:lake-manifest.json",
)
use_repo(lake, "lake_deps")
register_toolchains("@lake_deps//:lean_toolchain_def")

Quick start

Your repo root needs three Lake-convention files:

lean-toolchain — pins the Lean version (Lake and Bazel both honor it):

leanprover/lean4:v4.29.1

lakefile.lean — a deps-only lakefile listing Lake packages you want:

import Lake
open Lake DSL

package «my-project» where

require mathlib from git
  "https://github.com/leanprover-community/mathlib4.git" @ "v4.29.1"

lake-manifest.json — generate once with elan-installed lake, then commit:

lake update     # produces lake-manifest.json with all transitive revs pinned

Now any BUILD.bazel can typecheck Lean code against the resolved packages:

load("@rules_lean//lean:lean.bzl", "lean_test")

lean_test(
    name  = "smoke",
    srcs  = ["Smoke.lean"],
    entry = "Smoke.lean",
    deps  = [
        "@lake_deps//:mathlib",
        "@lake_deps//:batteries",
    ],
)
-- Smoke.lean
import Mathlib.Data.Finset.Basic
example : (∅ : Finset Nat).card = 0 := Finset.card_empty

bazel test //:smoke will, on first run: download the Lean toolchain, run lake update, run lake exe cache get (Reservoir-cached mathlib oleans), and typecheck Smoke.lean.

Gating a proof

A green lean_test means the code type-checks. It does not mean anything was proved: sorry is a warning, lean exits 0 on it, and until 0.7.0 that warning was discarded. Two rules close the gap, and neither needs Mathlib.

load("@rules_lean//lean:lean.bzl", "lean_axiom_test", "lean_test")

# `forbid_sorry` defaults True — an admitted goal anywhere in `srcs` is red.
lean_test(
    name = "proofs_test",
    srcs = glob(["**/*.lean"]),
    entry = "Root.lean",
)

# Every named theorem's TRANSITIVE axiom dependencies must be in the allowlist.
lean_axiom_test(
    name = "axioms_test",
    srcs = glob(["**/*.lean"]),
    theorems = [
        "MyProject.main_theorem",
        "MyProject.confluence",
    ],
    # allowed_axioms defaults to [propext, Classical.choice, Quot.sound].
)

lean_axiom_test generates a Lean module and checks at elaboration time with Lean.collectAxioms — the same API behind #print axioms, so it agrees with a hand audit by construction. The default allowlist is Lean’s three standard axioms; what it excludes is the point:

AxiomWhat its presence means
sorryAxAn admitted goal. The theorem is not proved.
Lean.ofReduceBoolA native_decide — the claim rests on the compiler and runtime, not the kernel.

Tightening the allowlist is the interesting direction. A theorem that needs only [propext, Quot.sound] today and quietly picks up Classical.choice tomorrow has changed, and allowed_axioms = ["propext", "Quot.sound"] is what reports it.

Auditing a compiled dep instead of sources — name the modules to import:

lean_axiom_test(
    name = "axioms_test",
    deps = [":my_library"],
    imports = ["MyProject"],
    theorems = ["MyProject.main_theorem"],
)

See examples/axiom_audit for both, with the negative tests that prove each gate actually fires.

How it works

lake_workspace is a Bazel repository rule that:

  1. Reads lean-toolchain, downloads the matching Lean tarball (sha256-pinned for known versions).
  2. Stages your lakefile + lake-manifest.json into the external repo.
  3. Runs lake update to materialize all transitive Lake package checkouts at the manifest-pinned revs.
  4. If mathlib is in the dep graph, runs lake exe cache get to fetch prebuilt oleans from the upstream Reservoir cache.
  5. Generates a BUILD.bazel exposing each resolved Lake package as its own lean_prebuilt_library (target name = Lake’s directory name: :mathlib, :batteries, :Cli, :LeanSearchClient, …).

What’s hermetic

LayerPinned by
Lean toolchainsha256 in lean/private/known_lean_versions.bzl
Lake dep git revsYour committed lake-manifest.json (Lake’s lockfile)
Mathlib oleansContent-addressed by mathlib commit in Reservoir cache (verified by Lake)

For Lake packages not covered by the Reservoir cache (anything outside mathlib’s transitive deps), pass allow_source_build = True to lake.workspace — the rule then runs lake build <pkg> to compile oleans from source. Slow but unavoidable for custom deps.

What’s not (yet) hermetic

  • Lean versions that aren’t pinned in known_lean_versions.bzl download unverified (with a warning). Add an entry — one line — for any new version you need.
  • lake update reaches the network. The lake-manifest.json constrains what gets resolved, but the network has to be there. Bazel’s normal repository-cache mitigates the cost on rebuilds.

Compatibility

  • Bazel: 7.4+, bzlmod required.
  • Lean: 4.29.1 and 4.32.2 exercised; lean_axiom_test and the sorry gate are verified on both. Other versions: add the platform sha256 to lean/private/known_lean_versions.bzl (compute with curl -fsSL <url> | shasum -a 256).
  • Platforms: darwin_aarch64, darwin_x86_64, linux_x86_64, linux_aarch64.

Contributing

Rule reference docs (docs/lean.md, docs/lake.md) are stardoc-generated from the .bzl docstrings and committed to source. After editing a rule docstring, regenerate:

bazel run //docs:update

CI gates this via bazel test //docs/... (diff_test against the committed output).

License

MIT.

Usage#

Real usage, taken from the module’s examples/.

examples/axiom_audit/BUILD.bazel

load("@rules_lean//lean:lean.bzl", "lean_axiom_test", "lean_library", "lean_test")

# `lean_axiom_test`, and the `forbid_sorry` gate, with their failure paths.
#
# Every claim a gate makes is only worth what its NEGATIVE test is worth, so
# each pass here has a matching `_is_rejected` target that must fail, plus a
# control that isolates WHICH gate is doing the rejecting. The negatives are
# `manual`-tagged (they cannot be in `bazel test //...`, they are meant to be
# red) and CI runs them explicitly, asserting a non-zero exit — see the
# `axiom_audit` job in .github/workflows/ci.yml.
#
# Fixture axiom footprints, from `#print axioms`:
#   Audited.constructive   — none
#   Audited.propositional  — [propext]
#   Audited.classical      — [propext, Classical.choice, Quot.sound]
#   Audited.admitted       — [sorryAx]

lean_library(
    name = "audited",
    srcs = ["Audited/Arith.lean"],
)

# ── Positive ──────────────────────────────────────────────────────────────

# The default allowlist: Lean's three standard axioms. Accepts all three
# honest theorems.
lean_axiom_test(
    name = "default_allowlist_test",
    srcs = ["Audited/Arith.lean"],
    theorems = [
        "Audited.constructive",
        "Audited.propositional",
        "Audited.classical",
    ],
)

# A TIGHTER allowlist that still holds. Without this, a rule that ignored
# `allowed_axioms` entirely and always passed would look identical to a working
# one on `default_allowlist_test` alone.
lean_axiom_test(
    name = "tight_allowlist_test",
    srcs = ["Audited/Arith.lean"],
    allowed_axioms = ["propext"],
    theorems = [
        "Audited.constructive",
        "Audited.propositional",
    ],
)

# Same audit, but against a COMPILED dep rather than sources — the `deps` +
# `imports` path, which is how a consumer with a published olean tree wires it.
lean_axiom_test(
    name = "via_dep_test",
    imports = ["Audited.Arith"],
    theorems = ["Audited.classical"],
    deps = [":audited"],
)

# The control for `sorry_is_rejected` below: the same file, same rule, gate
# off. If this ever goes red the negative test proves nothing, because the
# failure would not be attributable to the gate.
lean_test(
    name = "sorry_allowed_when_permitted_test",
    srcs = ["Audited/Admitted.lean"],
    entry = "Audited/Admitted.lean",
    forbid_sorry = False,
)

# ── Negative: these MUST fail. CI asserts it. ─────────────────────────────

# The allowlist tightened by one axiom. `Audited.classical` needs
# `Classical.choice`; dropping it from the allowlist must turn the build red.
lean_axiom_test(
    name = "tightened_allowlist_is_rejected",
    srcs = ["Audited/Arith.lean"],
    allowed_axioms = [
        "propext",
        "Quot.sound",
    ],
    tags = ["manual"],
    theorems = ["Audited.classical"],
)

# `sorryAx` is in no allowlist, so an admitted theorem fails the AXIOM gate.
# `forbid_sorry = False` on purpose: it isolates the axiom check from the
# compile-time sorry check, so this target proves the audit itself rejects it.
lean_axiom_test(
    name = "admitted_is_rejected",
    srcs = ["Audited/Admitted.lean"],
    forbid_sorry = False,
    imports = ["Audited.Admitted"],
    tags = ["manual"],
    theorems = ["Audited.admitted"],
)

# The compile-time gate, independent of any axiom audit: `forbid_sorry`
# defaults True, so merely compiling a tree containing a `sorry` is red.
lean_test(
    name = "sorry_is_rejected",
    srcs = ["Audited/Admitted.lean"],
    entry = "Audited/Admitted.lean",
    tags = ["manual"],
)

examples/batteries_smoke/BUILD.bazel

load("@lake_deps_smoke//:packages.bzl", "LAKE_PACKAGES")
load("@rules_lean//lean:lean.bzl", "lean_test")

package(default_visibility = ["//visibility:public"])

exports_files([
    "lakefile.lean",
    "lake-manifest.json",
    "lean-toolchain",
])

# End-to-end smoke: validates that lake_workspace materialized Batteries
# and exposed it as a per-package lean_prebuilt_library that lean_test
# can consume. No mathlib in deps -> no Reservoir cache get -> the
# `allow_source_build = True` source-build path is what's under test.
#
# Also validates the generated `packages.bzl`: instead of hand-listing
# `@lake_deps_smoke//:batteries`, consume the derived `LAKE_PACKAGES`
# label set (here, exactly [batteries]).
lean_test(
    name = "smoke_test",
    srcs = ["Smoke.lean"],
    entry = "Smoke.lean",
    deps = LAKE_PACKAGES,
)

examples/olean_roundtrip/BUILD.bazel

load("@rules_lean//lean:lean.bzl", "lean_library", "lean_olean_archive", "lean_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")

# Round-trip for the 0.4.0 cross-repo compiled-olean seam:
#
#   lean_library  → compile Lib/Thing.lean to a persistent Lib/Thing.olean tree
#   lean_test     → Consumer.lean type-checks against the PREBUILT olean (dep),
#                   with no source re-share and no recompile of the library
#   lean_olean_archive → bundle the library's .olean tree into a tarball
#   sh_test       → assert the tarball carries Lib/Thing.olean
#
# (The full http_archive → lean_imported_library hop is exercised where a
# release artifact is actually fetched; lean_imported_library shares its
# implementation with the lake-tested lean_prebuilt_library.)

lean_library(
    name = "lib",
    srcs = ["Lib/Thing.lean"],
)

lean_test(
    name = "consume_test",
    srcs = ["Consumer.lean"],
    entry = "Consumer.lean",
    deps = [":lib"],
)

lean_olean_archive(
    name = "lib_archive",
    library = ":lib",
)

sh_test(
    name = "archive_has_olean_test",
    srcs = ["archive_check.sh"],
    args = ["$(location :lib_archive)"],
    data = [":lib_archive"],
)

examples/regen_smoke/BUILD.bazel

load("@rules_lean//lean:lean.bzl", "lean_main_test", "lean_regen_test")

# Smoke for `lean_regen_test`. Validates the full pipeline:
#   1. lean_emit runs Hello.lean via the registered Lean toolchain.
#   2. Captures `IO.println "hello from lean_regen_test"` to stdout.
#   3. diff_test compares the captured stdout (with trailing newline)
#      against the committed expected.txt.
# Fails if the committed expected drifts from what Hello.lean emits.
lean_regen_test(
    name = "regen_smoke",
    srcs = ["Hello.lean"],
    entry = "Hello.lean",
    expected = "expected.txt",
)

# Smoke for the `data` attr (v0.3.3). EchoFixture.lean does
# `IO.FS.readFile "fixture.txt"` and echoes the content; lean_regen_test
# diff_tests the captured stdout against the same `fixture.txt`.
# Passing proves the data file is staged in the action's work dir
# and reachable via package-relative path from the Lean entry.
lean_regen_test(
    name = "regen_smoke_data",
    srcs = ["EchoFixture.lean"],
    data = ["fixture.txt"],
    entry = "EchoFixture.lean",
    expected = "fixture.txt",
)

# Smoke for `lean_main_test` (v0.3.5). Compiles + runs ExitZero.lean
# whose `main : IO UInt32` returns 0. Test passes iff the Lean program
# exits 0; non-zero exit (or compile failure) fails the test.
lean_main_test(
    name = "regen_smoke_exit",
    srcs = ["ExitZero.lean"],
    entry = "ExitZero.lean",
)

examples/shared_namespace/BUILD.bazel

load("@rules_lean//lean:lean.bzl", "lean_library", "lean_test")

# Regression test for LEAN_PATH namespace resolution.
#
# Lean resolves a module in the FIRST LEAN_PATH root owning its top-level
# directory and does not fall through. Two dep roots both owning `Pg/` therefore
# cannot both be reached via LEAN_PATH — the second one's modules resolve into
# the first one's root and the compile fails with "object file ... does not
# exist".
#
# `_dep_manifest_lines` used to stage a dep only when it shared a top-level
# namespace with the CONSUMER. Here the consumer is `App`, disjoint from both
# deps, so nothing was staged and both went on leanpath — the failing shape.
# Now a namespace owned by more than one dep root is staged regardless.
#
# This mirrors the real case: leangres publishes pgcatalog and pgquery, both
# rooted at `Pg/`, and a consumer rooted at `Aion/` has to import both.

lean_library(
    name = "pg_catalog",
    srcs = ["Pg/Catalog/Tables.lean"],
)

lean_library(
    name = "pg_query",
    srcs = ["Pg/Query/Top.lean"],
)

lean_test(
    name = "two_deps_one_namespace_test",
    srcs = ["App/Main.lean"],
    entry = "App/Main.lean",
    deps = [
        ":pg_catalog",
        ":pg_query",
    ],
)

Rules & providers#

Generated with Stardoc from the module's .bzl sources.

from docs/lake.md

Lake integration for rules_lean.

lake_workspace is a repository rule that materializes any Lake workspace (lakefile + lake-manifest.json) into a Bazel-managed external repo, downloads the matching Lean toolchain, resolves Lake packages, and exposes each resolved package as its own lean_prebuilt_library target.

Generated targets in @<name>//::

  • :lean_toolchain / :lean_toolchain_def — register via register_toolchains(...).
  • :<package> — one lean_prebuilt_library per Lake package found under .lake/packages/<package>/. Target names preserve Lake’s directory casing (e.g. :mathlib, :batteries, :Cli, :LeanSearchClient). Consumers depend on multiple packages by listing all needed names.

Deps are materialized from the pinned lake-manifest.json (never via lake update — see _lake_workspace_impl). Fast path for mathlib-based workspaces: if .lake/packages/mathlib/ is present, the rule runs lake exe cache get (tree-shaken by cache_roots) to pull prebuilt oleans from the Reservoir cache (covering mathlib + its transitive deps). For non-mathlib packages and workspaces, lake build produces oleans from source.

Use via the module extension:

lake = use_extension("@rules_lean//lean:lake.bzl", "lake")
lake.workspace(
    name = "lake_deps",
    lean_toolchain = "//:lean-toolchain",
    lakefile = "//:lakefile.lean",
    lake_manifest = "//:lake-manifest.json",
)
use_repo(lake, "lake_deps")
register_toolchains("@lake_deps//:lean_toolchain_def")

# In a BUILD.bazel:
lean_test(
    name = "smoke",
    srcs = ["Smoke.lean"],
    entry = "Smoke.lean",
    deps = ["@lake_deps//:mathlib", "@lake_deps//:batteries"],
)

Hermeticity:

  • The Lean toolchain is downloaded with a known sha256 (see private/known_lean_versions.bzl) when the version is pinned there. Unpinned versions download unverified (warning emitted).
  • Lake dep revs are pinned by the user’s committed lake-manifest.json.
  • Mathlib oleans (when applicable) are content-addressed by mathlib’s commit hash in the upstream Reservoir cache; integrity is verified by Lake.

Constraints on the lakefile passed in:

  • Should be a deps-only lakefile (the rule creates a placeholder package source). Build directives (lean_lib, lean_exe) for the user’s own code don’t belong here — those live in Bazel BUILD files via the lean_test / lean_emit rules.

lake_workspace

load("@rules_lean//lean:lake.bzl", "lake_workspace")

lake_workspace(name, allow_source_build, cache_roots, emit_imports_manifest, lake_manifest,
               lakefile, lean_dist_lake, lean_dist_toolchain, lean_toolchain, olean_cache,
               olean_cache_packages)

Materializes a Lake workspace as a Bazel external repo. Produces :lean_toolchain_def + one lean_prebuilt_library per resolved Lake package (target name = Lake’s directory name).

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this repository.Namerequired
allow_source_buildIf True, run lake build <pkg> for every package whose oleans aren’t covered by lake exe cache get. Slow for large packages (mathlib from source is ~30 min); fast and necessary for custom Lake deps that have no upstream cache.BooleanoptionalFalse
cache_rootsModule specs to TREE-SHAKE mathlib’s olean download to — the roots your workspace actually imports (e.g. [“Mathlib.Data.List.Infix”, “Mathlib.Order.Basic”]). Passed to lake exe cache get <roots>, which mathlib’s cache CLI resolves via filterByRootModules to those roots PLUS their transitive closure — so the set is always sound; you cannot under-fetch a module you import.

Empty (the default) fetches ALL of mathlib, which is what every consumer did before this attr existed. That is rarely what you want: measured against mathlib @ v4.30.0-rc2 (7933 modules, ~2.0 GB of olean+ilean), a Lean→SQL emitter needing 6 roots pulls 1302 modules / 324 MB — an 84% saving. Adding a CategoryTheory + Lie-algebra lane on top cost only +102 MB, so the win is in NOT fetching the other 6373 modules, not in trimming what you import.

Specs resolve against the src search path, so Mathlib.Data.List.Infix and Mathlib/Data/List/Infix.lean both work. Ignored for workspaces without mathlib (their cache exe does not exist).
List of stringsoptional[]
emit_imports_manifestIf True, populate @<ws>//:lake_imports_manifest (the <olean-path>\t<imported-module> TSV over every resolved package). Off by default because producing it first compiles the RulesLean oleanImports CLI with the consumer’s Lean toolchain, which leaves a ~149 MB .lake/build/ behind PER lake_workspace (measured, darwin/arm64, Lean v4.30.0-rc2). The manifest is a niche introspection aid (“what does olean X import?”) and nothing needs it to build Lean code, so the default is not to pay for it.

The target and the .tsv exist either way — empty when off — so flipping this attr never breaks a consumer’s BUILD file, only the contents it reads.
BooleanoptionalFalse
lake_manifestThe committed lake-manifest.json (pins git revs of every Lake dep).Labelrequired
lakefileThe lakefile (deps-only — no library/exe directives for the user’s own code).Labelrequired
lean_dist_lakeThe shared toolchain’s bin/lake (for fetch-time lake runs).Labelrequired
lean_dist_toolchainThe shared lean_toolchain rule; the workspace re-declares a toolchain() pointing at it and aliases :lean_toolchain to it.Labelrequired
lean_toolchainThe lean-toolchain file. Drives both Lake’s toolchain choice and the Lean binary Bazel downloads.Labelrequired
olean_cacheBase URL/path for prebuilt-olean tarballs (a private cache — never public by default). The LEAN_OLEAN_CACHE repo_env overrides it. Empty → packages without an upstream cache fall back to source build.Stringoptional""
olean_cache_packagesLake packages to fetch from the olean cache instead of source-building (e.g. [“cslib”]). Needs a configured cache base; artifact path is /---.tar.gz (the .lake/build tree).List of stringsoptional[]

lean_dist

load("@rules_lean//lean:lake.bzl", "lean_dist")

lean_dist(name, version)

Extracts the Lean toolchain once; shared by all lake.workspace repos of the same version (deduplicates the multi-GB toolchain across workspaces).

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this repository.Namerequired
versionLean version tag (e.g. ‘v4.30.0-rc2’); platform is auto-detected.Stringrequired

lake

lake = use_extension("@rules_lean//lean:lake.bzl", "lake")
lake.workspace(name, allow_source_build, cache_roots, emit_imports_manifest, lake_manifest,
               lakefile, lean_toolchain, olean_cache, olean_cache_packages)

TAG CLASSES

workspace

Attributes

NameDescriptionTypeMandatoryDefault
name-Namerequired
allow_source_build-BooleanoptionalFalse
cache_rootsModule specs to tree-shake mathlib’s olean download to (the roots this workspace imports). Resolved to those roots PLUS their transitive closure, so the fetch cannot miss something you import. Empty → fetch ALL of mathlib (~2.0 GB at v4.30.0-rc2). See the repo rule’s attr.List of stringsoptional[]
emit_imports_manifestPopulate @<ws>//:lake_imports_manifest. Off by default: producing it compiles the RulesLean oleanImports CLI (~149 MB of .lake/build/) per workspace, and building Lean code never needs it. The target exists either way — empty when off. See the repo rule’s attr.BooleanoptionalFalse
lake_manifest-Labelrequired
lakefile-Labelrequired
lean_toolchain-Labelrequired
olean_cacheBase URL/path for prebuilt-olean tarballs (private; overridden by the LEAN_OLEAN_CACHE repo_env). Empty → source-build packages with no cache.Stringoptional""
olean_cache_packagesLake packages to fetch from the olean cache instead of building.List of stringsoptional[]

from docs/lean.md

Bazel rules for Lean 4.

User-facing rules: lean_toolchain — registers a Lean compiler binary + runtime tree. Normally produced by lake_workspace (see lake.bzl); can also be declared by hand against a hermetic lean tarball. lean_prebuilt_library — exposes a tree of prebuilt .olean files as a LeanInfo provider consumable via the deps attr. The path_marker file’s parent directory becomes the LEAN_PATH entry. lean_library — compile a set of .lean sources to a persistent .olean import-root tree (build outputs) and expose it as LeanInfo. Lets one module be a compiled dep of another (no source re-sharing). Transitive: its LeanInfo carries its deps’ closure too. lean_olean_archive — bundle a lean_library’s own .olean tree into a tarball — the deployable cross-repo release artifact. lean_imported_library — expose an unpacked .olean tarball (e.g. from an http_archive of a release asset) as LeanInfo, with NO recompile. The cross-repo consume side. lean_test — stages a set of .lean sources into a module-path layout and invokes the compiler on an entry point. Returns 0 if all type-check, nonzero otherwise. Accepts deps = [LeanInfo] and prepends each dep’s import root to LEAN_PATH. lean_emit — like lean_test, but the entry file defines main : IO Unit; runs it and captures stdout to a declared output file. The Lean kernel becomes the source of truth for emitted artifacts (SQL, TTL, Markdown). Same deps plumbing as lean_test. lean_axiom_test — assert that each named theorem’s TRANSITIVE axiom dependencies are inside an allowlist, and fail the build otherwise. The check a proof repository actually wants: #print axioms prints, it does not gate.

Two things every rule here now does, which none of them did before 0.7.0:

  • compiler diagnostics are forwarded even on a SUCCESSFUL compile. lean exits 0 on a sorry (it is a warning) and on #print axioms, and the driver only echoed output when the exit code was non-zero — so both vanished. A sorry was a green, silent build.
  • forbid_sorry (default True) makes an admitted goal a build failure.

lean_library/lean_olean_archive/lean_imported_library (added 0.4.0) are the cross-repo compiled-artifact seam: split a monolithic Lean library into modules, publish each module’s .olean tree as a per-(lean-version, os, arch) release tarball, and have downstreams consume the prebuilt oleans without recompiling. .olean is neither Lean-version- nor architecture-portable (it is a compacted heap image), so a consumer must pin the SAME lean-toolchain and select() the matching-platform artifact; Lean itself rejects a mismatched olean loudly at use.

lean_axiom_test

load("@rules_lean//lean:lean.bzl", "lean_axiom_test")

lean_axiom_test(name, deps, srcs, allowed_axioms, forbid_sorry, imports, theorems)

Fail the build unless every named theorem’s transitive axiom dependencies are inside allowed_axioms.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsCompiled Lean libraries holding the theorems (e.g. a lean_library). Name the modules to import via imports.List of labelsoptional[]
srcsLean sources to compile before auditing. Compiled in import-topological order, so list order is irrelevant (a glob() is fine). May be empty when the theorems come from deps.List of labelsoptional[]
allowed_axiomsThe allowlist. A theorem depending on anything outside it fails the build, naming the theorem and the offending axioms.

Defaults to Lean’s three standard axioms — propext, Classical.choice, Quot.sound — which is classical logic with quotient types. What that default EXCLUDES is the point: sorryAx (an admitted goal) and Lean.ofReduceBool (a native_decide, which trusts the compiler rather than the kernel).

Tighten it when a theorem is meant to be constructive: a proof that needs only [propext, Quot.sound] today and silently acquires Classical.choice tomorrow is a real change, and this is what reports it. An empty list allows NOTHING.
List of stringsoptional["propext", "Classical.choice", "Quot.sound"]
forbid_sorryIf True (the default), an admitted goal — sorry, or any tactic that elaborates to sorryAx — fails the build.

Lean reports sorry as a WARNING and exits 0, so before 0.7.0 an admitted theorem type-checked, the driver discarded the warning along with the rest of the compiler’s output, and the target went green. A proof that is not a proof is the one thing a Lean ruleset must not report as passing.

Set False for a development tree that is deliberately mid-proof. It does not suppress the warning — that is now always printed — only the failure.
BooleanoptionalTrue
importsLean modules the generated audit imports (e.g. ["Soma"]). Defaults to every module in srcs, which is what a single-library audit wants; required when the theorems come only from deps.List of stringsoptional[]
theoremsFully-qualified theorem names to audit (e.g. ["Soma.network_confluence"]). Each is resolved as a real constant, so a typo fails rather than passing vacuously. An empty list is an error for the same reason.List of stringsrequired

lean_binary

load("@rules_lean//lean:lean.bzl", "lean_binary")

lean_binary(name, deps, srcs, entry, forbid_sorry)

A runnable Lean executable: compiles srcs to an olean root and lean --runs the entry with runtime argv.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
deps-List of labelsoptional[]
srcs-List of labelsrequired
entryModule-path of the src whose main is the entry point.Stringrequired
forbid_sorryIf True (the default), an admitted goal — sorry, or any tactic that elaborates to sorryAx — fails the build.

Lean reports sorry as a WARNING and exits 0, so before 0.7.0 an admitted theorem type-checked, the driver discarded the warning along with the rest of the compiler’s output, and the target went green. A proof that is not a proof is the one thing a Lean ruleset must not report as passing.

Set False for a development tree that is deliberately mid-proof. It does not suppress the warning — that is now always printed — only the failure.
BooleanoptionalTrue

lean_emit

load("@rules_lean//lean:lean.bzl", "lean_emit")

lean_emit(name, deps, srcs, data, out, entry, forbid_sorry)

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
deps-List of labelsoptional[]
srcs-List of labelsrequired
dataNon-Lean files staged alongside srcs in the action’s work directory (NOT compiled). The Lean entry runs from that directory, so it can IO.FS.readFile them by their package-relative path. Typical use: fixture .dat / .txt / .json inputs the entry processes.List of labelsoptional[]
outThe emitted artifact (one file). Filename should reflect the artifact kind.Labelrequired
entryPath of the entry-point .lean file (relative to the package) defining main : IO Unit. Stdout is captured to out.Stringrequired
forbid_sorryIf True (the default), an admitted goal — sorry, or any tactic that elaborates to sorryAx — fails the build.

Lean reports sorry as a WARNING and exits 0, so before 0.7.0 an admitted theorem type-checked, the driver discarded the warning along with the rest of the compiler’s output, and the target went green. A proof that is not a proof is the one thing a Lean ruleset must not report as passing.

Set False for a development tree that is deliberately mid-proof. It does not suppress the warning — that is now always printed — only the failure.
BooleanoptionalTrue

lean_imported_library

load("@rules_lean//lean:lean.bzl", "lean_imported_library")

lean_imported_library(name, srcs, path_marker)

Expose an unpacked .olean release tarball as LeanInfo (no recompile).

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
srcsAll files of the unpacked .olean tree (typically @<archive_repo>//:all or a glob).List of labelsrequired
path_markerAnchor file inside the unpacked import root (the archive’s .lean_root). Its parent dir becomes the LEAN_PATH entry.Labelrequired

lean_library

load("@rules_lean//lean:lean.bzl", "lean_library")

lean_library(name, deps, srcs, forbid_sorry)

Compile .lean sources to a persistent .olean import-root tree and expose it as LeanInfo.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsCompiled Lean libraries this one imports. Same-top-namespace deps are staged into the compile root; disjoint ones are on LEAN_PATH. All propagate transitively in this library’s LeanInfo.List of labelsoptional[]
srcsAll .lean files in this library. Module path is derived from the file’s path relative to its own package. Compiled in import-topological order, so list order is irrelevant (a glob() is fine).List of labelsrequired
forbid_sorryIf True (the default), an admitted goal — sorry, or any tactic that elaborates to sorryAx — fails the build.

Lean reports sorry as a WARNING and exits 0, so before 0.7.0 an admitted theorem type-checked, the driver discarded the warning along with the rest of the compiler’s output, and the target went green. A proof that is not a proof is the one thing a Lean ruleset must not report as passing.

Set False for a development tree that is deliberately mid-proof. It does not suppress the warning — that is now always printed — only the failure.
BooleanoptionalTrue

lean_main_test

load("@rules_lean//lean:lean.bzl", "lean_main_test")

lean_main_test(name, deps, srcs, data, entry, forbid_sorry)

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
deps-List of labelsoptional[]
srcsAll .lean files needed to compile the entry. Compiled in import-topological order, so list order is irrelevant (a glob() is fine).List of labelsrequired
dataNon-Lean files staged at their workspace-relative path in the action’s work directory. The Lean entry runs from that directory, so it can IO.FS.readFile them.List of labelsoptional[]
entryPath of the entry-point .lean file (relative to the package) defining main : IO UInt32 (test result = exit code).Stringrequired
forbid_sorryIf True (the default), an admitted goal — sorry, or any tactic that elaborates to sorryAx — fails the build.

Lean reports sorry as a WARNING and exits 0, so before 0.7.0 an admitted theorem type-checked, the driver discarded the warning along with the rest of the compiler’s output, and the target went green. A proof that is not a proof is the one thing a Lean ruleset must not report as passing.

Set False for a development tree that is deliberately mid-proof. It does not suppress the warning — that is now always printed — only the failure.
BooleanoptionalTrue

lean_olean_archive

load("@rules_lean//lean:lean.bzl", "lean_olean_archive")

lean_olean_archive(name, out, library)

Bundle a lean_library’s .olean import-root tree into a deployable tarball.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
outOutput tarball name (default <name>.tar.gz).Stringoptional""
libraryThe lean_library whose own .olean tree is archived.Labelrequired

lean_prebuilt_library

load("@rules_lean//lean:lean.bzl", "lean_prebuilt_library")

lean_prebuilt_library(name, srcs, path_marker)

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
srcsAll files in the prebuilt-olean tree (typically glob(["lib/**"])).List of labelsrequired
path_markerAnchor file inside the import-root directory. The marker’s parent is the LEAN_PATH entry.Labelrequired

lean_test

load("@rules_lean//lean:lean.bzl", "lean_test")

lean_test(name, deps, srcs, entry, forbid_sorry)

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsPrebuilt Lean libraries. Same-top-namespace deps are staged into the compile root; disjoint ones are on LEAN_PATH.List of labelsoptional[]
srcsAll .lean files in the proof tree. Module path is derived from the file’s path relative to this BUILD.bazel’s package. Compiled in import-topological order, so list order is irrelevant — glob(["**/*.lean"]) is fine.List of labelsrequired
entryPath of the entry-point .lean file relative to the package.Stringrequired
forbid_sorryIf True (the default), an admitted goal — sorry, or any tactic that elaborates to sorryAx — fails the build.

Lean reports sorry as a WARNING and exits 0, so before 0.7.0 an admitted theorem type-checked, the driver discarded the warning along with the rest of the compiler’s output, and the target went green. A proof that is not a proof is the one thing a Lean ruleset must not report as passing.

Set False for a development tree that is deliberately mid-proof. It does not suppress the warning — that is now always printed — only the failure.
BooleanoptionalTrue

lean_toolchain

load("@rules_lean//lean:lean.bzl", "lean_toolchain")

lean_toolchain(name, lean, runtime)

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
lean-Labelrequired
runtime-Labelrequired

LeanInfo

load("@rules_lean//lean:lean.bzl", "LeanInfo")

LeanInfo(markers, files)

A Lean library: a directory of importable .olean files, exposed via a marker file whose parent directory is the LEAN_PATH entry.

FIELDS

NameDescription
markersdepset[File]: each marker’s parent directory IS a LEAN_PATH entry.
filesdepset[File]: all .olean files (and the marker) needed when this lib is consumed.

LeanToolchainInfo

load("@rules_lean//lean:lean.bzl", "LeanToolchainInfo")

LeanToolchainInfo(lean, runtime)

Lean 4 compiler binary + runtime tree.

FIELDS

NameDescription
leanFile: the lean compiler binary (executable).
runtimedepset[File]: stdlib oleans, shared libs, etc.

lean_regen_test

load("@rules_lean//lean:lean.bzl", "lean_regen_test")

lean_regen_test(name, srcs, entry, expected, out, deps, data, tags)

Assert a committed file matches the current lean_emit output.

PARAMETERS

NameDescriptionDefault Value
nametarget name for the generated diff_test (e.g. regen_int_arith). The helper lean_emit is named <name>_emit.none
srcslist of .lean source labels needed to compile the entry. Compiled in import-topological order, so list order is irrelevant (a glob() is fine). Must include the entry.none
entrypath of the entry-point .lean file (relative to the rule’s package) defining main : IO Unit. Stdout is captured.none
expectedBazel label of the committed file the lean_emit output is diffed against.none
outoptional filename for the emitted artifact (defaults to <name>_emit.out).None
depsoptional list of LeanInfo-providing deps for prebuilt olean closures (passed through to lean_emit).None
dataoptional runtime files the entry point reads while it executes (passed through to lean_emit). Paths resolve relative to the emit action’s working directory.None
tagsoptional tags propagated to the generated diff_test target only.None

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

Dependencies#

rules_lean in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_shell0.6.1devstardoc0.7.2dev

Used by (10 in the registry)

Versions#

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

VersionIntegrity (sha256)Source archive
0.6.2 latest OESUpfl+zObTbpIP… tag archive ↗
0.6.1 9EHvuaVRCqAng60h… tag archive ↗
0.6.0 wLHXJPEmsBIy4GbO… tag archive ↗
0.5.5 mYAi/q2LeZS3qMZ4… tag archive ↗
0.5.4 zysuiLaHuEh/B+2O… tag archive ↗
0.5.3 U97l5mnM+nm0X77j… tag archive ↗
0.5.2 mYkyTxH7zEWTee6u… tag archive ↗
0.5.1 L6TqRJlTF6WVXJn6… tag archive ↗
0.5.0 uMBcfmuycyE873Q0… tag archive ↗
0.3.9 qKA+T7riKQFNaLpQ… tag archive ↗
0.3.8 BqQpA98mVRUOogP6… tag archive ↗
0.3.7 QkkKRq7bynhEUSYz… tag archive ↗
0.3.6 LXl5Cp/PbYSbjKby… tag archive ↗
0.3.5 lyEMMFe7YdN+nF8z… tag archive ↗
0.3.4 vYH80yE9giekbYXh… tag archive ↗
0.3.3 h6VVmRrtwKwzeTa6… tag archive ↗
0.3.2 T/87d8lIhjDL3JXN… tag archive ↗
0.3.1 bgVOPHOz2LbsrzoF… tag archive ↗
0.3.0 rLb+F+rDrpP39JM2… tag archive ↗
0.3.0-rc1 DT3FpRRcLZH8z+kt… tag archive ↗
0.2.2 X3f1MU150YqabKmG… tag archive ↗
0.2.1 mCvQk2T4RtTyYnQ3… tag archive ↗
0.2.0 UoSFQMzYzBpfCnwk… tag archive ↗
0.1.0 DF2Ry/QrmSeisrdy… tag archive ↗

Changelog#

All notable changes to rules_lean. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.

0.7.0 — a sorry was a GREEN, SILENT build; and axioms are now a gate

Two failures in the same place, found by trying to put citizen-sh/soma’s six confluence theorems behind a Bazel target. The proofs themselves compiled first try — lean_test over Lean 4 core with no Mathlib worked exactly as documented, on darwin/arm64 and linux. What did not work is everything that makes a green check mean something.

A sorry passed. lean reports an admitted goal as a WARNING and exits 0. topo_compile echoed the compiler’s output only when the exit code was non-zero — so the warning was discarded along with the exit code that said nothing was wrong, and the target went green. Measured, not inferred: a theorem … := by sorry appended to a real proof tree, rebuilt, PASSED, zero output. The control (a genuine type error in the same file) went red, so the harness worked; it just could not see this.

Two things change. The driver now forwards compiler diagnostics on SUCCESS as well as failure — which also un-hides #print axioms, whose output was going into the same void, so an Audit.lean full of them looked like it had run and found nothing. And forbid_sorry (default True) makes an admitted goal a build failure, on every rule that compiles Lean.

Behavior change. A tree containing a sorry that built green before now fails. That is the point — but it is a real break, so: forbid_sorry = False on the target restores the old behavior, and does not re-hide the warning.

The gate matches the compiler’s message text, which is exactly as brittle as it sounds, so //examples/axiom_audit:sorry_is_rejected is a NEGATIVE test in CI. If upstream rewords the warning, that test goes green and the job fails on it rather than the check quietly becoming a no-op. (Byte-identical in 4.29.1 and 4.32.2, both checked in-tree.)

#print axioms prints; it does not gate. A proof that silently acquires an axiom — a sorry in a lemma three imports down, a native_decide that swaps the kernel for the compiler — keeps printing a line nobody reads. The new lean_axiom_test fails the build instead:

lean_axiom_test(
    name = "axioms_test",
    srcs = glob(["**/*.lean"]),
    theorems = ["Soma.network_confluence", "Soma.run_perm_invariant"],
    # allowed_axioms defaults to [propext, Classical.choice, Quot.sound]
)

It generates a Lean module and checks at ELABORATION time with Lean.collectAxioms — the same API behind #print axioms, so it agrees with the hand audit by construction rather than by re-implementing it, and it is transitive. RulesLean.Internal.AxiomDeps.declaredAxioms is NOT that: it is a header-only scan of axioms a module declares directly, and its own docstring says the transitive version “lands once there’s a concrete consumer pushing on the shape”. soma was the consumer.

The default allowlist is Lean’s three standard axioms. What it excludes is the point: sorryAx and Lean.ofReduceBool. Tightening it is the interesting direction — a theorem that needs only [propext, Quot.sound] today and picks up Classical.choice tomorrow is a real change, and this reports it. Successful audits are silent; failed audits name the theorem, its axiom dependencies, the allowlist, and the disallowed axioms.

Verified by mutation on soma’s actual proofs, not by assumption. soma’s Audit.lean names eight theorems; with the three-axiom allowlist all eight pass. Drop Classical.choice and five fail by name while runBarriered_perm_invariant, network_confluence_barriered and drive_reaches still pass, because those genuinely need only two — which is exactly what soma’s README claims for them. A gate that cannot tell those apart is not a gate.

//examples/axiom_audit carries four positive targets and three negative ones (*_is_rejected, manual-tagged, run by CI with the exit code asserted non-zero), plus a control — the same sorry file with forbid_sorry = False, which must PASS, so a red negative is attributable to the gate and not to a broken fixture.

Lean v4.32.2 is pinned, all four platforms. It was not in known_lean_versions.bzl, so any workspace on a modern toolchain downloaded the compiler UNVERIFIED behind a print() warning — including soma, whose lean-toolchain says v4.32.2. Hashes are of the release ASSETS (immutable), not /archive/refs/tags/ tarballs.

0.6.2 — two deps can share a top-level namespace

Lean resolves a module name in the first LEAN_PATH root that owns its top-level directory and does not fall through to later roots. _dep_manifest_lines handled one consequence of that — a dep colliding with the consumer’s own namespace gets staged into the compile root — but not the other: two deps colliding with each other.

Two published modules both rooted at Pg/ therefore could not both be consumed, unless the consumer happened to have Pg/ sources of its own. The failure is silent and misdirected — the compile dies naming a path inside the wrong repository:

App/Main.lean:4:0: error: object file
  '.../pg_catalog_lib/Pg/Query/Top.olean' of module Pg.Query.Top does not exist

Pg/Query/Top.olean is in pg_query’s root; Lean looked only in pg_catalog’s, because that root owns Pg/ and it stopped there.

Now a top-level namespace owned by more than one dep root is staged, exactly as one colliding with the consumer already was. Namespaces owned by a single dep and untouched by the consumer still go on leanpath with no copy, so the common case is unchanged. The decision is per-file, so a dep whose namespaces are partly contested still contributes its uncontested ones via leanpath.

Why it mattered. This blocked the whole point of publishing compiled oleans for any consumer whose sources are rooted elsewhere. Concretely: leangres ships pgcatalog and pgquery, both rooted at Pg/, and their real consumer is rooted at Aion/ — so it could not adopt the compiled artifacts at all and had to keep recompiling from source.

//examples/shared_namespace is the regression test, and it is in CI. Verified by deliberate break: reverted to the old _dep_manifest_lines and confirmed the test fails with the error above, then restored.

0.6.1 — lean_olean_archive builds on linux, and is byte-reproducible

lean_olean_archive tarred the import root directly with tar -czhf. The import root is a symlink farm into bazel-out, so -h reads THROUGH the links while bazel may still be materialising them, and GNU tar treats that as fatal:

tar: ./Aion/Db/EntityType/EntityTypeFieldPredicates.olean:
     file changed as we read it

GNU tar exits 1 on that warning; BSD tar does not. The rule therefore worked on macOS and failed on every linux/RBE build — invisible to local verification, and it took an RBE worker log to see it (observed on aion/sql, green on the same commit on darwin). Downstream this was not a cosmetic failure: the olean publish job was removed from aion’s CI because of it, which is what has been blocking the cross-repo compiled-olean seam this rule exists to provide.

The rule now stages the import root into a private scratch dir with cp -RL, then tars that without -h. tar reads a tree nothing else is writing, and cp does not fail on concurrent mtime churn the way tar does, so the race is removed rather than narrowed.

While here: the tarball ships as a release asset that consumers pin by sha256, so it is now reproducible. Three things had to be pinned, not one — entry order (an LC_ALL=C-sorted list via -T -, not readdir order), per-entry mtimes (touch to a fixed date, because cp stamps the staged copies with the current time), and the gzip header timestamp (gzip -n). gzip -n on its own is not enough and the archive stayed non-reproducible with only it; that was caught by building the same tree twice a second apart and diffing.

uid/gid/uname/gname are still taken from the builder, so this is reproducible for a given builder — a CI runner rebuilding the same commit gets identical bytes — not across accounts.

And CI now actually runs the rule. //examples/olean_roundtrip has covered lean_olean_archive since 0.4.0, but nothing ever executed it: the fast gate is //docs/... only, and the two Lean jobs go through elan/lake rather than the Bazel rule. The rule was broken on linux for a week behind six green checks, none of which touched it. There is now an olean_archive PR gate on ubuntu — ubuntu-only on purpose, since the failure mode is GNU tar exiting 1 where BSD tar warns, so macOS cannot fail and re-proving it there buys nothing.

The round-trip assertion was also weaker than it looked: it grepped the tar listing for the olean’s path. Drop the -L from cp -RL and the archive still contains an entry at that path — a dangling symlink into bazel-out, useless to consumers — and the grep passed. It now asserts the entry is a regular file with non-zero bytes, verified by building both the correct and the non-dereferenced archive and confirming the old check accepted the broken one.

No API change; out and the produced tarball layout are unchanged.

0.6.0 — the imports manifest is opt-in; lake_workspace stops building a CLI it never runs

Every lake_workspace materialization compiled the RulesLean oleanImports CLI with the consumer’s Lean toolchain, unconditionally. Two things were wrong with that, and both are now fixed.

The dep-free path built the CLI and provably never invoked it. When the lakefile declares no requires, _lake_workspace_impl short-circuits — and the manifest generator it calls with an empty package list writes "" and returns before it would reach the binary. So the compile was pure cost. Observed on one darwin output base, three times over: ruleslean_lib/ at 149 MB next to a lake_imports_manifest.tsv of 0 bytes, in the rules_lang, rules_postgres and rules_spec lake workspaces.

And even on the dep-ful path, nothing reads the manifest. It is an introspection aid — “what does olean X import?” — not an input to compiling Lean. So it is now behind emit_imports_manifest, default False:

lake.workspace(
    name = "lake_deps",
    emit_imports_manifest = True,   # only if you actually consume the TSV
    ...
)

@<ws>//:lake_imports_manifest and the .tsv are still declared either way — empty when off — so no consumer BUILD file has to branch on the attr. Turning it back on restores the previous contents exactly.

_build_ruleslean_library’s timeout goes 600s → 3600s, and its docstring stops understating the cost. It claimed “~3-5s cold”. Measured here on darwin/arm64, Lean v4.30.0-rc2, fresh output base, uncontended: ~9s wall — but a 149 MB .lake/build/, per lake_workspace. The clock was roughly right; the footprint is the cost, and it is duplicated across every workspace in an output base. A separate investigation measured 130.9s / 237.6s / 497.5s for the same step on darwin, which was not reproduced here — so the wall time is environment-dependent somewhere between ~9s and several minutes, and nobody has measured it on linux. At ~9s the 600s cap is nowhere near binding; the raise is insurance for the slow end (a repository-rule timeout is a hard analysis failure, not a slow build), on a step that now rarely runs at all.

Measured end to end, on a throwaway consumer that bazel_deps on a module with a dep-free lake workspace and builds one cc_library (darwin/arm64):

before 2.6 GB of external/ — 2.5 GB Lean toolchain + 149 MB ruleslean_lib/ after 2.3 MB of external/ — no Lean repos materialized at all

with, in the before case, a lake_imports_manifest.tsv of 0 bytes sitting next to the 149 MB it cost to produce.

0.5.5 — resolve deps from the pinned manifest, never lake update

Backfilled entry; 0.5.5 shipped to the registry without one.

_lake_workspace_impl materialized deps with lake update, which regenerates the lake-manifest.json that was just pinned and fires every dep’s post_update hook. mathlib’s hook runs a hardcoded, unfiltered lake exe cache get, pulling all ~7900 oleans before 0.5.4’s tree-shaken cache_roots fetch got a say — which left cache_roots shipped-but-inert in 0.5.4. Now a non-mutating lake env true resolves from the committed manifest, no hook fires, and cache_roots actually takes effect. Verify by BYTES on disk, not by the “Already decompressed N” log, which reports the tree-shaken count either way.

0.5.4 — tree-shake mathlib’s olean download (cache_roots)

lake.workspace(cache_roots = [...]) restricts mathlib’s lake exe cache get to the given root modules plus their transitive closure, instead of fetching all of mathlib. mathlib’s cache CLI already supports this (get [ARGS]filterByRootModules); rules_lean simply never passed the args. The fetch stays sound — you cannot under-fetch a module you import.

Measured against mathlib @ v4.30.0-rc2 with a Lean→SQL emitter’s 6 roots (Data.Fintype.Basic, Data.Fin.Basic, Data.List.Basic, Data.List.Infix, Order.Basic, Order.BoundedOrder.Basic):

full fetch 8297 files (~2 GB) 6-root closure 595 files (52 MB) — 93% fewer files

Empty cache_roots (the default) keeps the fetch-everything behaviour, so existing workspaces are unaffected.

0.5.3 — configurable prebuilt-olean cache

  • lake.workspace can now fetch a package’s prebuilt oleans from a consumer-configurable cache instead of source-building it (e.g. cslib, which Reservoir doesn’t serve — a ~2.6k-job compile on every cold output base). Declare olean_cache_packages = ["cslib"]; the cache base is set via the olean_cache tag attr (MODULE) or the LEAN_OLEAN_CACHE repo_env (--repo_env=... in .bazelrc — the repo_env wins). Never hardcoded/public. Artifact path: <base>/<pkg>-<rev12>-<leanver>-<platform>.tar.gz (the package’s .lake/build tree). No base configured → source-build fallback (allow_source_build).
  • Each resolved package now also exposes a :<pkg>_build_tree filegroup, so producing the cache tarball is a hermetic pkg_tar over it (no manual host tar / AppleDouble cruft).
  • Validated: with the cache set, cslib is fetched + unpacked (0 source-build jobs), green.

0.5.2 — shared Lean toolchain (dedup)

  • The lake module extension now extracts the Lean toolchain once per version into a shared lean_dist repo; every lake.workspace symlinks it instead of extracting its own ~2.5G copy. Previously N workspaces (e.g. a project plus the lake workspaces of rules_lang / rules_postgres / rules_spec) each carried a full toolchain — 4× the same toolchain ≈ 10G per output base, the dominant cause of multi-GB Lean checkouts / CI ENOSPC. Now 1× per version.
  • Backward-compatible: @<ws>//:lean_toolchain_def is still a real toolchain() (so register_toolchains(...) is unchanged) — it now points at the shared lean_dist toolchain; @<ws>//:lean_toolchain is an alias to it. The per-package lean_prebuilt_library targets and the imports manifest are unchanged.

0.4.0 — compiled libraries + cross-repo olean artifacts

  • New lean_library: compiles .lean sources to a persistent .olean import-root tree (build outputs) and exposes it as LeanInfo, so one module can be a compiled dependency of another (no source re-share, no recompile). DefaultInfo carries the library’s own tree; LeanInfo carries the transitive closure (own + deps).
  • New lean_olean_archive: bundles a lean_library’s own .olean tree into a tarball — the deployable cross-repo release artifact.
  • New lean_imported_library: exposes an unpacked .olean tarball (e.g. from an http_archive of a release asset) as LeanInfo with no recompile — the consume side. Shares the lean_prebuilt_library implementation.
  • These three form the cross-repo compiled-olean seam (split a monolithic Lean library into modules that publish/consume prebuilt oleans). .olean is neither Lean-version- nor architecture-portable, so artifacts are built per-(lean-version, os, arch) and consumers pin the matching toolchain; Lean rejects a mismatched olean loudly at use.
  • Round-trip example under examples/olean_roundtrip/.
  • Cross-namespace deps. A lean_library dep that shares the consumer’s top-level namespace (e.g. two libs both under Aion/) is staged into the single compile root, since Lean commits to the first LEAN_PATH root owning a namespace and won’t fall through to siblings. Disjoint deps (Mathlib, …) stay on LEAN_PATH, uncopied. This makes lean_librarylean_library deps within one namespace work (the basis for splitting a monolith in place).
  • Shell-free compile. All four rules (lean_library, lean_test, lean_emit, lean_main_test) now drive the compiler through a self-contained Lean topo-compile driver (lean/private/topo_compile.lean, invoked lean --run … via ctx.actions.run) instead of a run_shell tsort pipeline — staging/copying uses native IO.FS; the only subprocess is lean. lean_test/lean_main_test now type-check / run at build time (a failure fails the build); their test executable is a trivial pass.

0.3.9 — import-topological compile order (glob()-safe srcs)

  • lean_test, lean_emit, and lean_main_test now compile their srcs in import-topological order instead of literal list order. Previously, Lean’s requirement that a module’s imports be compiled to .olean first meant srcs had to be hand-ordered with dependencies before dependents — and a natural glob(["**/*.lean"]) would fail, because a root file like Trading.lean sorts before Trading/Fx/Basic.lean (. < /) yet imports it. Now the generated runner derives the order at execution time: it parses each staged file’s import lines, keeps edges to modules that are themselves in srcs, and tsorts the graph. srcs = glob([...]) now Just Works; explicit ordered lists keep working unchanged (any valid manual order is already a valid topological order).
  • Implementation: a portable bash helper (__lean_topo_compile, shared via _topo_compile_block) using only grep/sed/cut/tsort/mktemp — no bash-4 associative arrays, so it runs on macOS’s stock bash 3.2. Out-of-srcs imports (Mathlib, dep packages) are ignored; genuine import cycles still fail the build (Lean rejects them downstream).

0.3.5 — lean_main_test rule

  • New lean_main_test(name, srcs, entry, deps, data) rule in lean/lean.bzl. Compiles + runs a Lean entry whose main : IO UInt32 returns the test result via its exit code (0 = pass, non-zero = fail). No expected-output diff required — use when the Lean script self-validates (round-trip stability, structural equivalence) and you’d otherwise need a committed expected.txt fixture just to flag drift. Accepts the same deps (LeanInfo) + data (workspace-relative staging) attrs as lean_emit / lean_regen_test.
  • New smoke examples/regen_smoke/regen_smoke_exit runs ExitZero.lean (pure 0) to exercise the happy path. A companion ExitOne.lean (pure 1) is committed for manual negative testing.

0.3.4 — lean_emit.data accepts external-repo files

  • lean_emit.data now stages files at their workspace-relative path (e.g. examples/regen_smoke/fixture.txt) instead of the package- relative path the 0.3.3 release used. Externally-sourced data (@some_repo//path:file) is staged at path/file — the ..//<canon> prefix in bazel’s external-repo short_path is stripped. This lets consumers pull fixtures directly from upstream repos (e.g. @postgres_src//:src/include/catalog/pg_namespace.dat) instead of vendoring them.
  • Smoke updated: examples/regen_smoke/EchoFixture.lean reads the full workspace-relative path.

0.3.3 — lean_emit.data attr

  • lean_emit (and lean_regen_test) gain a data attr — non-Lean fixture files staged alongside srcs in the action’s work directory without being compiled. The entry runs from that work dir, so it can IO.FS.readFile them by their package-relative path. Typical use: .dat / .txt / .json inputs the entry parses. Enabled rules_postgres’ Lean-native Pg.Catalog.Dat round-trip gate against the vendored pg_namespace.dat sample.
  • New smoke examples/regen_smoke/regen_smoke_data exercises the attr end-to-end: a Lean main reads fixture.txt and echoes it; the diff_test verifies the captured stdout matches the same fixture.txt (proving the data file is reachable from the Lean entry’s relative-path readFile).

0.3.2 — lean_regen_test macro

  • New lean_regen_test(name, srcs, entry, expected, ...) macro in lean/lean.bzl. Wraps lean_emit + skylib diff_test to assert a committed artifact matches the current Lean-emit output for a given Lean main. Captures the “Lean spec is source-of-truth; emitted X was generated from it” pattern that rules_postgres’ Pg.Ir cluster Gate 1 was building on top of lean_emit + diff_test by hand.
  • Smoke test under examples/regen_smoke/ exercises the macro end-to-end against a tiny Hello.lean and a committed expected.txt.

0.3.1 — External-repo Lean sources

  • _module_path and _lean_test_impl now handle external-repo source layouts (../<repo>+/<package>/<file> short_paths). Lets lean_library and lean_test targets in a consumer module reference Lean sources from a bazel_dep repo without copying the files into the consumer’s tree. Used by rules_postgres’ lean/Pg/Ir/Emit/ modules when consumed through the registry rather than through a local_path_override.

0.3.0 — RulesLean Lean library + lake_imports_manifest

  • Promote v0.3.0-rc1 and pin to Lean v4.30.0-rc2 for cslib compatibility.
  • Add RulesLean.Internal.Closure (transitive olean closure computed from the Lake manifest) and RulesLean.Internal.AxiomDeps (declaredAxioms + isAxiom, Internal v0.1).
  • CI: add a ruleslean_library matrix job so the in-tree Lean library is built + tested on every PR.
  • Untrack .vscode/ and notebook scratch artifacts; tighten .gitignore.

0.3.0-rc1 — RulesLean scaffold + manifest tooling

  • Introduce the RulesLean Lean library under lean/lib/ (Olean + Lake) and wire it through Bazel.
  • Add the lake_imports_manifest target: workspace API, exportedConstants + containsConstant, and the Internal namespace convention with namespacePackageIndex.
  • Add tools/reservoir_manifest.py — a stdlib-only Reservoir index fetcher.
  • Update the install snippet to point at fastverk/bazel-registry.

0.2.2 — Un-dev bazel_skylib

  • Promote bazel_skylib out of dev_dependency so downstream consumers can actually load() lean/BUILD.bazel without re-declaring it.

0.2.1 — README, license, CI, smoke test

  • Bump module version to 0.2.1.
  • Add README.md, MIT LICENSE, and the PR-gate CI workflow.
  • Add a Batteries-only lake_workspace smoke test.
  • Apply buildifier formatting fixes across the tree.

0.2.0 — Generalized Lake integration + stardoc

  • Generalize the Lake integration so lake_workspace works for arbitrary Lake projects instead of being hard-coded to a single layout.
  • Add stardoc generation for the public rules.

0.1.0 — Initial release

  • First public cut of rules_lean: lean_test, lean_emit, lean_prebuilt_library, lean_toolchain, and the initial lake_workspace repository rule + lake module extension reusing Mathlib’s Reservoir cache.

← All modules