Documentation
¶
Overview ¶
Package texmf gets a TeX engine the support tree a document asks for, on a machine with no TeX distribution installed.
It redistributes nothing. Every byte of TeX macro source comes from a pinned upstream release, fetched onto the user's own machine, checked against a recorded SHA-256, and kept in the user cache. That is deliberate: beamer, for one, is offered "under the LaTeX Project Public License and/or under the GNU Public License", and downloading it is not a redistribution, so the choice between those two licences never has to be made on the user's behalf.
The result plugs straight into the engine:
tree, err := texmf.Open(ctx, texmf.Beamer, texmf.Options{})
if err == nil {
opt.Resolve = tree.Resolve
}
A caller that already has the files — a TeX distribution on the machine, a vendored copy, an air-gapped host — does not need this package at all: the engine's own search path and Options.Resolve already take precedence over anything fetched here.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var All = map[string]Bundle{ Beamer.Name: Beamer, PGF.Name: PGF, PGFPlots.Name: PGFPlots, Translator.Name: Translator, }
All is the catalogue, keyed by bundle name. The publish workflow reads a pin from here rather than repeating it in YAML, so a mirror can never carry a version or digest this code does not name.
var Beamer = Bundle{ Name: "beamer", Version: beamerVersion, SHA256: "2ab4acf4c6be0d96d3f18161b08fd8e56ab54d380686f6440a42705e48c76f76", Prefixes: []string{"tex/latex/beamer/"}, Provides: []string{"beamer", "beamerarticle"}, Sources: []Source{ OCISource{ Registry: "ghcr.io", Repository: "go-tex/texmf/beamer", Reference: beamerVersion, Label: "ghcr.io/go-tex/texmf/beamer:" + beamerVersion, }, HTTPSource{ URL: "https://github.com/josephwright/beamer/releases/download/v" + beamerVersion + "/beamer.tds.zip", Label: "upstream release josephwright/beamer v" + beamerVersion, }, }, }
Beamer is the beamer presentation class and the files it loads.
Measured over 10025 real talks: with these files present the engine renders 99.9% of them and ~8.7 pages per talk; without them, falling back to the engine's own emulation, 92.3% and ~1.4 pages. The difference is not an edge case, it is most of the content.
The archive is beamer's TDS distribution. Only tex/latex/beamer/ is extracted: the rest is documentation and sources, thousands of files the engine never opens.
Routes, in order:
- ghcr.io/go-tex/texmf/beamer — a registry the project controls, so the default route does not depend on a third party staying up;
- the upstream release itself, so the bundle stays reachable when the registry is not.
Both must produce the same bytes: SHA256 is checked whichever route ran, so a route is a delivery mechanism and never a trust anchor.
var ErrNotCached = errors.New("texmf: bundle not cached and offline")
ErrNotCached is returned by Open when the bundle is absent from the cache and Options.Offline forbids fetching it.
var PGF = Bundle{ Name: "pgf", Version: pgfVersion, SHA256: "d243b67705ab4f0e4fe91c4b26ed9da67cd7b0643fc38f158e91b600df9ba15e", Prefixes: []string{"tex/generic/pgf/", "tex/latex/pgf/"}, Provides: []string{ "tikz", "pgf", "pgfcore", "pgfmath", "pgfkeys", "pgffor", "pgfpages", "pgfrcs", "pgfsys", "pgfparser", "pgfcalendar", "pgfpict2e", "pgfnodes", "pgfarrows", "pgfautomata", "pgfheaps", "pgfshade", "xxcolor", "tikzexternal", "pgfmanual", "pgfbaseimage", "pgfbaselayers", "pgfbasematrix", "pgfbasepatterns", "pgfbaseplot", "pgfbaseshapes", "pgfbasesnakes", "pgflibraryarrows", "pgflibraryautomata", "pgflibraryplothandlers", "pgflibraryplotmarks", "pgflibraryshapes", "pgflibrarysnakes", "pgflibrarytikzbackgrounds", "pgflibrarytikztrees", "pgfcomp-version-0-65", "pgfcomp-version-1-18", }, Sources: []Source{ OCISource{ Registry: "ghcr.io", Repository: "go-tex/texmf/pgf", Reference: pgfVersion, Label: "ghcr.io/go-tex/texmf/pgf:" + pgfVersion, }, HTTPSource{ URL: "https://github.com/pgf-tikz/pgf/releases/download/" + pgfVersion + "/pgf_" + pgfVersion + ".tds.zip", Label: "upstream release pgf-tikz/pgf " + pgfVersion, }, }, }
PGF is pgf and TikZ: the drawing package the engine emulates when it is absent and loads for real when it is present.
Two prefixes, because pgf splits itself the way a package that serves both plain TeX and LaTeX has to: the .sty wrappers live under tex/latex/pgf/ and almost everything else — tikz.code.tex, pgfcore, pgfkeys, the pgfsys drivers — under tex/generic/pgf/. Loading only one half loads nothing.
The .lua files under those trees are dropped on the way out (see readZip): the engine has no Lua interpreter, and they are the only base-name collisions pgf has.
var PGFPlots = Bundle{ Name: "pgfplots", Version: pgfplotsVersion, SHA256: "4c4f33e976ba01d3f635c92d0a697a70c2c8779d5bcaae3b3ec2fbd8c82cc7ce", Prefixes: []string{"tex/generic/pgfplots/", "tex/latex/pgfplots/"}, Provides: []string{"pgfplots", "pgfplotstable"}, Sources: []Source{ OCISource{ Registry: "ghcr.io", Repository: "go-tex/texmf/pgfplots", Reference: pgfplotsVersion, Label: "ghcr.io/go-tex/texmf/pgfplots:" + pgfplotsVersion, }, HTTPSource{ URL: "https://github.com/pgf-tikz/pgfplots/releases/download/" + pgfplotsVersion + "/pgfplots_" + pgfplotsVersion + ".tds.zip", Label: "upstream release pgf-tikz/pgfplots " + pgfplotsVersion, }, }, }
PGFPlots is pgfplots, which draws function plots on top of pgf and cannot load without it.
It is the single largest thing standing between the engine and a real paper's figures: of the arXiv documents that contain a tikzpicture and draw nothing, more than half use pgfplots, and \addplot is by a wide margin the most common command the engine reports as undefined.
var Requires = map[string][]string{ PGFPlots.Name: {PGF.Name}, Beamer.Name: {Translator.Name}, }
Requires names the bundles a bundle cannot load without, innermost first. It is a fact about the packages, not about any one caller, so it belongs beside the pins rather than in whatever program happens to fetch them.
var Translator = Bundle{ Name: "translator", Version: translatorVersion, SHA256: "89a7be175e1f8ae1b5ea479a22a7c2c0e02f95a6fb457ac97ffd9eaf7b573eba", Prefixes: []string{"translator-" + translatorVersion + "/"}, Provides: []string{"translator"}, Sources: []Source{ OCISource{ Registry: "ghcr.io", Repository: "go-tex/texmf/translator", Reference: translatorVersion, Label: "ghcr.io/go-tex/texmf/translator:" + translatorVersion, }, HTTPSource{ URL: "https://github.com/josephwright/translator/archive/refs/tags/v" + translatorVersion + ".zip", Label: "upstream tag josephwright/translator v" + translatorVersion, }, }, }
Translator is the translator package: the word-by-word translation mechanism beamer loads for every talk (beamerbasetranslator.sty requires it and then asks for six dictionaries).
Without it a talk loses the words a theme puts on the slide rather than the author: an undefined \translate leaves its own argument's braces behind, so every theorem and definition was headed "{Theorem} 2." and "{Definition} 1." instead of the language's own word — "Théorème", "Définition" — since the dictionaries carry 21 languages and this corpus is full of talks that are not in English.
The route is the tag's own source archive rather than a release asset: translator publishes no assets, and CTAN's install path (install/macros/latex/contrib/translator.tds.zip) names no version — it serves whatever is current, so it could not carry a pin. The archive's single prefix therefore carries the version, which is what a GitHub source zip unpacks into.
Functions ¶
func UpstreamURL ¶
UpstreamURL returns the bundle's upstream route, which is by convention its last one: the registry mirrors exist to be tried first, and the publisher needs the source they mirror.
Types ¶
type Bundle ¶
type Bundle struct {
// Name and Version identify the bundle, and together form its cache
// directory ("beamer@3.77"). Version is the upstream release's own version,
// so a cache entry is unambiguous.
Name string
Version string
// Sources are tried in order until one yields the archive. The first is
// normally a registry the operator controls; the last should be the upstream
// release, so the bundle stays reachable when the registry is not.
Sources []Source
// SHA256 is the archive's digest, lowercase hex. Every source must produce
// exactly these bytes — a source is a delivery route, never a trust anchor.
SHA256 string
// Prefixes are the archive directories holding the TeX macro files; only
// entries under one of them are extracted. Everything else in a TDS archive
// (documentation, sources, PDFs) is thousands of files the engine will never
// open.
//
// More than one is the normal case rather than the exception: a package that
// serves both plain TeX and LaTeX splits itself between tex/generic/<name>/
// and tex/latex/<name>/, and needs both halves to load. The files are
// flattened by base name on the way out, so the split does not survive into
// the tree the engine sees.
Prefixes []string
// Provides are the package names a document may ask for by \usepackage that
// this bundle answers. A bundle is named for the distribution it comes from,
// and a document names the .sty file it wants — which is almost never the
// same word. No document writes \usepackage{pgf} to get TikZ; it writes
// \usepackage{tikz}, and pgf is the archive that holds tikz.sty.
//
// These are the .sty basenames the bundle ships, so the list is a fact about
// the archive rather than a guess about callers.
Provides []string
}
A Bundle names one upstream release and says which of its files a TeX engine should be able to see. It is a value, not a fetch: nothing happens until Open.
func Lookup ¶
Lookup returns the catalogue bundle that answers a name — its own, or one of the package names it Provides. Matching only the bundle name would mean \usepackage{tikz} reaches nothing, since no archive is called tikz.
func WithDependencies ¶ added in v0.3.0
WithDependencies returns b preceded by everything it requires, in load order and without repeats.
type HTTPSource ¶
type HTTPSource struct {
// URL is fetched with GET, following redirects.
URL string
// Label names the route in messages ("upstream release josephwright/beamer
// v3.77"). Empty falls back to the URL.
Label string
// Client is used when set; nil means a client with a sane timeout.
Client *http.Client
}
HTTPSource fetches an archive from one URL. It is the plain route: a release asset published by the upstream project, pinned to an exact version so the bytes never move under us.
type OCISource ¶
type OCISource struct {
// Registry is the host ("ghcr.io").
Registry string
// Repository is the path within it ("go-tex/texmf/beamer").
Repository string
// Reference is a tag ("3.77") or a digest ("sha256:…").
Reference string
// Label names the route in messages. Empty builds one from the coordinates.
Label string
// Client is used when set; nil means a client with a sane timeout.
Client *http.Client
}
OCISource pulls an archive stored as a single-layer OCI artifact — the shape a registry the operator controls can serve, so the default route to a bundle is theirs rather than a third party's.
It speaks just enough of the distribution API to pull one blob: anonymous token auth, one manifest, one layer. Pulling one artifact does not need a registry client library, and not depending on one keeps this module's dependencies to the standard library.
type Options ¶
type Options struct {
// CacheDir overrides where extracted bundles live. Empty means
// <user cache>/go-tex/texmf, which is what the CLI wants.
CacheDir string
// Offline forbids every network fetch: a bundle already in the cache is
// used, and a bundle that is not returns ErrNotCached. This is what an
// air-gapped or reproducible build sets.
Offline bool
// Log, when set, is called with one line per notable step (which source was
// tried, what it produced). Nil discards them. The CLI wires this to its own
// stderr so a download is never silent.
Log func(string)
}
Options configures Open.
type Source ¶
type Source interface {
// Describe names the route for error messages ("upstream release
// josephwright/beamer v3.77").
Describe() string
// Fetch returns the archive bytes, or an error.
Fetch(ctx context.Context) ([]byte, error)
}
A Source is one route to a bundle's archive bytes. Fetch must return the complete archive; the digest is checked by the caller, so a Source is never trusted to validate itself.
type Tree ¶
type Tree struct {
// contains filtered or unexported fields
}
A Tree is an extracted bundle. Resolve answers the engine's file lookups from it, and is safe for concurrent use.
func FromArchive ¶ added in v0.2.0
FromArchive builds a Tree from bytes the caller already has, checking them against the bundle's pinned digest. No network, no filesystem.
This is the entry a BROWSER actually needs. Neither ghcr.io nor the GitHub release sends an Access-Control-Allow-Origin header (measured), so a page cannot fetch either of them itself — the bytes have to arrive some other way: same-origin next to the page, a CDN that does send the header, the Cache API, or a bundled asset. Whichever it is, the host does the fetching and this does the verifying, so the digest still decides.
func Open ¶
Open makes the bundle available, fetching and extracting it only if the cache does not already hold it. The returned Tree reads from disk lazily.
func OpenInMemory ¶ added in v0.2.0
OpenInMemory makes the bundle available without touching the filesystem: it fetches the archive, checks the digest and keeps the files in memory.
This is what a BROWSER needs. Go's js/wasm filesystem shim answers ENOSYS to most calls, so Open — which extracts into the user cache — cannot run there, even though the very reason Options.Resolve exists is to serve a host with no filesystem. Under node or wasip1, where a real filesystem is bridged in, Open works and is the better choice: it caches.
Nothing is cached here, so every call fetches. A host that compiles more than once should keep the returned Tree, or keep the bytes itself and answer from its own store.
func (*Tree) Dir ¶
Dir is where the tree was extracted, or "" for a tree held in memory. Useful for a caller that would rather put it on TEXINPUTS than go through Resolve.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
texmf-pin
command
Command texmf-pin prints one catalogue bundle's pin as key=value lines, so that the publish workflow mirrors exactly the version and digest the Go code names — and cannot drift from it.
|
Command texmf-pin prints one catalogue bundle's pin as key=value lines, so that the publish workflow mirrors exactly the version and digest the Go code names — and cannot drift from it. |