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]¶
Added¶
-
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¶
- 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.
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.