texmf

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: BSD-3-Clause Imports: 17 Imported by: 0

README

texmf — go-tex

ci License Go Coverage

Gets a TeX engine the support tree a document asks for, on a machine with no TeX distribution installed. Pure Go, no cgo, standard library only.

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, offers itself "under the LaTeX Project Public License and/or under the GNU Public License" — and a download is not a redistribution, so nothing here has to choose between those two licences on a user's behalf.

Use

tree, err := texmf.Open(ctx, texmf.Beamer, texmf.Options{})
if err == nil {
    opt.Resolve = tree.Resolve // github.com/go-tex/engine Options.Resolve
}

Tree.Resolve has exactly the shape the engine's Options.Resolve wants: a TeX engine asks for beamerbasetitle.sty, never for a path.

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.

Why it is worth fetching

Measured over 10025 real beamer talks:

documents rendered pages typeset
with beamer.cls 99.9% 87 209 (~8.7 per talk)
engine emulation only 92.3% 14 162 (~1.4 per talk)

The difference is not an edge case; it is most of the content.

How a bundle arrives

Routes are tried in order, and the digest is the only trust anchor — a route is a delivery mechanism, never an authority:

  1. the cache (<user cache>/go-tex/texmf/beamer@3.77), so a second run is offline whatever the options say;
  2. ghcr.io/go-tex/texmf/beamer, a registry this project controls, so the default route does not depend on a third party staying up;
  3. the upstream release, so the bundle stays reachable when the registry is not.

Only the files a TeX engine opens are extracted: tex/latex/beamer/ and nothing else. A TDS archive's doc/ and source/ trees are thousands of files that never reach the disk.

Options.Offline forbids every fetch: a cached bundle is used, an absent one returns ErrNotCached. That is what an air-gapped or reproducible build sets.

Tests

go test ./...100% statement coverage, including every filesystem and transport failure, on six 64-bit architectures, three operating systems and both wasm targets.

The suite never touches the network: it runs against a local httptest registry and temporary directories, so it is as deterministic on a laptop as on a runner. One test does use the network and is skipped unless TEXMF_NETWORK is set — CI runs it to check the pinned digests still describe what upstream serves, so a re-cut release is found there rather than by a user with a cold cache.

License

BSD-3-Clause, © the go-tex/texmf authors. The material it fetches carries its own licence, from its own publisher, onto the user's own machine.

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

View Source
var All = map[string]Bundle{
	Beamer.Name: Beamer,
}

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.

View Source
var Beamer = Bundle{
	Name:    "beamer",
	Version: beamerVersion,
	SHA256:  "2ab4acf4c6be0d96d3f18161b08fd8e56ab54d380686f6440a42705e48c76f76",
	Prefix:  "tex/latex/beamer/",
	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:

  1. ghcr.io/go-tex/texmf/beamer — a registry the project controls, so the default route does not depend on a third party staying up;
  2. 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.

View Source
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.

Functions

func UpstreamURL

func UpstreamURL(b Bundle) (string, bool)

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

	// Prefix is the archive directory holding the TeX macro files; only entries
	// under it are extracted. Everything else in a TDS archive (documentation,
	// sources, PDFs) is thousands of files the engine will never open.
	Prefix 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

func Lookup(name string) (Bundle, bool)

Lookup returns a catalogue bundle by name.

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.

func (HTTPSource) Describe

func (s HTTPSource) Describe() string

Describe implements Source.

func (HTTPSource) Fetch

func (s HTTPSource) Fetch(ctx context.Context) ([]byte, error)

Fetch implements Source.

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.

func (OCISource) Describe

func (s OCISource) Describe() string

Describe implements Source.

func (OCISource) Fetch

func (s OCISource) Fetch(ctx context.Context) ([]byte, error)

Fetch implements Source: resolve the manifest, then pull its single layer.

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 Open

func Open(ctx context.Context, b Bundle, opt Options) (*Tree, error)

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 (*Tree) Dir

func (t *Tree) Dir() string

Dir is where the tree was extracted. Useful for a caller that would rather put it on TEXINPUTS than go through Resolve.

func (*Tree) Names

func (t *Tree) Names() []string

Names lists the base names the tree can answer for, sorted. Mostly for tests and for a caller reporting what it got.

func (*Tree) Resolve

func (t *Tree) Resolve(name string) ([]byte, bool)

Resolve returns the bytes of one file by its base name, which is how a TeX engine asks: "beamerbasetitle.sty", not a path. It matches the signature of the engine's Options.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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL