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:
@@ -0,0 +1,79 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakePdftotext stands in for the real executable: what matters here is how it
|
||||
// is put on disk, not what it does once there.
|
||||
var fakePdftotext = []byte("#!/bin/sh\necho bundled\n")
|
||||
|
||||
// The bundled copy has to land somewhere it can be exec'd, and run.
|
||||
func TestInstallBundledRuns(t *testing.T) {
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
path, err := installBundled(fakePdftotext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := exec.Command(path).Output()
|
||||
if err != nil {
|
||||
t.Fatalf("run %s: %v", path, err)
|
||||
}
|
||||
if string(out) != "bundled\n" {
|
||||
t.Errorf("output %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// A copy already in place is reused, but only once its contents check out: a
|
||||
// write cut short by a killed process must not become what every later run
|
||||
// trusts.
|
||||
func TestInstallBundledReplacesADamagedCopy(t *testing.T) {
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
path, err := installBundled(fakePdftotext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, fakePdftotext[:5], 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := installBundled(fakePdftotext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again != path {
|
||||
t.Errorf("reinstalled to %s, want %s", again, path)
|
||||
}
|
||||
if got, _ := os.ReadFile(path); string(got) != string(fakePdftotext) {
|
||||
t.Errorf("damaged copy kept: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Each build's copy has its own name, so a newer binary never runs an older
|
||||
// one's pdftotext left in the same cache.
|
||||
func TestInstallBundledNamesByContent(t *testing.T) {
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
a, err := installBundled(fakePdftotext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := installBundled(append([]byte(nil), "#!/bin/sh\necho newer\n"...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a == b || filepath.Dir(a) != filepath.Dir(b) {
|
||||
t.Errorf("paths %s and %s, want two names in one directory", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Without -tags bundled, nothing is embedded and PATH decides, as before.
|
||||
func TestUnbundledUsesPath(t *testing.T) {
|
||||
if len(bundledPdftotext) != 0 {
|
||||
t.Skip("built with -tags bundled")
|
||||
}
|
||||
if cmd, err := pdftotextCommand(); err != nil || cmd != "pdftotext" {
|
||||
t.Errorf("command = %q, %v; want pdftotext from PATH", cmd, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user