Skip to content

Make doc builds report failure and rerun PythonTeX - #87

Open
dbosk wants to merge 17 commits into
masterfrom
fix/doc-build-failures
Open

dbosk wants to merge 17 commits into
masterfrom
fix/doc-build-failures

Conversation

@dbosk

@dbosk dbosk commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Four build-infrastructure fixes, found while canvaslms's manual quietly typeset broken examples (dbosk/canvaslms#459):

  • tex.mk exit status: COMPILE.tex's rerun loop ended with break, so the recipe's status discarded every latexmk failure — a broken document built "green". Every latexmk call now ends || exit $?. Verified: a deliberately broken document goes from exit 0 to exit 2, a good one stays 0.
  • PythonTeX staleness: the ifneq (${TEX_PYTHONTEX},) ${TEX_OUTDIR}/%.pdf: ${TEX_OUTDIR}/%.pytxmcr endif block was a complete no-op — a recipe-less pattern rule adds no prerequisites in GNU make, so PythonTeX only ever ran via latexmk's missing-file fallback and edited pycode blocks kept stale output on incremental builds. PythonTeX is now a phase of COMPILE.tex (new PYTHONTEX_PASS, active only when TEX_PYTHONTEX is set): pythontex + one extra latexmk pass between pass 1 and the rerun loop. Verified on a synthetic document and on canvaslms's 888-page manual: an edited pycode block now reaches the PDF on an incremental rebuild, and a failing block fails the build.
  • latexmkrc: pythontex3 does not exist in TeX Live 2025; now pythontex --interpreter python:python3.
  • noweb.mk peer-target warnings: the multi-target pattern rule $(addprefix %,${NOWEB_SUFFIXES}): %.nw made GNU make warn "pattern recipe did not update peer target" for every tangle (238 warnings per canvaslms CLI build). Replaced with one independent single-target pattern rule per suffix (same define/foreach/eval idiom as with_suffix_target); not grouped targets (&:), since one tangle genuinely does not produce the peers. Verified: 0 warnings, all tangled outputs byte-identical.

Note on #82: its suggested deletion of the rule would break the plain %.py: %.nw form — with_suffix_target only covers %.py: %.py.nw. The independent-rules-per-suffix alternative it mentions is what this implements.

Test plan

  • Synthetic broken/good documents: exit 2/0 (was 0/0)
  • canvaslms manual: exit 2 on injected LaTeX error, 0 restored; edited pycode block re-runs on incremental build
  • canvaslms full tangle: 238 → 0 warnings, 17 .py + 23 test files byte-identical
  • Woven prose of tex.mk.nw/noweb.mk.nw typesets clean

Fixes #85
Fixes #82

🤖 Generated with Claude Code

https://claude.ai/code/session_01LP19qHRs8y2afoFxiefNBj

dbosk and others added 17 commits June 12, 2026 09:04
The prose examples like [[<<module_name.py>>=]] made noweave parse the
inner <<...>> as a real chunk reference: the raw underscore in the
chunk name reached LaTeX unescaped (breaking compilation with "Missing
$ inserted") and the reference linked to a never-defined chunk.
Examples quoting the [[...]] notation itself, like [[[[...]]]],
truncated at the first ]] and left stray bracket text in the PDF.

Literal chunk syntax is now written with noweb's @<<...@>> escape
inside [[...]], and literal double brackets with \texttt and split
brackets, so the document compiles again and the examples render as
intended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NOWEAVE.tex now weaves through noweb's autolang and tominted filters
(dbosk noweb fork): each chunk is syntax-highlighted with minted using
a per-chunk lexer, and -autodefs python3 fills the identifier index
with -autolang gating the autodefs filters so chunks in other
languages stay out of the index.  The chunk-name-equals-filename
convention our tangling rules already enforce is what drives the
language inference.

The bundled Pygments lexer keeps chunk references hyperlinked inside
Python strings; noweb.mk provides a rule copying it from noweb's
library directory (read from the LIB= line of the noweave script),
and documents declare a dependency on it, as makefiles.pdf now does.
Documents must load minted and compile with -shell-escape, which our
own build already did.

NOWEAVE.pdf keeps the classic rendering: its preamble is generated by
noweave and cannot load minted.  Everything is set with ?=, so
projects without the fork override NOWEAVEFLAGS.tex.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The autodefs filters now index make targets such as upload and
autocommit, which turns every [[...]] quote of those names into a
hyperlinked identifier use --- including the ones in section
headings, where hyperref cannot survive a link inside the moving
argument (the PDF bookmark breaks with "Undefined control sequence
\hyper@@link").  Headings now write \texttt where body prose keeps
the quoted, linked form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
noweb now ships autodefs filters for shell and make (and Haskell,
Rust and Java), gated by the same @language annotations -autolang
provides, so stacking them indexes every chunk by exactly the filter
that understands it.  The default stack --- Python, shell, make ---
covers what our projects mix into one document: program modules,
helper scripts and their build files; our own woven documentation
now indexes its make variables and targets.

The stack stays at three because noweave has seven filter slots and
-autolang, each -autodefs and tominted occupy one each; the default
leaves two slots free for project filters, and other languages
remain a one-line NOWEAVEFLAGS.tex override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The caution claimed the default stack leaves two of noweave's seven
filter slots free.  It missed that -index inserts two filters
(finduses + noidx); with -autolang and tominted also taking one each,
the three-autodefs default fills all seven exactly.  Adding a fourth
-autodefs or a project -filter overflows unless something is dropped.

Prose-only change in a documentation chunk; the tangled noweb.mk is
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replace the seven-filter-slot caution with the dbosk fork's unbounded
  pipeline; keep the old ceiling only as a note for pre-fork noweave.
- Note that the preamble loads minted through the noweb package
  (\usepackage[minted]{noweb}) rather than a hand-added \usepackage{minted}.
- Record that noweave -minted can load minted into the generated preamble,
  so the standalone .pdf weave stays classic by choice, not necessity.

Documentation only; the tangled noweb.mk is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-added \usepackage{minted} (plus a separate
\usepackage{noweb}) with \usepackage[minted]{noweb}, so the minted
dependency rides the noweb package the way tominted weaves expect.
\setminted{autogobble} still applies, as minted is loaded (via
\AtEndOfPackage) before it runs.

Requires the noweb fork whose noweb.sty declares the minted option;
to pass minted options such as outputdir, load minted directly before
\usepackage{noweb} instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dbosk fork's noweave, with -index, runs -autolang's inference as a
pre-pass to discover which languages a document contains and stacks the
matching autodefs filter for each. The default weave therefore no
longer lists -autodefs python3/sh/make: discovery covers them, and an
explicit -autodefs is still merged without duplication for chunks whose
language the name does not reveal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test chunks follow the convention <<test [[foo.py]]>>, labelling the
file rather than being it, and autolang deliberately does not infer a
language from such a labelled name. Left unclassified, a test chunk is
scanned by every autodefs filter and salts the index with
cross-language false matches. Add one -langrule per supported language
(python, shell, make) so each test chunk is classified, and hence
indexed, by the same filter as the module it exercises.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A pattern rule with several targets is one rule whose recipe must produce
all of them, not a shorthand for several rules.  The general tangling rule
listed every supported suffix as a target, but notangle(1) only ever
extracts the chunk named by $@, so GNU make 4.3 and later warned

  noweb.mk:99: warning: pattern recipe did not update peer target 'X.c'.

once per unmade peer.  Building the canvaslms CLI modules emitted 238 such
warnings, drowning out real errors.

Generate one independent pattern rule per suffix instead, the same way the
suffixed-source rules are already generated.  The tangled output is
unchanged: all 17 CLI modules tangle byte-identically, with zero warnings.
The PythonTeX custom dependency in latexmkrc invoked pythontex3.  That
wrapper existed only to select the Python 3 interpreter while pythontex
still defaulted to Python 2; TeX Live no longer ships it (TeX Live 2025
installs pythontex alone), so the cus_dep could only ever fail with
"command not found".

Call pythontex and pass --interpreter python:python3 explicitly, matching
the default of PYTHONTEXFLAGS in tex.mk.
COMPILE.tex ran latexmk and then a bounded rerun loop, joined with ";".  A
recipe reports the status of its last command, and that is the loop, which
ends by running "break" -- so every latexmk failure was discarded and
"make" reported success on a document that had not compiled.  The same held
for the reruns on the DVI path.

End every latexmk invocation with "|| exit $?" so the first failing pass
ends the recipe with its own status, instead of spending the remaining
passes on a document already known to be broken.

Verified with a minimal document containing an undefined control sequence:
"make bad.pdf" exited 0 before this change and exits 2 after it, while a
well-formed document still exits 0.
PythonTeX was wired in with

  ifneq (${TEX_PYTHONTEX},)
  ${TEX_OUTDIR}/%.pdf: ${TEX_OUTDIR}/%.pytxmcr
  endif

which does nothing: a recipe-less *pattern* rule cannot build anything, so
make never selects it and never attaches its prerequisites to the pattern
rule that does have a recipe.  PythonTeX therefore ran only through
latexmk's -use-make fallback, which fires only while the .pytxmcr file is
missing.  On every later build an edited pycode block kept the previous
run's output.

Attaching the prerequisite to the rule with the recipe would not have been
enough either: the chain from .pytxmcr bottoms out in the master .tex file,
while the executed code normally lives in a chapter the master inputs --- and
those inputs are declared as prerequisites of the PDF, not of the .aux file.

Make it a phase of COMPILE.tex instead, between the pass that writes the
.pytxcode file and the pass that reads the macros back, so PythonTeX is
consulted whenever the PDF is rebuilt for any reason the project's makefile
knows about.  This is cheap: pythontex re-executes only the sessions whose
hash changed, and latexmk skips the extra pass unless the .pytxmcr file
actually changed.

Verified with a document whose \input'ed chapter holds the pycode block:
editing the block and rebuilding incrementally kept the old output before
this change and produces the new output after it.  A pycode block that exits
nonzero now fails the build (exit 2) instead of passing silently.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant