Skip to content

Commit a68d2fc

Browse files
luisleo526claude
andauthored
feat(docker): engine image -> pure runtime base (#36)
Drop the bundled pineforge-codegen transpiler and the Pine entrypoint from the engine image: it is now a pure base (g++, Eigen, python3, libpineforge.a + headers) that compiles + runs a pre-transpiled generated.cpp against the C ABI. The full `.pine -> trades` pipeline (transpiler + entrypoint + run_json) lives in pineforge-release, which builds FROM this base. A bare `docker run` prints a migration message (exit 64). Corpus REGEN now transpiles via the pineforge-release container (regen_corpus_cpp.sh IMAGE default + run_corpus.sh / README). docker/entrypoint.sh + docker/run_json.py stay in-repo as the tested harness source (test_entrypoint_indir + fingerprint self-test); pineforge-release ships its own copy. Validated: ctest 72/72; corpus parity excellent=245/anomaly=1 (baseline); the release container transpiles all 246 byte-identically to the engine image (drift vs committed cpp is pre-existing, image-independent). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5d57cc9 commit a68d2fc

4 files changed

Lines changed: 40 additions & 52 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,10 @@ git clone https://github.com/pineforge-4pass/pineforge-engine.git
407407
cd pineforge-engine
408408
git submodule update --init corpus
409409
410-
# (Optional) Reproduce the C++ from strategy.pine via the engine image.
410+
# (Optional) Reproduce the C++ from strategy.pine via the pineforge-release image.
411411
# VERIFY=1 proves the shipped generated.cpp is byte-identical to the
412412
# transpiler output (drift guard, no overwrite); drop it to regenerate.
413-
docker pull ghcr.io/pineforge-4pass/pineforge-engine:latest
413+
docker pull ghcr.io/pineforge-4pass/pineforge-release:latest
414414
VERIFY=1 scripts/regen_corpus_cpp.sh
415415
416416
# Build + run + verify TV parity. (Add REGEN=1 to regenerate the C++

docker/Dockerfile

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
# PineForge runtime container.
1+
# PineForge runtime base image — PURE RUNTIME, no transpiler.
22
#
33
# Stage 1 builds libpineforge.a and stages the public headers.
4-
# Stage 2 is a thin runtime: g++, python3, the prebuilt static
5-
# library, and the JSON harness. Each `docker run` compiles the
6-
# user-supplied generated.cpp against libpineforge.a and runs it
7-
# against the user-supplied OHLCV, emitting a JSON report on stdout.
4+
# Stage 2 is a thin runtime base: g++, Eigen, python3, the prebuilt static
5+
# library, and the public headers. It compiles + runs a *pre-transpiled*
6+
# generated.cpp against libpineforge.a (the C ABI).
7+
#
8+
# This image deliberately does NOT bundle the PineScript->C++ transpiler
9+
# (pineforge-codegen) and has NO Pine entrypoint. The full `.pine -> trades`
10+
# pipeline (transpiler + entrypoint + JSON harness) lives in the combined
11+
# image **ghcr.io/pineforge-4pass/pineforge-release**, which builds FROM this
12+
# base. Run a `.pine` there, not here.
813
#
914
# Build args (release.yml passes these; sane defaults for local builds):
1015
# PINEFORGE_VERSION semver "MAJOR.MINOR.PATCH" (defaults to VERSION file)
1116
# PINEFORGE_GIT_SHA full commit sha
1217
#
1318
# Build:
14-
# docker build -t pineforge -f docker/Dockerfile .
15-
# Run:
16-
# docker run --rm \
17-
# -v $(pwd)/strategy.cpp:/in/strategy.cpp:ro \
18-
# -v $(pwd)/ohlcv.csv:/in/ohlcv.csv:ro \
19-
# pineforge > report.json
19+
# docker build -t pineforge-engine -f docker/Dockerfile .
2020

2121
# === Stage 1: build libpineforge.a =====================================
2222
FROM debian:bookworm-slim AS builder
@@ -27,16 +27,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2727
ca-certificates \
2828
git \
2929
libeigen3-dev \
30-
python3-pip \
3130
&& rm -rf /var/lib/apt/lists/*
3231

33-
# Stage the pure-Python transpiler here so the runtime never needs pip or
34-
# network: target-install gives a plain directory tree to COPY across.
35-
# PINNED: the codegen version is explicit so image builds are reproducible AND
36-
# the buildx (gha) layer cache busts when we need a newer codegen. Bump this in
37-
# lockstep with a pineforge-codegen release.
38-
RUN pip3 install --break-system-packages --no-cache-dir -U --target /opt/codegen pineforge-codegen==0.7.5
39-
4032
WORKDIR /src
4133
# VERSION file is the version source-of-truth inside the build context;
4234
# release.yml writes the bumped value before `docker build` so the
@@ -57,52 +49,46 @@ RUN cmake -B build \
5749
# stage 2 can copy a single tree.
5850
RUN cp build/include/pineforge/version.h include/pineforge/version.h
5951

60-
# === Stage 2: runtime =================================================
52+
# === Stage 2: runtime base ============================================
6153
FROM debian:bookworm-slim
6254

6355
ARG PINEFORGE_VERSION=unknown
6456
ARG PINEFORGE_GIT_SHA=unknown
6557
ARG PINEFORGE_BUILD_DATE=unknown
6658

67-
LABEL org.opencontainers.image.title="pineforge" \
68-
org.opencontainers.image.description="Deterministic PineScript v6 backtest runtime" \
59+
LABEL org.opencontainers.image.title="pineforge-engine" \
60+
org.opencontainers.image.description="Deterministic PineScript v6 backtest runtime (pure base; transpiler ships in pineforge-release)" \
6961
org.opencontainers.image.version="${PINEFORGE_VERSION}" \
7062
org.opencontainers.image.revision="${PINEFORGE_GIT_SHA}" \
7163
org.opencontainers.image.created="${PINEFORGE_BUILD_DATE}" \
7264
org.opencontainers.image.source="https://github.com/pineforge-4pass/pineforge-engine" \
7365
org.opencontainers.image.licenses="Apache-2.0"
7466

75-
# g++ + Eigen to compile the generated TU; python3 for the JSON harness and
76-
# the bundled PineScript->C++ transpiler (pineforge-codegen, target-installed
77-
# in the builder stage and COPY'd in — the runtime carries no pip, no
78-
# ca-certificates, and never touches the network). With codegen baked in, the
79-
# container accepts strategy.pine directly and transpiles locally — no hosted
80-
# API, no API key, source never leaves the box.
67+
# g++ + Eigen to compile a pre-transpiled generated.cpp against the static lib.
68+
# python3 stays: the pineforge-release image builds FROM this base and its
69+
# JSON harness (run_json.py) needs it — keeping it here avoids re-installing it
70+
# in the release layer. The bare engine image carries NO transpiler and NO Pine
71+
# entrypoint (see header); those live in pineforge-release.
8172
RUN apt-get update && apt-get install -y --no-install-recommends \
8273
g++ \
8374
libeigen3-dev \
8475
python3 \
8576
&& rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/locale /usr/share/info
8677

87-
# Bundle the prebuilt static library, the public headers (curated +
88-
# generated version.h), and the JSON harness. Users only mount their
89-
# own code.
90-
RUN mkdir -p /opt/pineforge/lib /opt/pineforge/include /opt/pineforge/bin
91-
COPY --from=builder /opt/codegen /opt/pineforge/pycodegen
78+
# Bundle the prebuilt static library and the public headers (curated +
79+
# generated version.h). Consumers link their own compiled strategy against this.
80+
RUN mkdir -p /opt/pineforge/lib /opt/pineforge/include
9281
COPY --from=builder /src/build/lib/libpineforge.a /opt/pineforge/lib/
9382
COPY --from=builder /src/include/pineforge /opt/pineforge/include/pineforge
9483

95-
COPY docker/run_json.py /opt/pineforge/bin/run_json.py
96-
COPY docker/entrypoint.sh /opt/pineforge/bin/entrypoint.sh
97-
RUN chmod +x /opt/pineforge/bin/entrypoint.sh /opt/pineforge/bin/run_json.py
98-
9984
ENV PINEFORGE_PREFIX=/opt/pineforge \
10085
PINEFORGE_VERSION=${PINEFORGE_VERSION} \
101-
PINEFORGE_GIT_SHA=${PINEFORGE_GIT_SHA} \
102-
PYTHONPATH=/opt/pineforge/pycodegen
86+
PINEFORGE_GIT_SHA=${PINEFORGE_GIT_SHA}
10387

104-
# Drop root: the entrypoint compiles and runs everything under mktemp dirs.
88+
# Drop root.
10589
RUN useradd -r -u 10001 -s /usr/sbin/nologin pineforge
10690
USER pineforge
10791

108-
ENTRYPOINT ["/opt/pineforge/bin/entrypoint.sh"]
92+
# No ENTRYPOINT: this is a base/runtime image, not a standalone Pine tool.
93+
# Make a bare `docker run` self-documenting instead of silently doing nothing.
94+
CMD ["sh", "-c", "echo 'pineforge-engine is a pure runtime base (compile + run a pre-transpiled generated.cpp against /opt/pineforge/lib/libpineforge.a). For PineScript .pine -> backtest, use ghcr.io/pineforge-4pass/pineforge-release.' >&2; exit 64"]

scripts/regen_corpus_cpp.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
#!/usr/bin/env bash
22
# scripts/regen_corpus_cpp.sh — regenerate (or verify) every corpus
33
# generated.cpp straight from strategy.pine, using the bundled transpiler
4-
# in the pineforge-engine Docker image. Docker is the only dependency —
4+
# in the pineforge-release Docker image. Docker is the only dependency —
55
# no host Python, pip, or C++ toolchain needed for this step.
66
#
77
# This closes the reproducibility loop: the shipped corpus/*/*/generated.cpp
8-
# can be re-derived from corpus/*/*/strategy.pine through the public engine
9-
# image (which bundles pineforge-codegen in transpile-only mode).
8+
# can be re-derived from corpus/*/*/strategy.pine through the public
9+
# pineforge-release image (engine runtime + bundled pineforge-codegen),
10+
# in transpile-only mode. NOTE: the bare pineforge-engine image no longer
11+
# bundles the transpiler — REGEN must use pineforge-release.
1012
#
1113
# Env vars:
12-
# IMAGE Engine image to transpile with
13-
# (default: ghcr.io/pineforge-4pass/pineforge-engine:latest)
14+
# IMAGE Image to transpile with
15+
# (default: ghcr.io/pineforge-4pass/pineforge-release:latest)
1416
# ONLY Substring filter; only process strategies whose path matches
1517
# VERIFY 1 = do NOT overwrite; transpile to a temp file and diff against
1618
# the committed generated.cpp. Exit non-zero if any file drifts.
@@ -29,7 +31,7 @@ set -euo pipefail
2931
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
3032
cd "$ROOT_DIR"
3133

32-
IMAGE="${IMAGE:-ghcr.io/pineforge-4pass/pineforge-engine:latest}"
34+
IMAGE="${IMAGE:-ghcr.io/pineforge-4pass/pineforge-release:latest}"
3335
VERIFY="${VERIFY:-0}"
3436

3537
log() { printf '\033[1;34m[regen_corpus]\033[0m %s\n' "$*"; }

scripts/run_corpus.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ fi
4242

4343
# --- 0) (optional) regenerate generated.cpp from strategy.pine --------
4444
# REGEN=1 re-derives every corpus/*/*/generated.cpp from its strategy.pine
45-
# through the engine Docker image (which bundles the transpiler), so the
46-
# build below compiles freshly-transpiled C++ instead of the committed copy.
45+
# through the pineforge-release Docker image (engine + bundled transpiler), so
46+
# the build below compiles freshly-transpiled C++ instead of the committed copy.
4747
# Requires Docker. Honours ONLY. Off by default → committed C++ is used.
4848

4949
if [[ "${REGEN:-0}" == "1" ]]; then
50-
log "regenerating generated.cpp from strategy.pine via the engine image"
50+
log "regenerating generated.cpp from strategy.pine via the pineforge-release image"
5151
"$ROOT_DIR/scripts/regen_corpus_cpp.sh"
5252
fi
5353

0 commit comments

Comments
 (0)