Build System¶
← Documentation · Architecture · Building documents · Validation, tests, and CI
latex/tools/build.py is the repository's document build entry point. It discovers LaTeX documents, selects targets, compiles them into isolated output directories, updates generated README content, and verifies tracked generated fixtures.
latex/tools/package_notes.py owns the separate packaging step that turns complete course build outputs into release-ready assets.
Document discovery¶
The build system discovers independent main.tex entry points under:
Each discovered main.tex is treated as one buildable document.
Review the output owns output-path examples, course-PDF policy, and fixture replacement. This reference describes the underlying selection and generation behavior.
Target selection¶
Exactly one selection mode is required:
| Mode | Selection |
|---|---|
| Explicit target | One or more document directories or main.tex files |
--all |
Every course, component example, and integration project |
--changed-from REVISION |
Documents affected by changes from REVISION to HEAD |
--changed-file-list FILE |
Documents affected by NUL-terminated repository-relative paths listed in a file |
--changed-to may be used with --changed-from to change the end revision from its default of HEAD. Both revisions must be available locally. This selects from committed differences only; it does not discover staged, unstaged, or untracked edits. Compilation still reads the current working tree, not a checkout of the selected revision.
Both changed-file modes preserve accented filenames, spaces, and embedded newlines. Moves are treated as a deletion plus an addition so both the original and receiving documents are selected. To prepare a file list outside the TeX container:
mkdir -p .build
git diff --no-renames --name-only -z --diff-filter=ACMRD origin/main HEAD -- \
> .build/changed-files.nul
Pass that file to --changed-file-list .build/changed-files.nul. Legacy newline-delimited lists must be regenerated with -z; they are rejected rather than silently misinterpreted. An empty file is a valid no-op.
Changed-file selection is intentionally conservative:
| Changed area | Build impact |
|---|---|
| File inside one course | That course |
| File inside one component example | That example |
| File inside one integration example | That integration project |
Shared class, .sty component package, .otf/.ttf font, compose.yaml, build.py, latex_source.py, or validation/publication workflow |
Every document |
| Unrelated documentation or policy | No LaTeX document |
Complete archive publication uses --all; reduced selection is only an optimization for development and validation.
Compilation¶
Each selected document is compiled with latexmk and LuaLaTeX into a mirrored .build/ directory. The tool deletes that document's previous output directory first, so builds do not reuse auxiliary state. Compilation runs from the source directory, which keeps course-relative includes working.
The effective compilation uses:
latexmk
-lualatex
-Werror
-halt-on-error
-interaction=nonstopmode
-file-line-error
-outdir=<mirrored .build directory>
main.tex
-Werror makes latexmk fail when its final warning summary reports unresolved references or citations, multiply defined labels, or missing characters. Temporary first-pass reference warnings are allowed while latexmk reruns the document; other package warnings are not automatically fatal. A failed compilation does not replace tracked PDF fixtures or generated README content.
The environment adds latex/ to TEXINPUTS, fixes the source-date environment, and uses UTC to reduce avoidable output differences.
Canonical builds use the pinned texlive service defined in compose.yaml.
After compilation, the generated PDF, table of contents, logs, and auxiliary files remain under:
For course work, .build/<year>/<course>/main.pdf is the PDF to review locally.
Generated README content¶
For courses and integration projects, build.py reads the compiled main.toc and generates a localized Markdown section between:
Content outside those markers is preserved.
Course README sections link to the stable asset name in the rolling notes-latest release and include non-empty course-code and channel metadata. The link does not guarantee that the asset has been published or matches the local build. Integration projects link to their tracked local PDF. Component examples do not receive generated README content.
The README includes supported .toc entries, including unnumbered entries added with \addcontentsline. A literal preamble \setcounter{tocdepth}{N} filters entries to the requested depth; if several occur, the last is used.
Do not edit generated sections manually.
Generated-state verification¶
--check-generated recompiles and compares README content and example PDFs byte for byte against the working-tree fixtures. It does not modify those fixtures or check whether a fixture is staged; repository and Git review handle tracking separately.
It checks:
- generated course and integration README sections;
- tracked component-example PDFs;
- tracked integration PDFs.
Course PDFs are deliberately excluded from tracked-PDF comparison because normal course output belongs only under .build/.
Repository validation separately rejects generated course PDFs that are forced into Git.
Build options¶
| Option | Behavior |
|---|---|
--no-compile |
Reuse existing .build/ PDF and .toc; examples may fall back to a source-adjacent PDF, and .toc may fall back to a source-adjacent file |
--no-readme |
Compile without updating generated README content |
--keep-going |
Continue through all selected documents and report failures afterward |
--clean |
Remove the repository-level .build/ directory after success |
--check-generated |
Verify generated tracked state without replacing it |
--check-generated cannot be combined with --no-compile or --no-readme.
Without --keep-going, processing stops at the first failure.
Release packaging¶
After a complete build, latex/tools/package_notes.py converts course PDFs into deterministic release assets under .build/release/. Invoke it with release metadata supplied by the publishing workflow:
python3 latex/tools/package_notes.py \
--source-commit <40-character-sha> \
--release-timestamp <iso-8601-timestamp-with-utc-offset> \
--release-title "Latest compiled notes"
The source commit must be a full SHA. The release timestamp must include a UTC offset, for example 2026-08-04T12:00:00+00:00 or its equivalent Z form. Publishing uses the source commit timestamp so repeated packaging of the same source remains deterministic.
The packaging step:
- discovers direct course entry points under
1/,2/, and3/; - requires the matching compiled PDF under
.build/; - derives stable asset names such as
1-analisi-matematica-1.pdf; - rejects invalid paths and naming collisions;
- copies the built PDF bytes without modifying them;
- reads canonical course metadata from
\unipdsetup; - creates a sorted manifest and release index;
- generates SHA-256 checksums;
- succeeds with an empty manifest and explanatory release index when the course archive is empty.
A discovered course with no matching compiled PDF, a missing or unreadable non-empty course name, or invalid release metadata causes packaging to fail. Full course-metadata validation is a separate earlier check.
Packaging clears .build/release/ before assembling assets. It does not compile, verify PDF freshness, or prove that PDF bytes correspond to --source-commit; the publishing workflow ensures a clean checkout and complete build first.
The release staging directory contains:
.build/release/
├── <year>-<course-slug>.pdf
├── manifest.json
├── SHA256SUMS.txt
└── RELEASE_NOTES.md
Packaging owns asset naming, metadata generation, checksums, and release staging. GitHub workflows publish these outputs but should not duplicate that logic.
For publication behavior, workflow permissions, CI artifacts, and rolling or snapshot releases, see Validation, tests, and CI.
For local commands and normal student workflows, see Building documents and Docker builds.