Bazel rules for building LaTeX documents with the Tectonic typesetting engine. Bzlmod-only, toolchain-based, hermetic, with auto-managed package caching and an Overleaf-style live preview.
load("@rules_latex//latex:defs.bzl", "latex_document")
latex_document(
name = "cv",
main = "cv.tex",
srcs = ["cv.tex"],
)That's it. No package enumeration, no checked-in tarballs, no system
LaTeX install. bazel build //:cv works on a fresh machine.
bazel_latex |
rules_latex (this repo) |
|
|---|---|---|
| Backend | TeX Live (full distribution) | Tectonic (XeTeX + bundle) |
| Package management | Explicit Bazel labels per .sty |
Implicit, by Tectonic at compile time |
| Module system | WORKSPACE + Bzlmod | Bzlmod-only |
Bibliography (biblatex/biber) |
System install, manual flags | Vendored biber toolchain |
| Newer-than-bundle CTAN packages | Manual vendoring | ctan_packages = [...] |
| Reproducible builds | Possible, manual | reproducible = True attr |
| First-build cost | Many MB of TeX Live as needed | ~20 MB tectonic + 10–100 MB cache snapshot per document |
The first time you build, rules_latex runs Tectonic once online to
populate a per-document cache (~10–100 MB depending on the document),
then runs the actual compile offline against it. Bazel's action cache
makes the prime a one-time cost; subsequent builds (including across
CI machines via the remote cache) skip it entirely. See the
caching
docs page for the user-facing summary and DESIGN.md for the
architectural rationale.
In your MODULE.bazel:
bazel_dep(name = "rules_latex", version = "0.5.0")
tectonic = use_extension("@rules_latex//latex/toolchain:extensions.bzl", "tectonic")
tectonic.toolchain()
use_repo(tectonic, "rules_latex_tectonic_toolchains")
register_toolchains("@rules_latex_tectonic_toolchains//:all")In a BUILD.bazel:
load(
"@rules_latex//latex:defs.bzl",
"latex_document",
"latex_library",
"latex_test",
)
latex_library(
name = "preamble",
srcs = ["preamble.tex"],
)
latex_document(
name = "cv",
main = "cv.tex",
srcs = ["cv.tex"],
deps = [":preamble"],
# biber = True # for biblatex documents
# ctan_packages = ["..."] # for packages not in the bundle
# reproducible = True # byte-identical PDF across builds
# synctex = True # PDF clicks copy <file>:<line> to clipboard
)
# Catch regressions: fails CI if cv.tex stops compiling cleanly.
latex_test(
name = "cv_compiles",
main = "cv.tex",
srcs = ["cv.tex"],
deps = [":preamble"],
)bazel build //:cv # first build: ~30-90s (online prime + compile)
bazel build //:cv # subsequent builds: ~1-5s (action-cache hit)
bazel test //:cv_compilesFor more, see the examples directory — letter, CV, paper, thesis, beamer slides, a CTAN-overlay paper, and a shared-library monorepo (a preamble + bibliography shared across multiple documents) — and the full user guide.
| Rule | Purpose |
|---|---|
latex_document |
Compile a .tex file (plus its transitive sources) into a PDF (or other tectonic-supported format). |
latex_library |
Group reusable LaTeX source files (preambles, custom style/class files) that other targets depend on. |
latex_pkg |
Group non-LaTeX resources (images, fonts, .bib files) that documents may need. |
latex_test |
Compile a document under bazel test and assert on patterns in the tectonic log file (e.g. fail on LaTeX Error:). |
latex_cache_snapshot |
bazel run-able command that captures a small, per-document offline cache snapshot for hermetic builds. |
latex_live |
bazel run-able live-preview loop: watches the document's sources, rebuilds via bazel build on every save, and serves the result as a localhost HTTP page rendered with PDF.js — Overleaf-style in-browser preview with auto-refresh, search, outline sidebar, and a build-log drawer. |
All six are loaded from @rules_latex//latex:defs.bzl.
latex_live is a bazel run-able watcher that rebuilds your
document on every save and pushes the result to a localhost HTTP
page rendered with PDF.js.
bazel run //:cv_live # http://127.0.0.1:8765/- PDF.js, vendored — no CDN, works on disconnected networks, doesn't leak the document to a third-party host.
- WebSocket push transport — after each successful rebuild the
server pushes only the changed PDF chunks (content-addressed by
SHA-256) to the connected tab in a single duplex burst. SSE
remains at
/eventsas a transparent fallback for clients that can't upgrade. Scroll position, zoom level, and current page are preserved across reloads, so a 90-page thesis doesn't snap back to page 1 on every save. - Page navigation, zoom, fit modes, fullscreen, download — a proper PDF-viewer chrome with keyboard shortcuts (PageUp/Down, Home/End, +/-/0, w/p, f, g, t, Ctrl+F, …).
- In-document search —
Ctrl+Fopens a find bar that highlights matches across the document with next/prev nav. - Selectable text — PDF.js text-layer overlay means you can select and copy text from the preview natively.
- Outline sidebar — when the document has hyperref bookmarks
(any
\section/\subsection/\chapter), a collapsible left sidebar shows them as clickable nav, with the current section highlighted as you scroll. - Build-log drawer — a collapsible bottom panel exposes the
latest
bazel buildstdout+stderr. Auto-expands on the first failed build of a session so the error is one click (or zero) away from where you saw the failure. - Status pill —
✓ 1.42 s · build #5 · 12 s agowith a live- ticking "Xs ago" suffix; footer shows current git branch + dirty marker so you know which state you're previewing. - Light / dark / auto theme — full palette driven by CSS
variables;
autofollowsprefers-color-scheme.tcycles. - SyncTeX source-location lookup — when the document declares
synctex = True, clicking a glyph in the preview resolves the matching.texfile + line via the generated.synctex.gzindex and copies<file>:<line>to the clipboard. The browser can't drive your editor, so the click doesn't jump — it hands you the location to paste wherever opens files for you (vim's:e, the VS Code Quick Open prompt,code -g, etc.). - SyncTeX forward-sync (editor → PDF) —
POST /sync/forwardlets an editor plugin point the preview at a(file, line)location; the matching glyph scrolls into view and gets a brief highlight flash. This is the half of SyncTeX where the jump is real, because the editor is the one driving it. - DPI-aware rendering — canvases render at
devicePixelRatio, so the preview is crisp on Retina / 4K displays. - Configurable port and debouncer —
port,poll_interval_ms,debounce_ms, anddebounce_max_msare all rule attributes for noisy filesystems or shared dev hosts.
Earlier releases (v0.4.x and below) also shipped a
latex_serverule that opened the document in the system PDF viewer. It was removed in v0.6.0 because the two viewers most users would default to on macOS — Preview and Acrobat — don't reliably auto-reload when the PDF changes on disk, leaving users with a stale preview and no clear hint why. See the migration note in CHANGELOG.
latex_document(
name = "paper",
main = "paper.tex",
srcs = glob(["paper.tex", "references.bib"]),
biber = True,
)A vendored biber binary (2.21, matching the bundle's biblatex 3.21) is
staged onto PATH at compile time — including on Linux arm64, which is
now covered by a prebuilt binary. Modern citation styles
(biblatex-apa, biblatex-chicago, biblatex-ieee, …) work out of
the box. If you're on an unsupported platform, set
biber_strategy = "system" to fall back to a distro-installed binary.
See the
bibliography guide.
latex_document(
name = "thesis",
main = "thesis.tex",
srcs = ["thesis.tex", "references.bib"],
ctan_packages = ["biblatex-apa"], # extension style, not in the bundle
biber = True,
)rules_latex ships a self-hosted TeX Live 2026 bundle, so the core
distribution and the standard biblatex styles are current. The
ctan_packages attribute fetches anything outside it — niche contrib
packages, bleeding-edge releases — directly from mirrors.ctan.org in
TDS format and folds them into the implicit cache pipeline. No extra
targets, no manual vendoring.
Because the bundle ships biblatex 3.21 + biber 2.21, modern extension
styles (biblatex-apa 9.x, biblatex-chicago, biblatex-ieee, …)
work without any toolchain opt-in — just add the style package to
ctan_packages. See the
bibliography guide
for the version-coupling discussion.
For most documents you don't need this attribute: the bundle covers
~95% of real-world LaTeX. When a fetched package transitively
requires another package outside the bundle, the populate-cache step
surfaces a targeted hint naming the missing file and which of your existing
ctan_packages referenced it — so the next iteration is one
attribute edit away. See the CTAN packages user
guide
for when to reach for it (and when not to).
latex_document(
name = "cv",
main = "cv.tex",
srcs = ["cv.tex"],
reproducible = True,
)Combines SOURCE_DATE_EPOCH=0 with Tectonic's -Z deterministic-mode
to produce byte-identical output across clean builds. CI verifies this
on every push.
For fully air-gapped CI, latex_cache_snapshot captures a tiny
per-document cache tarball that you check into the repo and pass
as cache = "..." — see the hermetic builds
guide.
Most users won't need this: the default implicit pipeline already
caches the online prime through Bazel's action cache.
| Platform | tectonic | biber | bundle |
|---|---|---|---|
| Linux x86_64 | ✅ musl | ✅ glibc | ✅ |
| Linux aarch64 | ✅ musl | ✅ prebuilt | ✅ |
| macOS x86_64 | ✅ | ✅ universal binary | ✅ |
| macOS aarch64 | ✅ | ✅ universal binary | ✅ |
| Windows x86_64 | ✅ MSVC | ✅ | ✅ |
biber 2.21 is prebuilt for every supported platform, including Linux
arm64. biber_strategy = "system" remains as an escape hatch for
unsupported platforms.
- Bazel: 8.0+ (Bzlmod-only). CI tests against 8.0.0, 8.7.0, and 9.1.0 on every push and PR.
- Tectonic: 0.16.9 (pinned)
- biber / biblatex: 2.21 / 3.21 (paired by control-file format)
- TeX Live: 2026 (self-hosted bundle — see the roadmap)
- User guide — generated from Stardoc, with the Material theme
DESIGN.md— architectural rationale, the v0.x → v1.0 roadmap, and open questionsCHANGELOG.mdexamples/— runnable examples (hello, letter, CV, paper, ctan_paper, thesis, beamer, and a shared-library monorepo)
Apache License 2.0. See LICENSE.
