feat(cli): --bundle — single-file distribution with optional attached tape (#413)#550
Merged
Conversation
ba3a01c to
3a0d187
Compare
… tape (#413) Closes #413 `eigenscript --bundle app.eigs out [--with-tape tape]` copies the runtime binary and appends an archive: the script (always the first entry), the adjacent eigs_modules/ tree (whole tree — dependency analysis can't see dynamic imports and the superset is cheap), the stdlib lib/ modules (so `import json` works on a bare machine), and optionally a trace tape. One executable, runs anywhere. Startup (eigs_bundle_selfexec, called before any flag handling) detects the 24-byte trailer magic on the binary's own tail, extracts entries to a mkdtemp dir (removed at exit), and rewrites argv to run the extracted script — the script's dir becomes the resolve base, so every existing resolution rule just works: no VFS, no resolver changes. In bundle mode all args belong to the bundled script; only `--replay` (first arg) is ours — it points EIGS_REPLAY at the extracted tape, making a tape-carrying bundle an executable bug report that replays byte-identically with no environment. The #411 version contract is pinned by construction: the tape names its recording version and the bundle carries that exact binary. Format (fmt 1, little-endian): [u32 plen][path][u64 size][bytes]* entries + [u64 off][u32 count][u32 fmt][EIGSBNDL] trailer. Torn archives (misparsed entry headers, traversal paths) refuse with exit 3. Re-bundling from a bundle copies only the runtime image, so archives don't accrete. src/bundle.c is CLI_ONLY — excluded from embed/LSP/freestanding/fuzz builds like main.c/repl.c/step.c. tests/test_bundle.sh (suite [42g], 8 checks): script+modules+stdlib from bare cwd, arg passthrough, byte-identical --replay serving the recorded nondet value, no-tape refusal, torn-archive refusal, re-bundle size stability. docs/BUNDLE.md documents the format and positions ouroboros AOT as the native-speed tier of the same story. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm
3a0d187 to
7393c56
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #413
eigenscript --bundle app.eigs out [--with-tape tape]copies the runtime binary and appends an archive — the script, the adjacenteigs_modules/tree, the stdliblib/modules (soimport jsonworks on a bare machine), and optionally a trace tape — producing one executable that runs on a machine with no EigenScript checkout.How it runs
Startup (
eigs_bundle_selfexec, before any flag handling) detects a 24-byte trailer magic on the binary's own tail, extracts entries to amkdtempdir (removed at exit), and rewrites argv to run the extracted script. The script's directory becomes the resolve base, so every existing resolution rule just works —import name→eigs_modules/name/name.eigs,import json→lib/json.eigs. No VFS, no resolver changes. In bundle mode all arguments belong to the bundled script; only--replay(first arg) is intercepted.The executable bug report
With a tape attached,
out --replayre-runs the recorded run byte-identically, serving every nondet input from the tape — no network, no env, no luck. The #411 version contract is pinned by construction: the tape names its recording version and the bundle carries that exact binary, so the pair can't drift.Format & hardening
Fmt 1 (little-endian, same x86-64 assumption as the JIT):
[u32 plen][path][u64 size][bytes]*+[u64 off][u32 count][u32 fmt][EIGSBNDL]. Torn archives (misparsed headers, traversal paths) refuse with exit 3 rather than running garbage; re-bundling from a bundle copies only the runtime image so archives don't accrete.src/bundle.cis CLI_ONLY — excluded from embed/LSP/freestanding/fuzz builds.Tests & docs
tests/test_bundle.sh(suite [42g], 8 checks): script+modules+stdlib from a bare cwd (cd /), arg passthrough toargs of null, byte-identical--replaywhose output equals the recorded run's stdout, no-tape refusal (exit 3), torn-archive refusal (exit 3), re-bundle size stability.docs/BUNDLE.md: format, the tape-carrying story, and ouroboros AOT documented as the native-speed tier of the same single-file deliverable.eigenscript --bundle examples/data_pipeline.eigs pipeline && ./pipeline(verified from/).Validation
Release suite 2745/2745 with [42g] green; ASan+UBSan
detect_leaks=1full run green (the one earlier failure was the test's own oracle comparing print formatting against the tape's%.17gserialization — fixed to compare replay stdout against the recorded run's stdout, which is the stronger check anyway).🤖 Generated with Claude Code
https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm