Bundle a static pdftotext into the binary

The PDF parsers shell out to pdftotext, so every machine running money needed
poppler-utils installed. scripts/build-bundled.sh now builds a deployable
binary that carries its own: pdftotext is compiled in a container from a
checksum-pinned poppler release as a fully static musl executable, then
embedded with `go build -tags bundled`. The result is one file that runs on
any Linux of that architecture with nothing installed alongside it.

It is still the real pdftotext, run as a subprocess. Linking poppler through
cgo would have cost the pure-Go build, and its C++ text API is not guaranteed
to space columns the way pdftotext -layout does, which is what the parsers
were tuned on. Only what text extraction needs is compiled in -- no
fontconfig, cairo or image codecs -- and its output is byte-identical to a
full distro build on the same PDF.

At runtime the embedded copy is written to the user cache directory, not
/tmp, which servers often mount noexec. It is named by content hash, so a
newer build never runs an older copy, and verified before reuse, so a write cut
short by a killed process is replaced rather than trusted. `money config` says
which pdftotext is in use.

The tag is opt-in: plain go build and go test never need the 5 MB executable,
which is gitignored rather than committed. Building with the tag for anything
but linux/amd64 or linux/arm64 fails with a message saying so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 18:02:46 +02:00
co-authored by Claude Opus 5.5
parent 6faf99719a
commit 5847245638
12 changed files with 398 additions and 3 deletions
+27 -1
View File
@@ -56,6 +56,31 @@ Give it the package path, not `cmd/money/main.go`. Naming the file puts the go
tool in file mode: it names the binary after the source file (`main`) and
compiles only the files listed, which breaks the moment the package has two.
That build runs `pdftotext` from `PATH` for the PDF parsers, so the machine
needs poppler-utils. To deploy one file with nothing to install alongside it,
build with `pdftotext` inside:
```
scripts/build-bundled.sh # → dist/money-linux-amd64
scripts/build-bundled.sh arm64 # → dist/money-linux-arm64
```
It needs podman or docker: `pdftotext` is compiled in a container from a
checksum-pinned poppler release (`scripts/pdftotext.Containerfile`), as a fully
static musl executable with only what text extraction needs, then embedded with
`go build -tags bundled`. The result runs on any Linux of that architecture.
The first PDF import writes the embedded copy to `~/.cache/money/` (or
`$XDG_CACHE_HOME/money/`) and runs it from there; `money config` says which
`pdftotext` is in use. Only Linux on amd64 and arm64 is offered — elsewhere,
install poppler-utils.
Building arm64 from an x86 machine runs the container under emulation, so the
host needs qemu-user-static and the first build is slow; the executable is then
cached under `internal/parser/bundled/` and later builds only rebuild money.
Poppler is GPL, so a bundled binary is GPL-licensed as a whole. That matters
only if you hand the binary to someone else.
## Usage
```
@@ -672,7 +697,8 @@ parser = "traderepublic"
The two PDF parsers shell out to `pdftotext -layout` (poppler-utils), exactly
as the Python versions did; its layout reconstruction is what makes the
column-based parsing work.
column-based parsing work. A build made with `scripts/build-bundled.sh`
carries its own copy instead (see Install).
Amount parsing is shared and deliberately tolerant: `1.234,56`, `-45.20`,
`45,20-`, `(45.20)` and `45.20 EUR` all work, whichever parser reads them.