Faster repeated LaTeX builds
  • Python 57.4%
  • C 30.2%
  • Roff 6.8%
  • TeX 3.7%
  • Shell 1.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
निरंजन 7e55363678
chore: version 0.2.1
Set the version to 0.2.1 in the program, the man page and
pyproject.toml.  NEWS gets a 0.2.1 entry for the SyncTeX fix, which
moves there from the 0.2.0 entry.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-14 16:05:18 +05:30
src/aint chore: version 0.2.1 2026-11-14 16:05:18 +05:30
testbook fix: rename the program 2026-11-14 14:46:43 +05:30
tools docs: correct the SyncTeX note and small slips 2026-11-14 14:49:29 +05:30
aint.1 chore: version 0.2.1 2026-11-14 16:05:18 +05:30
AUTHORS fix: rename the program 2026-11-14 14:46:43 +05:30
COPYING AnT 0.1.0: faster repeated LaTeX builds 2026-11-09 21:08:45 +05:30
install.sh fix: rename the program 2026-11-14 14:46:43 +05:30
MESSAGES chore: version 0.2.1 2026-11-14 16:05:18 +05:30
NEWS chore: version 0.2.1 2026-11-14 16:05:18 +05:30
pyproject.toml chore: version 0.2.1 2026-11-14 16:05:18 +05:30
README fix: catch SyncTeX's busy file in engines with zlib built in 2026-11-14 15:42:09 +05:30

aint (aint is not TeX)
======================

aint is free software that makes repeated LaTeX builds faster.  Its command
is `aint'.  It needs no root access and changes neither the TeX engine nor
your document.

While TeX runs, aint keeps frozen copies ("snapshots") of the running
engine: one just before \begin{document} and one before each \include.
After an edit, aint continues from the last snapshot before the change, so
the preamble, its packages and fonts, and all unchanged chapters before
the edit are not processed again.

You give aint the TeX command you would type without it, as with env or
time; aint's own options come before it:

    aint lualatex thesis             draft build (default): fast,
                                     references not updated
    aint --final lualatex thesis     final build: references, citations
                                     and table of contents up to date
    aint -v pdflatex thesis          also show aint's details (passes,
                                     snapshots, memory)
    aint pdflatex -synctex=1 thesis  TeX's options go to TeX unchanged
    aint -q thesis                   short for `aint lualatex thesis'

`man aint' has the full manual.


Contents
--------

  1. What you see
  2. Installation
  3. How it works
  4. Snapshots on disk
  5. Measured results
  6. Building without snapshots
  7. Options and environment
  8. Limitations
  9. Files in this package
 10. Copyright and license


1. What you see
---------------

TeX's own terminal output, followed by aint's messages:

    Output written on talk.pdf (14 pages, 74150 bytes).
    Transcript written on talk.log.
    aint: 1.8s instead of ~5.6s for a plain run; saved 3.8s (68% less time).

Each message is one line and starts with `aint:', as with other Unix
commands; --silent (or AINT_SILENT) leaves out all but error messages.  The
comparison uses the time of the last full run of the document.  When a
build saves no time, aint says so and gives the reason.  Every message
aint can print is listed, with examples, in the file MESSAGES; all texts
live in src/aint/messages.py.

aint runs your command as you typed it, in the current folder, and adds
only -recorder right after the command name: it needs the list of files
TeX reads, which TeX writes to <job>.fls.  (XeTeX also gets -no-pdf, see
section 3.)  TeX's questions, after an error or from \typein, appear
on your terminal and take your answers, and Ctrl-C reaches TeX, which
answers with its "Interruption" prompt; this holds for resumed builds too.

On a resumed build, aint first prints what TeX printed up to the snapshot
(saved with it), then the live output, so the terminal transcript is the
same as a plain run's with the same options and -recorder.  So is the
.log file, except for two memory-statistics lines at the very end (aint
defines one extra macro).  Errors, warnings and SyncTeX refer to your
real file names and line numbers.


2. Installation
---------------

aint itself needs only Python 3.9 or newer.  It installs and runs without
TeX, but to turn a document into a PDF it calls a TeX engine (lualatex,
pdflatex, xelatex, ...), so for that you need a TeX distribution such as
TeX Live, with LaTeX 2020-10 or newer.

For snapshots, aint also needs:

  * GNU/Linux (aint uses LD_PRELOAD and /proc; it is not tested
    elsewhere);
  * a C compiler (cc, gcc or clang) for the snapshot library, which aint
    compiles from src/aint/aintsnap.c on first use and caches in
    ~/.cache/aint/ (in /tmp/aint-<uid>/ if that path contains a space or a
    colon, which LD_PRELOAD cannot handle);
  * an x86-64 machine.

Without a C compiler or on another processor, aint builds with plain runs
(section 6).

No root access is needed:

    ./install.sh              installs ~/.local/bin/aint and the man page
    ./install.sh --system     installs into /usr/local (uses sudo)
    ./install.sh --uninstall  removes aint again

The `aint' command is a single executable file (a Python zip application).
If ~/.local/bin is not on your PATH, the installer says what to add.


3. How it works
---------------

The core idea of process checkpointing is to capture a running process and
later continue from that exact point.  Tools such as CRIU do this from
outside the process, which needs root or special privileges.  aint lets the
engine copy itself instead, with fork(), which any program may do.

  * A small library, src/aint/aintsnap.c, is loaded into the unmodified
    engine with LD_PRELOAD.  The engine is started with your command, as
    in section 1 (`lualatex -recorder talk').

  * Pause points.  When the engine opens your main file, the library hands
    it the same text with two hooks added at the start of line 1, so line
    numbers do not change.  TeX still prints and records your file's real
    name.  The hooks make TeX open a special file just before
    \begin{document} and before every \include that TeX actually runs
    (not those skipped by \includeonly or a false conditional).  When the
    library sees that file opened, it notes the position of every open
    file and forks:

      - the copy freezes and becomes a snapshot.  It waits for commands
        and shows up in ps as `aint-snap-<n>'.  It marks itself as the first
        process the kernel's out-of-memory killer should stop, so if
        memory runs out, the kernel ends snapshots before your other
        programs (other memory managers, such as systemd-oomd or
        earlyoom, may choose differently);

      - the original reports to aint, waits while aint saves copies of the
        output files (.pdf, .aux, .log, ...), and carries on typesetting.

  * Resuming.  On the next run, aint puts the output files back as they
    were at the snapshot and tells that frozen copy to fork again.  The
    new copy restores every file position and continues typesetting from
    the pause point; for the main file it also discards text that had been
    read ahead, so an edited body is read fresh.  It reads its terminal
    input from the `aint' command that resumed it and gets back the
    engine's own signal handlers, so TeX can ask questions and Ctrl-C
    works as without aint.  The frozen copy stays frozen (holding no
    terminal) and can be resumed again and again.

  * Memory.  A snapshot holds what the engine holds at that point, mostly
    fonts and packages.  fork() is copy-on-write, so further snapshots of
    the same run only cost the memory pages changed in between: all 13
    snapshots of the 220-page test book took 330 to 400MB in our tests.
    `aint -v' shows the figure for your document.

  * Lifetime.  Snapshots are ordinary processes, so they survive between
    runs of aint and the closing of the terminal.  They end:

      - at a reboot, like every process;
      - at a logout only if the system ends a user's processes then (with
        systemd, when KillUserProcesses is on in logind.conf; distributions
        differ);
      - 8 hours after the document's last build (AINT_TTL; any build
        counts, even one that resumes no snapshot);
      - with `aint --clean', or within half a minute after aint's cache
        folder for the document is deleted;
      - if the kernel runs out of memory (see above).

    aint does not watch for any of this.  At the next build it finds that
    the snapshots are gone, and restores the preamble snapshot from disk
    (section 4) or makes a full run.

  * Which engine.  aint finds it from the program your command finally
    runs: pdflatex and pdflatex-dev run pdfTeX, lualatex runs LuaHBTeX,
    xelatex runs XeTeX.  Any command that runs pdfTeX, LuaTeX (also
    LuaHBTeX, LuaJITTeX) or XeTeX works.

  * Which file.  TeX's first line is what follows the options: usually the
    file name, as in `lualatex thesis'.  It may also be TeX code, as in
    `pdflatex '\nonstopmode\input paper.ltx''; aint then takes the file of
    the first \input as the main file.  Without an \input, aint runs the
    command as it is, without snapshots, and says so.  The file is looked
    for as TeX does: in the current folder, then on TeX's search path (as
    kpsewhich reports it), so `aint pdflatex sample2e' works.  If TeX
    would not find the file either, aint runs the command as it is and
    TeX reports the problem.

  * Where aint keeps its data.  Everything goes to ~/.cache/aint/
    ($XDG_CACHE_HOME/aint/ if that is set), one folder per document,
    readable only by you.  Nothing is written next to the document except
    TeX's own output (including <job>.fls), so a folder synchronised
    between computers (Nextcloud, Syncthing, ...) carries no snapshots;
    each computer keeps its own.  One build of a document runs at a time:
    a second one (from an editor that builds on every save, say) waits
    for the first.

  * Draft and final builds.  LaTeX reads the previous run's .aux (and
    .toc, ...) at \begin{document} and writes new ones during the run.  A
    normal build always does one pass, so a new or moved label shows as
    ?? or with an old number until you run `aint --final'.  That reruns
    from the preamble snapshot (taken before the .aux is read) until the
    .aux, .toc and similar files stop changing, like latexmk (at most 4
    passes).  In drafts, references keep the values read by the last
    build that started at \begin{document} (a full run, a final build, or
    an edit that resumed from the preamble snapshot); a draft that resumes
    from a chapter snapshot uses what that snapshot read.  The text and
    page layout are always current.

  * Change tracking.  Every file the engine reads is recorded (-recorder)
    and assigned to the part of the run that first read it: the preamble,
    the main file's body, or the stretch after one of the pauses.  The
    parts follow what TeX actually did, so \includeonly, conditionals and
    \include inside an \input file are handled correctly.  A change to a
    file read in the preamble needs a full run; any change to the main
    file's body resumes from the preamble snapshot; any other change
    resumes from the snapshot just before the part that first read the
    file.  The library also records every file TeX looked for but did not
    find in the document's folder, so a new chapter, image, .bbl (after
    biber) or .ind (after makeindex) is noticed too.  Files that other
    programs write for TeX are also watched by name: .bbl, .ind, .gls,
    .acr, .nls, .glstex, and glossary types declared in the .aux.  More
    can be added with AINT_WATCH.

  * Edits during a build.  A file saved while a build is running may or
    may not have been read by TeX already.  aint records each input as it
    was when the build started, so the next build takes such an edit as a
    change and none is lost.

  * Errors.  Snapshots taken before a LaTeX error stay valid, so after
    you fix the error aint resumes just before the chapter that failed.

  * Interrupted builds.  If aint is ended while it builds (kill, a closed
    terminal), it ends the TeX engine too.  The output files are then
    incomplete, and aint knows: the next build rebuilds them even if no
    input changed.

  * XeTeX runs with -no-pdf, so snapshots never involve its separate PDF
    converter (a second process, which a snapshot cannot hold); aint runs
    it once at the end, as XeTeX would (xdvipdfmx -q -E).  If you give
    -no-pdf yourself, aint leaves the .xdv file as it is.

  * SyncTeX.  TeX writes <job>.synctex(busy) during the run and renames
    it to <job>.synctex.gz at the end.  A snapshot holds the busy file
    open, so the snapshot library keeps that file in aint's cache (it
    catches the file however the engine opens it, also in engines with
    zlib built in, as TeX Live's own binaries are), and the renaming
    becomes a copy to <job>.synctex.gz.  So snapshots work with
    SyncTeX on.  For pdfTeX and LuaTeX the .synctex.gz file is the same as
    a plain run's; XeTeX's names the .xdv file as its output (it runs with
    -no-pdf), and viewers find the same positions in it.


4. Snapshots on disk
--------------------

After a build that made a new preamble snapshot, a copy of that snapshot
writes itself to aint's cache (section 3) in the background, unless it
would be larger than AINT_SAVE_LIMIT, so it survives a reboot.  Only what
is needed to rebuild it is stored:

  * memory backed by files (the engine, its libraries, mapped fonts) is
    mapped again from the files; only pages that differ are stored;
  * empty pages are skipped;
  * the rest is compressed with zstd (zlib if zstd is not installed).

Saving happens in a copy of the frozen engine, which reads the memory of a
pristine child of its own (a parent may read its child's memory without
root), so no build waits for it.

Restoring needs no root either.  aint starts the engine exactly as it
started the original: same command, recorded environment, and
address-space randomisation switched off for the engine (allowed for any
user), so the binary, libraries, stack and vDSO land at the same
addresses.  Before the engine's main() runs, the snapshot library
replaces the process's memory with the image, reopens its files at their
positions, restores signal handlers, working directory and thread pointer,
and continues from the frozen point.

An image is used only on the same kernel, with the same engine binary,
libraries and mapped files; otherwise aint makes a full run and says why.
AINT_SAVE_LIMIT (in MB, default 20) sets the largest image kept; 0 turns
saving off.  Images contain the engine's memory, including its environment
variables, which is why aint's cache is readable only by you.

