Changelog¶
All notable changes to rules_latex are documented here. This project follows
Semantic Versioning once v1.0.0 is reached; before
that, expect breaking changes in any v0.x release.
[Unreleased]¶
[0.6.1] - 2026-06-02¶
Added¶
-
latex_live(serve_fast = True)— opt-in faster live rebuilds. When set, the watcher recompiles content edits by replaying the compiled action directly viatectonic_compile.pyinstead of shelling out tobazel build, skipping Bazel's CLI/analysis/sandbox overhead (~50% of warm-rebuild latency). The first build and any fast build that hits a missing cached resource still go throughbazel build, and the replay stages only the document's declared inputs, so it stays consistent withbazel build/CI. DefaultFalse. See DESIGN.md §4.7.4. -
shared_libraryexample. A miniature monorepo showing how to share LaTeX infrastructure across packages: a house-style preamble and a bibliography (bothlatex_library) plus the shared logo (latex_pkg), consumed by two sibling documents (a report and a memo) viadepsand workspace-relative-path references. Demonstrates the cross-package main-rooted staging (DESIGN.md §4.11) and the "edit the style once, every document updates" payoff.
Changed¶
latex_livenow picks up newly-added source files on its own. The watcher polls the directories that contain the document's sources (not just the source files themselves), so dropping a new.texinto aglob()-matched directory triggers a rebuild and is included with no manual touch or serve restart. Detection is filtered to source extensions, so atomic temp+rename saves (vim/VS Code) still take the fast in-place edit path.
[0.6.0] - 2026-05-31¶
Changed¶
- BREAKING: adopted a fresh TeX Live 2026 package bundle; retired the
modern_biblatexopt-in and the biber 2.17 pin. rules_latex now ships its own bundle — a TeX Live 2026.ttbbuilt from source and hosted on Cloudflare R2 (rules-latex.ndl.au) — instead of relying on tectonic's relay (the frozen 2022 bundle). The implicit-pipeline prime range-fetches packages from it (--bundle <R2 .ttb URL>);tectonic.bundle()downloads it whole. Because TL2026 ships biblatex 3.21 natively, the default bibliography stack is now biber 2.21 and thetectonic.toolchain(modern_biblatex = True)overlay is gone (its 3.21/2.21 stack is the default). Migration: remove anytectonic.toolchain(modern_biblatex = True)from yourMODULE.bazel— modern citation styles (biblatex-apa 9.x, etc.) now work out of the box. Resolves the bundle-staleness issue (DESIGN.md §4.10 / §5 #1) and makes the linux/aarch64 biber the off-the-shelf CTAN 2.21 binary (retiring the from-source 2.17 build, issue #10). See DESIGN.md §4.4 / §4.9 / §4.10.
Added¶
-
tectonic.bundle()can now mirror the package bundle. The tag accepts optionalurl+sha256(root module only) to repoint the full-bundle download at your own mirror — e.g. an internal artifact host — for availability independence. Omitting them (the common case) keeps the pinned default (the self-hosted TeX Live 2026 bundle on R2), so this is fully backward-compatible. Setup runbook inDESIGN.md§4.4 "Self-hosting the bundle". -
Hermetic biber on linux/aarch64.
biber = Truedocuments now build hermetically on Linux arm64, using a prebuilt biber 2.21 binary from CTAN'sbiber-linux-aarch64package (mirrored tobiber-mirror-v2.21, pinned by SHA inbiber_versions.bzl, and CI-verified on aubuntu-24.04-armrunner by building//paper:paper). Previously Linux arm64 had no biber andbiber = Truefailed at analysis. SeeDESIGN.md§4.9 / §5 #9 / issue #10.
Changed¶
-
BREAKING: renamed the
latex_serve_webrule tolatex_live. The live-preview rule is now loaded and called aslatex_live(load("@rules_latex//latex:defs.bzl", "latex_live")), and its source moved tolatex/private/latex_live.bzl. No behaviour or attribute change — only the rule name. Update yourload()and rule calls;latex_serve_webno longer exists. (Pre-1.0, breaking changes are expected in any v0.x release.) The internal server template and its/_assets/serve_web.*routes are unchanged. The repo's example targets were renamed to match:hello_serve_web→hello_live,hello_synctex_serve→hello_synctex_live,cv_serve→cv_live. -
SyncTeX reverse-sync framing: "click to jump" → "click to copy source location." The in-app hint, the README, and the docs previously described clicking on a glyph in the preview as jumping to source. A web page can't drive your editor (vim, emacs, VS Code, etc.) to a
(file, line)location — only the user (or a server-invoked CLI for a few editors that happen to have one onPATH) can. The two paths that would make the jump real both fail silently for too many editor + setup combinations to ship as a default, so v0.6 walks the framing back instead: the click resolves the source location, displays it in the footer, and copies<file>:<line>to the clipboard vianavigator.clipboard.writeText(with a textarea +execCommand("copy")fallback for hostile environments). The footer entry is itself clickable to recopy. Users paste the location into whatever opens files for them — vim's:e, the VS Code Quick Open prompt,code -g, etc.
Forward-sync (editor → PDF) is unaffected — that direction does jump, because the editor is the one driving it.
No code or attribute changes for end users — synctex = True
still works the same way; the behaviour and copy text are the
only difference. See DESIGN.md §4.8 for the full rationale.
-
Live-preview renders pages lazily.
latex_serve_webpreviously re-rendered every PDF page into its own canvas on each reload — invisible on a CV, a perceptible per-page stall on a long thesis. Pages now get a dimensioned placeholder up front and anIntersectionObserverrasterizes each canvas only as it nears the viewport (cancelling the raster if it scrolls away first); the visible page is painted immediately. Text layers stay eager so Ctrl+F search is unaffected. No user-facing API change. SeeDESIGN.md§5 #13. -
Live-preview client JS/CSS extracted into testable modules. The ~1500-line browser client that lived inline in
serve_web.py.tplis now real ES modules underlatex/private/(serve_web.js,serve_web_synctex.js,serve_web.css), served at/_assets/instead of inlined. This adds a JS unit-test harness undertests/js/(node --testviash_test, no npm deps) — the JS analogue of the repo's system-python3test convention. No user-facing change tolatex_serve_web. SeeDESIGN.md§5 #11. -
Unit-tested the live-preview render path. The lazy-paint state machine + render-observer decision and the
ChunkedTransportbyte-range planner are factored into pure modules (serve_web_render.js,serve_web_chunks.js) and unit-tested undertests/js/withnode --test. Behavior-preserving refactor; thelatex_serve_webrule now globs its client assets via a filegroup so new modules need no rule change. -
Per-page content index in the live-preview manifest (server side of option B).
pdf_chunks.pynow resolves the PDF page tree — including the compressed object stream (/ObjStm) tectonic emits — and/pdf-manifest(and the WebSocket manifest push) carry a per-page{contentHash, width, height}, so a future change can skip re-rendering unchanged pages on reload. Reuses the existing chunk hashes, so a page's hash changes iff its content stream did. Best-effort: an unparseable page tree yields an empty index and the client re-renders every visible page. No behavior change yet — the client doesn't consume it until the reuse logic lands. SeeDESIGN.md§5 #13. -
Live preview reuses unchanged pages across reloads (option B complete). On a rebuild, the client diffs the manifest's per-page content hashes against the previous render and moves the unchanged
.page-wraps over — keeping their already-painted canvases — instead of rebuilding and re-rasterizing them; only the edited page(s) re-render. A zoom or an unresolvable page tree falls back to a full re-render. Completes the §5 #13 work atop the lazy-paint change above. -
Live-preview jank mitigations + render-cost measurement.
content-visibility: autolets the browser skip paint/compositing of off-screen pages; the render observer now defers a page's raster until scrolling settles, so a fast fling-scroll no longer starts (then cancels) a render for every page flown past; and per-page raster timing is recorded onwindow.__serveWebRenderStats(avg / max / slow-count) to inform whether off-main-thread rendering is worth its cost. SeeDESIGN.md§5 #13.
Removed¶
-
VS Code "Simple Browser" auto-open from
open_on_start. It tried to open the preview as a Simple Browser tab viacode --open-url vscode://vscode.simpleBrowser/show?url=…, but that never worked: VS Code routes avscode://URI by extension id, and the built-in Simple Browser registers no URI handler — so it failed with "extension 'vscode.simplebrowser' … not found." Dropped the editor detection / handoff entirely;open_on_startnow just opens the system default browser, and the http URL is always printed (paste it into Simple Browser manually if you prefer). Same "don't fake driving the editor" stance as DESIGN.md §4.8. -
latex_serverule (system-PDF-viewer live preview). The rule opened the built document inopen/xdg-open/startand relied on the PDF viewer to detect the file-on- disk change and reload itself. That contract eroded: - macOS Preview's auto-reload became unreliable after the Sonoma sandbox changes and stopped firing dependably by Sequoia.
- Adobe Acrobat never watched the file on macOS and locks it on Windows.
- Users hitting either default viewer would see "saves don't appear in the preview" with no in-rule way to diagnose it.
v0.6 drops the rule rather than ship a viewer-specific
workaround (AppleScript force-reload, plugin recommendations,
etc.). latex_serve_web (introduced in v0.2 and overhauled in
v0.5) covers the use case better in every dimension that
matters — faster reload via WebSocket chunk push, page
navigation, in-doc search, outline sidebar, build-log drawer,
light/dark theme, native text selection.
Migration: if your BUILD.bazel has
bazel run //:doc_live as before. The browser tab
opens automatically and refreshes on save. Users who genuinely
prefer a native PDF viewer can keep one open against
bazel-bin/<pkg>/<doc>.pdf — every bazel build keeps that
path fresh — but the viewer must support file-watch reload
(Skim, Sioyek, Zathura, PDF Expert all do; Preview and
Acrobat don't).
Files removed: latex/private/latex_serve.bzl,
latex/private/serve_watcher.py.tpl. The
latex_serve symbol is no longer exported from
@rules_latex//latex:defs.bzl.
[0.5.0] - 2026-05-24¶
The headline of this release is a full overhaul of the
latex_serve_web live preview: faster reload transport, a real
PDF viewer chrome (page nav, search, outline, build log), and a
light/dark theme. Server contract is unchanged — latex_document,
latex_test, and the toolchain layer are identical to v0.4.2.
Added¶
- WebSocket push transport for live-preview reloads.
latex_serve_webnow exposes a/wsendpoint that, after each successful rebuild, pushes the chunk manifest plus any PDF chunks the connected client doesn't already have — in a single duplex burst, no client poll needed. Compared to the previous SSE-only flow (reload event →/pdf-manifestfetch → one/chunk/<hash>fetch per missing chunk), this saves two pull round-trips on the hot path.
Hand-rolled stdlib WebSocket server at
tools/ws_server.py
(RFC 6455 — handshake, framing, ping/pong, fragmentation,
close). No third-party dependency, no rules_python adoption
needed; the permessage-deflate and subprotocol corners of
the spec are skipped deliberately (chunks are already
FlateDecode'd, no need for subprotocols on a single-peer
transport).
SSE remains at /events as a transparent fallback for clients
that can't upgrade (proxies that don't speak Upgrade,
deployments that fail to load ws_server.py on the server
side, etc.). The user-visible UX is unchanged on the SSE path;
WS just makes rebuild-to-render lower-latency. See the
live-preview docs
for the wire format and
DESIGN.md §5.7
for the historical context. Resolves
#9.
-
Live-preview chrome overhaul. The header is now a proper PDF viewer control bar instead of three zoom buttons:
-
Page navigation —
‹ N / M ›with an editable page input (Enterto jump).IntersectionObservertracks current page on scroll. - Zoom — real
%(was lying as100%), plus dedicated fit-width↔and fit-page▭buttons that auto-recompute on window resize. - Download —
⤓link to/pdfwith a download attr matching the document name. - Fullscreen —
⛶togglesrequestFullscreenon the viewer. -
Keyboard —
PageUp/PageDown,Home/End,+/-/0,w(fit width),p(fit page),f(fullscreen),g(focus page input),s(toggle outline),l(toggle log),t(cycle theme),Ctrl/⌘+F(find). -
In-document search.
Ctrl/⌘+F(or the⌕header button) opens a find bar above the viewer. Substring match, case- insensitive, highlights matches in the PDF.js text layer with the current match in a stronger accent colour and scrolled into view.Enter/Shift+Entercycle next/prev with wrap-around. -
Selectable PDF text. Each rendered page now carries a PDF.js
TextLayeroverlay so the user can select-and-copy text in the preview — the canvas-only render in v0.4.x had no selectable text at all. -
Outline sidebar. Documents with hyperref bookmarks (any
\section/\subsection/\chapteretc.) get a collapsible left sidebar with clickable section nav. Auto-shows on first render that produces an outline; the toggle button stays hidden for documents without sections. Current-section is highlighted as you scroll. Show/hide preference persists vialocalStorage. -
Build-log drawer. Collapsible bottom drawer that exposes the latest
bazel buildstdout+stderr (capped at 64 KiB, head-trimmed). Header shows a summary tail (the last non-empty line — usuallyBuild completed successfullyor the actual error); expand for the full log in a scrollable<pre>. Auto- expands on the first failed build of a session unless the user has explicitly closed it. Copy-to-clipboard button. New/logHTTP endpoint +log-updateWS push event. -
Build status + git context. The status pill now shows
✓ 1.42 s · build #5 · 12 s agowith a live-ticking "Xs ago" suffix (no extra polling — the ticker only restrings the cached status). Footer gains a git badge showing branch + dirty marker, with the short HEAD SHA in the tooltip. Server-sideBuildState.get_git_info()shells out to git with a 2-second TTL cache so the per-second status poll doesn't spawn three subprocesses per tick. -
Light / dark / auto theme. Full palette refactor onto CSS variables. The new
⊙button cyclesauto→dark→light(keyboardt);autofollowsprefers-color-scheme. Choice persists vialocalStorage. The PDF page surface stays white in both themes (flipping it would invert document content). -
Polish details. Inline data-URI SVG favicon in the project's teal accent, 2px accent stripe under the header to pull the same colour into the most-visible chrome edge, redesigned empty state with a pulsing teal glyph + helpful hint (
prefers-reduced-motion-aware). -
Unit tests for
BuildStatehelpers. 40 new test cases coveringset_logtruncation contract,get_git_infocaching - non-git fallback,
broadcast_chunks/broadcast_event/broadcast_ws_build_failed/broadcast_log_updatefan-out ordering and isolation,_combine_outputstderr-after-stdout invariant. Seetests/py/test_build_state_*.py.
Changed¶
-
WS manifest payload uses the
rangeskey matching the existing/pdf-manifestJSON shape, soChunkedTransportconsumes both transports through the same code path. -
Forward-sync (SyncTeX
POST /sync/forward) events fan out to both SSE and WS clients so editor-jump UX works the same regardless of which transport the connected browser tab chose. -
run_bazel_buildsignature gains a fourth element (combined_output); all internal call sites updated. The serve script captures the combined stdout+stderr per build and hands it toBuildState.set_log, which feeds the new/logendpoint. No effect on the rule-side action protocol. -
README release badge now filters tags to
v*so the shields.io semver sort no longer mistakes thebiber-mirror-v2.21tag for a project release.
Documentation¶
- DESIGN.md §5.7 (WebSocket transport) marked SHIPPED with the original deferral rationale preserved as an audit trail.
- DESIGN.md §5 #11 (rules_python trade-off) picks up a sub-section recording the JS test-harness gap from the UI overhaul as one of the accumulating triggers that would justify revisiting the stdlib-only convention.
- DESIGN.md §5 #13 (new) tracks live-preview render perf for long docs: viewport-gated canvas paint, off-screen swap, canvas reuse on unchanged geometry, OffscreenCanvas worker rendering. Punted for v0.5.0; see #50.
docs/site/about/design.mdrewrites the "Why SSE not WebSockets?" section as "Why WebSocket and Server-Sent Events?" — the answer is now both.docs/site/getting-started/live-preview.mdadds a wire- format table for the WS push transport.
[0.4.2] - 2026-05-24¶
Fixed¶
- Release workflow now reliably produces a GitHub release. Two release-pipeline-only fixes folded in:
- The reusable workflow's
bazel teststep now boots the CTAN fixture HTTP mirror first, mirroring CI. Without it,transitive_resolve_test404'd against real CTAN looking for the synthetictest-pkg-a/test-pkg-bfixtures (PR #35). - The final
bazel testline inbazel_test_commandends with a backslash so the disk/repository cache flags that the reusable workflow appends become continuation args rather than a fresh shell command (PR #37).
No code changes versus the v0.4.0 / v0.4.1 attempts — same feature set, working pipeline.
[0.4.1] - 2026-05-23 [YANKED]¶
Tag exists but no GitHub release was produced — the release workflow's appended
--disk_cache=...flag tripped over a missing shell line-continuation inbazel_test_command(exit code 127 after all tests passed). Use 0.4.2 instead.
[0.4.0] - 2026-05-23 [YANKED]¶
Tag exists but no GitHub release was produced — the release workflow failed at the
bazel teststep (CTAN fixture mirror was not started). Use 0.4.2 instead.
Added¶
-
tectonic.toolchain(modern_biblatex = True)opt-in. Makes the toolchain extension fetch biblatex 3.21 from CTAN and biber 2.21 from the rules_latex GitHub mirror, and overlay them on top of the bundle via Tectonic's-Z search-pathflag. Required for modern biblatex extension styles (biblatex-apa9.x,biblatex-chicago,biblatex-ieee,biblatex-nature, etc.) which need biblatex 3.18+ / biber 2.18+ — the bundle pins 3.17 / 2.17, an incompatibility documented in DESIGN.md §4.10. Default workspaces stay on the stable 3.17 / 2.17 pair, which is fine for the five core biblatex styles the bundle ships. Seedocs/site/getting-started/bibliography.md#modern-citation-styles. -
CTAN auto-resolve for transitive dependencies. Listing
ctan_packages = ["biblatex-apa"](or any other entry) now also auto-fetches everything that package transitively requires from CTAN that isn't in Tectonic's bundle. The resolver scans each fetched package's.sty/.cls/.bbx/.cbx/.lbx/.dbxfiles for\RequirePackage/\usepackage/\LoadClass, filters references against a shipped bundle manifest (latex/toolchain/bundle_manifest.txt, ~6100 entries for tlextras-2022.0r0), HEAD-probes CTAN for any name not in the manifest, and recurses. Single compile pass; users only list entry-point packages. -
RULES_LATEX_CTAN_MIRRORenvironment variable. Replaces the hardcodedhttps://mirrors.ctan.orgURL prefix with a configurable one. Three audiences: CI (point at a local fixture server to avoid real-CTAN flake), enterprise users behind firewalls with internal CTAN mirrors, and reproducibility-conscious users pinning a specific mirror. -
Retry-with-backoff on CTAN downloads.
_retry_urlretrievewraps each URL attempt up to three times with 1s/2s/4s exponential backoff. RetriesURLError(timeouts, DNS, TLS) andHTTPErrorwith 5xx status; 4xx propagates immediately so the existing fallback chain takes over. Helps real users on flaky networks just as much as it helps CI. -
Targeted failure-path hints. When the populate-cache step fails, the tool now greps the tectonic
.logfor the missing- file LaTeX error AND for the biblatex/biber version-mismatch signature. The first emits a hint that names the requiring package and suggests adding the missing name toctan_packages; the second points at themodern_biblatex = Trueopt-in. Both include direct links to the relevant docs section. -
Proactive
ctan_packagesdep map. The populate step prints a per-package summary of upstream\RequirePackage-style references so users see what each fetched package pulled in, even on successful builds. -
CTAN fixture mirror for hermetic CI. Checked-in TDS zips at
tests/ctan/fixtures/macros/latex/contrib/{,biblatex-contrib/}*.zipserved by a localpython3 -m http.serverstarted by the CI workflow. Integration tests in//tests/ctan:*now run ~4–8× faster than against real CTAN and are flake-free. Seetests/ctan/fixtures/README.mdfor the refresh procedure. -
SyncTeX forward-sync. New
POST /sync/forwardendpoint onlatex_serve_web. Maps a source(file, line)tuple to a PDF location via the same SyncTeX index that powers reverse-sync; broadcasts a JSON{"type": "jump", ...}event over the existing SSE channel; the browser scrolls the matching page into view and flashes a yellow highlight overlay at the box. Editors / CLI shims invoke via curl. Five documented response shapes (success / unmatched / file unknown / synctex not produced / synctex disabled). Includes editor-integration snippets for Neovim, VS Code, and Emacs atdocs/site/getting-started/live-preview.md#synctex-forward-sync. -
RULES_LATEX_ACTION_SCHEMAcache-key contribution. Baked into the env ofTectonicPopulateCacheandTectonicCompileso adding or removing a declared output on those rules invalidates pre-existing action-cache entries (Bazel's action cache key doesn't include declared outputs; this paper-cuts the class of bug we hit once on synctex). A newaction_schema_canary_testanalysistest snapshots the declared-output set + verifies the env wiring and fails on drift, prompting the developer to bump the constant. Seelatex/private/action_schema.bzland DESIGN.md §5 item 10. -
ctan_packagesattribute onlatex_document,latex_test, andlatex_cache_snapshot. Accepts a list of CTAN package names (e.g.["biblatex-apa"]) and pulls them frommirrors.ctan.orgin TDS format during the implicit cache pipeline's online prime. Closes the gap between Tectonic's frozen 2022 bundle and modern CTAN: APA / Chicago / IEEE biblatex styles, recenttcolorboxreleases, niche contrib packages, and so on. Zero new targets required — just list package names where they're used.
latex_document(
name = "thesis",
main = "thesis.tex",
srcs = ["thesis.tex", "references.bib"],
ctan_packages = ["biblatex-apa"],
biber = True,
)
Compatible with the implicit cache pipeline (default) and with
per-document cache snapshots; explicitly incompatible with
tectonic.bundle() (the bundle path skips the online prime).
See docs/site/getting-started/ctan-packages.md and the
new examples/ctan_paper/ for the user-facing treatment, plus
DESIGN.md for the architectural rationale.
-
Structured cache-snapshot tarball format. Snapshots produced with
ctan_packagesnon-empty wrap two trees:cache/(the tectonic bundle cache, what the flat format used to hold) andctan_pkgs/(the extracted TDS overlay).tectonic_compile.pydetects the structure at extract time and setsTECTONIC_CACHE_DIR+TEXMFHOMEaccordingly. Legacy flat-format snapshots from olderrules_latexreleases keep working unchanged — the format detection is purely structural. -
New
examples/ctan_paper/example demonstrating an APA-style bibliography viactan_packages = ["biblatex-apa"]+biber = True. -
latex_serve_webnow detects when it's being launched from a VS Code-family editor's integrated terminal (viaTERM_PROGRAM=vscode/cursor/vscodium) and prints an<editor>://vscode.simpleBrowser/show?url=...URI alongside the plain http URL. Cmd/Ctrl-clicking that URI in the editor's terminal opens the live preview as a Simple Browser tab in the same window — no separate window or extension required. -
New
open_on_startattribute onlatex_serve_web(defaultFalse). WhenTrue, the preview is opened automatically once the server is ready: in a Simple Browser tab via the editor CLI when an editor is detected, otherwise in the system default web browser. The plain http URL is always printed regardless, so users can copy/ paste manually if either auto-open path fails.
Fixed¶
-
Fetched CTAN packages now actually reach tectonic. The original
ctan_packagesplumbing setTEXMFHOMEon the tectonic invocation, which was a no-op: tectonic doesn't honour TEXMFHOME (it's a kpathsea concept, and tectonic uses its own simpler resolver). Fetched packages were downloaded and extracted but tectonic never consumed them — the bundle served every request. Discovered while writing the first end-to-end test for the auto-resolver. The fix: switch both the populate- cache and compile actions to Tectonic's-Z search-pathflag, walkingctan_pkgs/for directories holding package files and emitting one flag per directory. Tectonic's lookup is now cwd → search-path → bundle, exactly as we'd assumed it already was. See DESIGN.md §5 item 12. -
HTTP HEAD support.
latex_serve_web's embedded server now honoursHEADrequests per HTTP/1.1: every GET endpoint returns the same status code and headers under HEAD, with an empty body. Previously every HEAD request 501'd with "Unsupported method ('HEAD')" — a latent bug that didn't surface in normal use (PDF.js and the index page only issue GETs) but brokecurl -I, browser prefetch heuristics, link checkers, and any future reverse-proxy in front of the serve target. Implementation is inlatex/private/serve_web.py.tpl: ado_HEADre-entersdo_GETwith a per-request flag set, and an overriddenend_headers()swapsself.wfileto a sink so subsequent body writes no-op. The SSE handler (/events) short-circuits on HEAD to avoid leaking listener threads. -
Stray-backslash bug in generated bash scripts in
latex_test.bzlandlatex_cache_snapshot.bzl. The" \\" + " \\".join([...])pattern emitted a leading lone backslash whenctan_packageswas empty (PRs #20 / #25). Switched to the same" \\\n ".join([...])pattern thatsrc_argsandpkg_file_argsalready use — works with both empty and non-empty lists, no conditional needed.
Added¶
-
Content-addressed PDF chunk transport. The serve script now parses each compiled PDF's cross-reference (xref) table, breaks the PDF into per-object content-addressed chunks (SHA-256 of bytes), and exposes two new endpoints:
GET /pdf-manifest— JSON manifest of{ pdfSize, ranges: [{ objectId, start, end, hash }, ...], skeletonRanges }.GET /chunk/<hash>— raw bytes of one chunk, served withCache-Control: public, max-age=31536000, immutableso the browser's HTTP cache pins it indefinitely.
Client side, a custom PDFDataRangeTransport subclass
intercepts PDF.js's byte-range fetches: ranges covered by a
chunk in the latest manifest are served from a client-side
hash cache (or fetched once from /chunk/<hash>), while
skeleton ranges (PDF header, gaps between objects, trailer)
come from /pdf via HTTP Range requests. Chunks are
prefetched in the background after each reload so subsequent
page renders are wire-free.
On a one-line edit to examples/cv/cv.tex, 14 of 20 chunks
(70%) stay unchanged across the rebuild, dropping the reload's
network volume by ~50% even on this 24 KB document. For
multi-page documents (theses, books) the savings approach
100% because edits typically don't shift page-content stream
offsets for unaffected pages.
Chunks live under
$BUILD_WORKSPACE_DIRECTORY/.cache/rules_latex/<doc-slug>/chunks/,
GC'd 5 minutes after they leave the active manifest so quick
edit-undo round-trips stay free. Falls back to whole-PDF
transport (the previous behaviour) on any parse failure —
cross-reference-stream-stream PDFs, malformed output, etc.
-
New tool:
tools/pdf_chunks.py— stdlib-only cross-reference stream and classic xref-table parser. ~150 lines plus exhaustive unit tests (cross-reference-stream parse, classic xref parse, chunk hashing, coverage invariant, error paths, hash stability, atomic writes, dedup). -
Debounce-then-fire watcher.
latex_serve_webnow coalesces bursts of source-file changes into a single build instead of firing one build per detected mtime change. A small FSM (_debouncer_stepinserve_web.py.tpl) waitsdebounce_ms = 250of source-idle before triggering, with a hard cap atdebounce_max_ms = 1500for continuous-typing cases. Both are exposed as attributes onlatex_serve_web; setdebounce_ms = 0to reproduce the legacy fire-on-every-poll behaviour.
Coalesced bursts get one combined log line at fire time. The
poll interval (poll_interval_ms = 80) is unchanged — it
controls how fast we notice a change, while the debouncer
controls when we act on it.
Practical motivations: editors that write-then-rename (vim, neovim) produce two mtime bumps for one save; format-on-save hooks write twice; fast autosave produces many mtime bumps for a single logical edit. All three now collapse to one build.
-
Pre-extracted serve cache directory. When the persistent serve cache is primed (see prior entry below), the snapshot is also extracted into a sibling
cache/directory protected by its own atomicity sentinel. The compile action consumes the extracted directory directly asTECTONIC_CACHE_DIR, skipping the ~100-500 ms of gzip-decompression + 300+ file writes per warm rebuild on macOS APFS. Hermeticity-equivalent to the previous tarball-passing path: tectonic doesn't write to its cache directory under--only-cached(verified empirically), so concurrent compiles can safely share it. -
Persistent worker for
TectonicCompile. The compile action now declaressupports-workers = "1"+requires-worker-protocol = "json". Bazel keeps a singlepython3 tools/tectonic_compile.py --persistent_workerprocess alive across actions and dispatches each compile as aWorkRequestover stdin. Eliminates the ~80-150 ms CPython cold-start cost per warm rebuild after the first one. The worker implementation is stdlib-only (JSON protocol, not protobuf) to keep the no-rules_python-dep invariant. Users can force the legacy path with--strategy=TectonicCompile=local,sandboxedfor debugging. -
tectonic_compile.py --cache-dir. New flag accepting a pre-extracted cache directory; mutually exclusive with--cache-tarballand--bundle. The implicit-pipeline / usercache=paths continue to pass tarballs through--cache-tarball; only the serve-cache fast-path uses--cache-dir. -
latex_serve_webnow auto-primes a persistent cache snapshot on startup for documents that take the implicit-pipeline path (nocache=, no toolchain bundle). The snapshot lives under$BUILD_WORKSPACE_DIRECTORY/.cache/rules_latex/<doc-slug>/and is reused across serve sessions. Body-only edits to the document no longer trigger an online re-prime; rebuilds drop from ~30-90 s to ~2-3 s. The first start of any serve target still pays the one-time prime cost (online, requires network), but subsequent starts and edits are offline and fast.
The snapshot is invalidated automatically when a rebuild fails
with a missing-resource error (e.g. the user just added a new
\usepackage): the serve script re-primes and retries the build
once before giving up. The cache directory is added to
.gitignore on first prime to keep it out of users' source
trees.
Documents that set cache = "..." or run against a toolchain
bundle skip all of this and behave exactly as before — the
serve override only fills a gap that previously made
latex_serve_web painfully slow for the zero-config case.
-
examples/cvships with a checked-in cache snapshot (cv_cache.tar.gz) and uses thecache=attribute, matching the pattern inexamples/hello. This is independent of the serve-time auto-prime above and gives users a fully-offline reference of the explicit-snapshot pattern. Refresh withbazel run //cv:cv_cache_snapshot. -
New private build setting
//latex:_serve_cache_override. Not part of the public API. Set bylatex_serve_webto pointlatex_documentat the persistent serve cache; ignored by documents that already havecache=or a toolchain bundle. -
New provider
LatexDocumentInfo. Carries the compile-time inputs (main file, biber binary, pkg_files overrides, toolchain handle) of alatex_documentso live-preview rules can drive parallel tectonic invocations without re-introspecting attributes. -
LatexInfogrows anoffline_strategyfield reporting which offline-mode strategy the target resolved to ("user_cache","bundle", or"implicit").latex_libraryandlatex_pkgleave it as the empty string. Consumed bylatex_serve_webto decide whether the persistent-cache fast-path is needed.
Changed¶
-
staging.stage_sourcesmaterialises with hardlinks (then symlinks, then copy) instead of unconditional copy. The staging tmpdir is per-action and torn down at action end, so the "self-contained snapshot" rationale for unconditional copy doesn't actually apply. Hardlinks save ~5-50 ms perstage_sourcescall depending on source-set size — small per- action but compounds across the live-preview hot path. Falls back to copy on filesystems / platforms that don't permit linking. -
TectonicCompileandTectonicPopulateCacheactions now run viactx.actions.rundirectly (with/usr/bin/env python3) instead ofctx.actions.run_shell, except for thebiber_strategy = "system"escape hatch which still needs the shell to propagatePATH. Saves ~5-15 ms per action by not forking/bin/shto immediatelyexec python3. -
latex_serve_web's defaultpoll_interval_msdropped from 250 to 80 ms, to reduce perceived save-to-preview latency. The watcher is still polling (nowatchdog/inotify dependency), so this is the amortised cost of onestat()per watched file per 80 ms — cheap for the document-tree sizes this serves. -
Tectonic's stdout is now captured and forwarded to stderr inside the compile action wrapper. In persistent-worker mode our own stdout is the worker protocol channel; this prevents tectonic's user-facing progress notes (
note: Running TeX ...) from corrupting Bazel's worker responses. In single-shot mode it just collapses all tectonic chatter onto one stream — a UX improvement.
Performance summary¶
Cumulative effect on warm rebuilds:
| Path | Before | After |
|---|---|---|
latex_serve_web persistent-cache (implicit pipeline) |
~2.4 s | ~2.0-2.3 s |
bazel build with explicit cache= |
~2.6-2.8 s | ~2.3-2.5 s |
| First-prime cost (cold workspace) | 30-90 s | 30-90 s (unchanged) |
The remaining floor is tectonic itself (~600 ms-2 s depending on
document) plus Bazel client/server startup (~150-400 ms). See
docs/site/about/roadmap.md for the levers that would push past
that floor.
[0.3.1] - 2026-05-17¶
Fixed¶
-
latex_testscript generation used${{...}}(double braces) for shell variable expansions in a section of the launcher that wasn't passed through.format(). macOS bash silently tolerated the malformed form; Linux bash rejected it with "bad substitution", breakinglatex_testtargets in CI. Drop the extra braces. -
Buildifier docstring-header lint regressions on
_resolved_pkg_fileshelpers. Add proper one-line summaries. -
latex_test(biber_strategy = "system")silently produced a broken test script (use_system_biberwas set but never wired). Replace silent inability with an explicitfail()at analysis time:latex_testdoesn't currently support system biber because the test sandbox scrubs PATH.
[0.3.0] - 2026-05-17¶
Changed (breaking)¶
- Main-rooted source staging. Both
TectonicPopulateCacheandTectonicCompileactions now stage sources into a temporary work directory and run Tectonic with cwd set to the directory containing the main.texfile. Relative paths inmain.tex(in\input,\graphicspath,\addbibresource, etc.) resolve against main's directory, exactly as they would in an editor-driven local compile.
Previously, TectonicCompile ran tectonic from the Bazel execroot
with main passed as an execroot-relative path, while
TectonicPopulateCache staged sources under a common-ancestor work
dir. The two action paths therefore had different cwd conventions
and could disagree about whether a path resolved.
Migration: documents using .. in \graphicspath,
\input{../...}, or \addbibresource{../...} need to update those
paths. The new layout makes cross-package sources reachable at
their workspace-relative path (e.g.
_shared/logo/logo.png instead of ../_shared/logo/logo.png),
and the new pkg_files attribute lets you override placement of
specific inputs to keep main.tex clean.
See DESIGN.md §4.11 for the full staging contract.
-
make_cache_snapshot.pyreplaced. The old single-tool design is split into: -
tools/staging.py: the shared layout library. tools/tectonic_populate_cache.py: TectonicPopulateCache and the backing tool forlatex_cache_snapshot.tools/tectonic_compile.py: TectonicCompile action wrapper.
Out-of-tree consumers that referenced //tools:make_cache_snapshot.py
directly need to migrate to the new layout.
Added¶
latex_document.pkg_filesattribute. Map of label → relative-path-under-main's-work-dir. Lets you stage a cross-package source (typically a.bibfile) at any path inside main's work directory, including as a sibling of main.tex itself. The classic use case is sharing onereferences.bibacross multiple documents in different packages:
latex_document(
name = "notes",
main = "notes/main.tex",
srcs = [...],
biber = True,
pkg_files = {"//lib/refs:refs.bib": "refs.bib"},
)
Then \addbibresource{refs.bib} in notes/main.tex resolves
correctly. Without pkg_files the file would auto-stage at
lib/refs/refs.bib and need to be addressed by that full path
from main.tex (which is also valid).
- Same
pkg_filesattribute onlatex_testandlatex_cache_snapshot. Stay consistent across all three rules.
Fixed¶
-
Tectonic's bibliography subprocess (biber) refused paths containing
..with "relative parent paths are not supported for the external tool". The new main-rooted staging avoids..paths entirely, fixing biblatex compiles for documents whose.biblives in a sibling package. -
latex_test's--keep-logsoutput and tectonic invocation now go through the sametectonic_compile.pywrapper aslatex_document, so log-path and staging behaviour is identical between the two rules. Previously the test rule used its own inline shell snippet with subtly different conventions.
[0.2.0] - 2026-05-16¶
Added¶
- Biber toolchain. A
biberfield on thelatex_toolchainrule points at a platform-specific biber binary fetched from a rules_latex-owned GitHub release mirror (biber-mirror-v2.17). The toolchain is materialised by the sametectonicmodule extension that wires up tectonic. Pinned to biber 2.17 to match the biblatex v3.8 control-file format shipped in the currenttlextras-2022.0r0bundle (see DESIGN.md §4.10). Available on linux/x86_64, macos/x86_64+aarch64 (universal), and windows/x86_64; linux/aarch64 is gapped (see DESIGN.md §4.9). latex_document(biber = True). When set, the action stages the toolchain biber binary onto PATH so tectonic's biblatex subprocess finds it. Optionalbiber_strategy = "system"escape hatch propagates$PATHfor users on linux/aarch64 (or air-gapped builds with a pre-installed system biber).- Implicit cache pipeline.
latex_documentnow synthesises a two-action build for documents without an explicitcache =or toolchain bundle:TectonicPopulateCachedoes one online prime (content-addressed by .tex sources × tectonic × bundle URL) and feeds the resultingtar.gzinto a hermeticTectonicCompile. The online prime is action-cached so subsequent builds skip it entirely. Net effect: users get a cache snapshot for free without declaring any new targets or checking anything in. See DESIGN.md §4.4. latex_cache_snapshot(biber = True). Same biber wiring as above, for the manual-vendoring path. Snapshots primed without biber are missing biblatex-related files and won't satisfylatex_document(biber = True)consumers.latex_document(synctex = True)produces a<name>.synctex.gznext to the PDF, exposed via thesynctexOutputGroup.latex_serve_webauto-discovers the synctex output when the document was built withsynctex = Trueand grows aPOST /sync/reverseendpoint that maps PDF-point (page, x, y) clicks to(source_path, line)tuples. The browser bindsclickon the rendered canvases and shows the resolved source location in a footer banner.- Self-hosted PDF.js:
latex_serve_webno longer fetches PDF.js from cdn.jsdelivr.net. The pinnedpdfjs-dist@5.4.149tarball is fetched at repository-rule time via the newpdfjsmodule extension (@rules_latex_pdfjs), and served at/_pdfjs/pdf.mjs+/_pdfjs/pdf.worker.mjsfrom the running server. Air-gapped live preview now works out of the box. - New
thesis_likeexample: a minimal biblatex+biber document that exercises the implicit-cache pipeline end-to-end.
Changed¶
- The
latex_toolchainrule grew abiberattribute. Auto-generated toolchain BUILD files include it when a biber binary is available for the platform; absent otherwise. Backwards-compatible — existing toolchains continue to work, just without biber support. latex_serve_webno longer accepts apdfjs_versionattribute; the version is pinned in//latex/private:pdfjs_versions.bzland bumped via a normal rules_latex release. To override the URL/SHA, fork the pin file or vendor your own@rules_latex_pdfjs.
[0.1.0] - 2026-05-16¶
Added¶
- Initial scaffold:
latex_document,latex_library,latex_pkgrules. - Bzlmod module extension that downloads Tectonic 0.16.9 binaries for Linux x86_64/aarch64 (both musl, statically linked), macOS x86_64/aarch64, and Windows x86_64.
tectonic.bundle()module extension tag that opts into a pinned offline package bundle (tlextras-2022.0r0, format v33, matching what tectonic 0.16.9 asks for by default), making compilation fully hermetic.latex_cache_snapshotrule: abazel run-able command that compiles a document once in online mode, captures the resulting tectonic cache, and writes a small (~10–100 MB) tarball into the source tree. Combined with the newlatex_document(cache = "…tar.gz")attribute, this enables fully-offline, content-addressed builds that are orders of magnitude smaller and faster than the full-bundle approach.latex_serverule: abazel run-able live-preview loop. Watches the document's transitiveLatexInfosources, rebuilds viabazel buildon every save, and opens the resulting PDF in the system viewer. Uses--watchfsand the resident Bazel server so steady-state rebuilds for small documents complete in ~200–400 ms.latex_serve_webrule: Overleaf-style in-browser preview. Stands up a localhost HTTP server with PDF.js rendering and Server-Sent Events for "reload" pushes on every successful rebuild. Preserves scroll position across re-renders. Pure-stdlib Python on the server side; PDF.js loaded from a CDN at page-load time.latex_document(reproducible = True)attribute that combinesSOURCE_DATE_EPOCH=0with-Z deterministic-mode, producing byte-identical PDFs across clean builds.latex_documentnow propagatesLatexInfoso meta-rules likelatex_servecan discover a document's sources without re-declaring them.latex_testrule: compiles a document underbazel testand asserts on patterns in the tectonic log (e.g. fails the build onLaTeX Error:). Supports acache = …attribute for fast offline test execution.LatexInfoprovider for inter-target source propagation.- Apache 2.0 license.
- Hello-world example workspace under
example/exercising the public API end-to-end (document, reproducible document, cache snapshot, offline-mode document, live preview, and test). - CI workflow building the rules and smoke-testing the example on Linux and macOS, plus buildifier linting.
- Tag-triggered release workflow that produces a
git archivesource tarball, publishes a GitHub Release, and emits a BCRsource.jsonsnippet ready to paste into a Bazel Central Registry PR. - Design document and README.