Measured as an ordinary user:

    document                     engine memory  on disk  restore
    article, pdfLaTeX                     28MB    6.7MB     0.1s
    article, XeLaTeX                     113MB    8.4MB     0.2s
    article, LuaLaTeX                     96MB   13.5MB     0.2s
    220-page book, LuaLaTeX               90MB   13.3MB     0.2s
    talk with a Japanese font            719MB   55.5MB     1.1s
                                                 (over the default limit)


5. Measured results
-------------------

Test files are in testbook/ (run `python3 make_chapters.py' there first).
Machine: TeX Live 2023, LuaLaTeX, as an ordinary user.

book.tex: 220 pages, 12 \include'd chapters with TikZ, pgfplots, siunitx,
hyperref and cleveref.  A plain lualatex run takes 8.5s.  Medians of three
draft builds after an edit:

    edit in            aint    compared with a plain run
    chapter 12         0.77s   11 times as fast
    chapter 11         1.45s   5.8 times as fast
    chapter 9          2.63s   3.2 times as fast
    chapter 6          4.54s   1.9 times as fast
    chapter 1          7.54s   1.1 times as fast
    preamble           9.09s   7% slower (a full run)
    nothing changed    0.08s   build skipped

article.tex (the same preamble, one chapter, 20 pages): 2.07s plain,
0.76s with aint after an edit (63% less time).

Correctness: after `aint --final', the book's PDF was compared with plain
lualatex run on the same sources; all 220 pages were identical, also after
an edit that changes references across chapters.  The same held for
pdfLaTeX and XeLaTeX, and for builds restored from disk.


6. Building without snapshots
-----------------------------

When snapshots are not possible (no C compiler, not an x86-64 machine), or
with --no-snapshots, aint builds with plain runs:

  * pdfTeX, and XeTeX when the preamble loads no system fonts: the
    preamble is cached once in a private format (mylatexformat) and
    reused until it changes, unless TeX's first line is TeX code;
  * LuaTeX, and XeTeX with system fonts: these fonts cannot be stored in
    a format file, so the preamble is processed normally;
  * all engines: reruns until references settle with --final, and no run
    at all when no input changed.


7. Options and environment
--------------------------

    aint [aint options] TeX-command [TeX options] file
    aint [aint options] -q file

aint's options come first; the first word that is not one of them starts
the TeX command (`--' ends aint's options explicitly).

    -q, --quick          the file only, with the default engine:
                         `aint -q thesis' is `aint lualatex thesis'
    -F, --final          rerun until references, citations and table of
                         contents are up to date
    -d, --draft          one pass; references not updated (the default)
    -v, --verbose        also show passes, changed files, snapshots and
                         their memory
    --silent             show only TeX's output and aint's error messages
    --no-snapshots       build with plain runs instead of snapshots
    -f, --force          stop snapshots, discard cached state, build from
                         scratch
    --clean              stop a document's snapshots, delete its cache,
                         and exit.  Name the document as for a build
                         (`aint --clean lualatex thesis') or just by its
                         name (`aint --clean thesis'; thesis.tex and
                         thesis.ltx mean the same).  The file itself need
                         not exist any more.
    -V, --version        show version, copyright and license
    -h, --help           show a short help text

TeX's options go to TeX unchanged (-shell-escape, -synctex=1,
-interaction=batchmode, -file-line-error, -jobname=NAME, ...).  aint does
not support these yet, and says so: -ini, -fmt (and &format),
-output-directory, -aux-directory, -output-format and -output-driver.  A
change of the TeX command makes the next build a full run.

    AINT_ENGINE          the command for -q instead of lualatex; it may
                         have options ("pdflatex -shell-escape")
    AINT_SILENT          if set, as --silent
    AINT_TTL             seconds snapshots live after the document's last
                         build (default 28800, i.e. 8 hours)
    AINT_SAVE_LIMIT      largest preamble snapshot kept on disk, in MB
                         (default 20; 0 = never)
    AINT_WATCH           more generated-file extensions to watch
    AINT_SHIM            use this snapshot library instead of building one
    AINT_DEBUG           if set, explain why a snapshot could not be used


8. Limitations
--------------

  * Snapshots use memory while they live, roughly what the engine itself
    needs for your fonts and packages.  Use `aint --clean' when done, or a
    lower AINT_TTL.
  * Snapshots need \begin{document} in the main file itself.  Other
    documents -- plain TeX, or a main file that \inputs another with
    \begin{document} -- are built with plain runs (section 6); -v says
    so.
  * Chapter snapshots happen only at \include; documents that use only
    \input get the preamble snapshot.  With \includeonly, only the
    chapters it includes get snapshots.
  * The TeX options -ini, -fmt, -output-directory, -aux-directory,
    -output-format and -output-driver are not supported yet.
  * Snapshots keep the output files (.pdf, .aux, ...) open.  If another
    program replaces one of them (some synchronisation tools do), the
    snapshots that hold it cannot be used, and aint starts from an earlier
    one or makes a full run.
  * Only the preamble snapshot is saved on disk; after a reboot the first
    build resumes from the preamble and makes the chapter snapshots again.
  * Disk images need the same kernel and TeX installation as when they were
    saved (checked; otherwise a full run).  Where processes may not read
    their children's memory (Yama ptrace scope 2 or 3), images cannot be
    saved; snapshots in memory still work.
  * The engine runs with address-space randomisation switched off, which
    restoring images requires.  This affects only the engine aint starts.
  * aint's hooks are added to the start of line 1 of the copy of the main
    file that the engine reads, so a %&format line or a byte-order mark at
    the very start of the file is not recognised.
  * The pause uses read stream 15 (\openin15) for an instant; it would
    clash only with a document that keeps stream 15 open across
    \begin{document} or an \include.
  * \today, time stamps and \write18 side effects come from the run that
    made the snapshot.
  * The engine must be dynamically linked (true for TeX Live and the
    usual distribution packages), because the library is loaded with
    LD_PRELOAD.


9. Files in this package
------------------------

    README                 this file
    COPYING                the GNU General Public License, version 3
    AUTHORS                who wrote aint
    NEWS                   changes between versions
    MESSAGES               every message, the help page and the man page
    aint.1                 the man page
    install.sh             builds the single-file `aint' and installs it
    pyproject.toml         alternative installation with pip or pipx
    src/aint/cli.py        command line
    src/aint/messages.py   every text aint prints
    src/aint/snapshot.py   builds with snapshots
    src/aint/aintsnap.c    the snapshot library (LD_PRELOAD)
    src/aint/shim.py       compiles and locates the snapshot library
    src/aint/fallback.py   builds without snapshots
    src/aint/common.py     shared helpers
    tools/catalogue.py     regenerates MESSAGES from the source
    testbook/              test documents (book.tex, article.tex,
                           talk.tex, make_chapters.py)


10. Copyright and license
-------------------------

Copyright © 2026 निरंजन (Niranjan)

aint is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your
option) any later version.

aint is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with aint.  If not, see <https://www.gnu.org/licenses/>.

aint was written with the help of Claude Opus 5.5, a large language model
(LLM) made by Anthropic.  Courts are divided on whether material
generated by an LLM can be protected by copyright.  Should it be found
that the copyright claimant cannot hold copyright in any part of aint,
that part is in the public domain.  Wherever copyright does hold, aint is
distributed under the terms of the GNU General Public License, version 3
or (at your option) any later version, as stated above.