Distribution: standalone binary, man page, shell completions
ober
66248060639e8f441ca7f5555e3eea5998f8a605
--- a/.gitignore +++ b/.gitignore @@ -16,6 +16,11 @@ jpgp-musl.sha256 jpgp-macos.sha256 jpgp +# Binary build intermediates +*-main.c +*.wp.so +*_boot.h + # Editor / OS .DS_Store *.swp --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ JERBOA_HOME ?= $(realpath $(CURDIR)/../jerboa) SCHEME ?= $(JERBOA_HOME)/.chez/bin/scheme BIN_DIR := $(HOME)/.local/bin +LIB_DIR := $(HOME)/.local/lib +MAN_DIR := $(HOME)/.local/share/man/man1 NATIVE_DIR := $(CURDIR)/pgp-native NATIVE_RELEASE := $(NATIVE_DIR)/target/release @@ -10,7 +12,7 @@ else NATIVE_LIB := $(NATIVE_RELEASE)/libjpgp_native.so endif -.PHONY: help run test test-interop build-native binary install clean +.PHONY: help run test test-interop build-native binary install install-script clean .DEFAULT_GOAL := help help: @@ -50,18 +52,52 @@ test-interop: build-native JERBOA_HOME=$(JERBOA_HOME) JPGP_REPO=$(CURDIR) SCHEME=$(SCHEME) \ bash test/interop-gpg.sh +BINARY_OUTPUT ?= jpgp-bin + binary: build-native - @echo "Native binary build not yet implemented — use 'make run' or 'make install-script'" + @echo "==> Building self-contained $(BINARY_OUTPUT) binary" + JPGP_REPO=$(CURDIR) SCHEME=$(SCHEME) JERBOA_HOME=$(JERBOA_HOME) \ + JERBOA_CHEZ_PREFIX=$(JERBOA_HOME)/.chez \ + bash support/build-binary.sh support/binary-entry.ss $(BINARY_OUTPUT) + @echo "" + @echo "Built ./$(BINARY_OUTPUT). The native lib is still required at runtime:" + @echo " $(NATIVE_LIB)" + @echo " /usr/local/lib/libjpgp_native.{dylib,so}" + @echo " /opt/homebrew/lib/libjpgp_native.{dylib,so}" + @echo " or set JPGP_NATIVE_LIB=/path/to/libjpgp_native.{dylib,so}" install-script: build-native - mkdir -p $(BIN_DIR) + mkdir -p $(BIN_DIR) $(MAN_DIR) printf '#!/bin/sh\nexec %s -q --libdirs %s:%s/lib --script %s/pgp/main.ss -- "$$@"\n' \ "$(SCHEME)" "$(CURDIR)" "$(JERBOA_HOME)" "$(CURDIR)" \ > $(BIN_DIR)/jpgp chmod +x $(BIN_DIR)/jpgp + cp doc/jpgp.1 $(MAN_DIR)/jpgp.1 @echo "Installed jpgp launcher to $(BIN_DIR)/jpgp" + @echo "Installed man page to $(MAN_DIR)/jpgp.1" + @echo "" + @echo "Shell completions are in completions/ — copy to your distro's path:" + @echo " bash: completions/jpgp.bash -> ~/.local/share/bash-completion/completions/jpgp" + @echo " zsh : completions/_jpgp -> any directory in \$$fpath" + @echo "" @echo "(This wraps the dev interpreter; for a static binary see PLAN.md)" +install: binary + mkdir -p $(BIN_DIR) $(LIB_DIR) $(MAN_DIR) + cp $(BINARY_OUTPUT) $(BIN_DIR)/jpgp + chmod +x $(BIN_DIR)/jpgp + cp $(NATIVE_LIB) $(LIB_DIR)/$(notdir $(NATIVE_LIB)) + cp doc/jpgp.1 $(MAN_DIR)/jpgp.1 + @echo "" + @echo "Installed:" + @echo " $(BIN_DIR)/jpgp" + @echo " $(LIB_DIR)/$(notdir $(NATIVE_LIB))" + @echo " $(MAN_DIR)/jpgp.1" + @echo "" + @echo "Shell completions are in completions/ — copy to your distro's path:" + @echo " bash: completions/jpgp.bash -> ~/.local/share/bash-completion/completions/jpgp" + @echo " zsh : completions/_jpgp -> any directory in \$$fpath" + clean: cd $(NATIVE_DIR) && cargo clean find . -name '*.so' -not -path './pgp-native/*' -delete --- a/README.md +++ b/README.md @@ -19,12 +19,15 @@ in OpenPGP format using [rPGP] — so the recipient can decrypt with plain ## Status -Pre-alpha. v1 implements: +v1 implements: -- `keygen` / `pubkey` -- `encrypt` / `decrypt` (age) -- `encrypt -r FOO.asc` (OpenPGP outbound interop via rPGP) -- `sign` / `verify` (Ed25519) +- `keygen` / `pubkey` / `list` / `fingerprint` (SSH-style `SHA256:`) +- `encrypt` / `decrypt` (age, multi-recipient, symmetric) +- `encrypt -r FOO.asc` — OpenPGP outbound (rPGP, SEIPDv1) +- `decrypt --pgp-key SECRET.asc` — OpenPGP inbound +- `sign` / `verify` (Ed25519 native, `--pgp-key` produces OpenPGP detached sigs) +- Self-contained `jpgp-bin` (Chez `compile-program` + boot-embedded) +- Bidirectional gpg interop tested end-to-end (`make test-interop`) See `PLAN.md` for the design and out-of-scope items. @@ -49,10 +52,24 @@ Requires Chez Scheme + [Jerboa] checked out at `~/mine/jerboa` (or set ``` make run ARGS='version' # interpreter mode make test # smoke tests -make binary # native `jpgp` binary -make install # → ~/.local/bin/jpgp +make test-interop # bidirectional gpg interop tests +make binary # native `jpgp` binary (5MB Mach-O / ELF) +make install # → ~/.local/bin/jpgp + ~/.local/lib/libjpgp_native ``` +## Interop with gpg + +`jpgp encrypt -r alice.asc -i secret.txt -o secret.asc` produces a SEIPDv1 +(RFC 4880) OpenPGP message that any version of gpg can decrypt. + +The reverse direction (`jpgp decrypt --pgp-key alice.sec.asc -i msg.asc`) +works when the gpg recipient key does **not** advertise AEAD preferences +in its self-signature. Modern GnuPG 2.5 defaults to its proprietary OCB +packet (tag 20), which rPGP cannot parse. To produce a compatible key in +gpg, drop AEAD from the default preferences (`features=0x05` rather than +the default `0x07`). See `test/interop-gpg.sh` for a working +`~/.gnupg/gpg.conf`. + ## License ISC new file mode 100644 --- /dev/null +++ b/completions/_jpgp @@ -0,0 +1,86 @@ +#compdef jpgp +# zsh completion for jpgp. +# Install to a directory in your $fpath, e.g. /usr/local/share/zsh/site-functions/_jpgp, +# or add `fpath+=(<dir-containing-_jpgp>)` before `compinit` in ~/.zshrc. + +_jpgp() { + local -a commands + commands=( + 'keygen:Generate a new identity' + 'pubkey:Print or write the public-key line' + 'list:Show identity info' + 'fingerprint:Print SHA256: fingerprint' + 'encrypt:Encrypt data to recipients or a passphrase' + 'decrypt:Decrypt age, jpgp, or OpenPGP data' + 'sign:Produce a jpgp or OpenPGP signature' + 'verify:Verify a jpgp or OpenPGP signature' + 'version:Print version' + 'help:Show help' + ) + + if (( CURRENT == 2 )); then + _describe 'command' commands + return + fi + + local cmd="$words[2]" + case "$cmd" in + keygen) + _arguments \ + '--out[output path]:identity file:_files' \ + '-o[output path]:identity file:_files' + ;; + pubkey) + _arguments \ + '--identity[identity file]:file:_files' \ + '--out[output path]:file:_files' \ + '-o[output path]:file:_files' + ;; + fingerprint|list) + _arguments \ + '--identity[identity file]:file:_files' + ;; + encrypt) + _arguments \ + '*-r[recipient]:recipient:_files' \ + '*--recipient[recipient]:recipient:_files' \ + '-i[input file]:file:_files' \ + '--in[input file]:file:_files' \ + '-o[output file]:file:_files' \ + '--out[output file]:file:_files' \ + '-s[symmetric mode]' \ + '--symmetric[symmetric mode]' + ;; + decrypt) + _arguments \ + '-i[input file]:file:_files' \ + '--in[input file]:file:_files' \ + '-o[output file]:file:_files' \ + '--out[output file]:file:_files' \ + '--identity[identity file]:file:_files' \ + '--pgp-key[OpenPGP secret key]:file:_files' \ + '--pgp-pass[OpenPGP passphrase]:passphrase' \ + '-s[symmetric mode]' \ + '--symmetric[symmetric mode]' + ;; + sign) + _arguments \ + '-i[input file]:file:_files' \ + '--in[input file]:file:_files' \ + '-o[output file]:file:_files' \ + '--out[output file]:file:_files' \ + '--identity[identity file]:file:_files' \ + '--pgp-key[OpenPGP secret key]:file:_files' \ + '--pgp-pass[OpenPGP passphrase]:passphrase' + ;; + verify) + _arguments \ + '-i[input file]:file:_files' \ + '--in[input file]:file:_files' \ + '--pubkey[jpgp1 pubkey]:file:_files' \ + '--pgp-pubkey[OpenPGP pubkey]:file:_files' \ + '*:signature file:_files' + ;; + esac +} +_jpgp "$@" new file mode 100644 --- /dev/null +++ b/completions/jpgp.bash @@ -0,0 +1,59 @@ +# bash completion for jpgp +# Source from ~/.bashrc or install to /etc/bash_completion.d/ (or your distro's path). + +_jpgp() { + local cur prev words cword + _init_completion || return + + local commands="keygen pubkey list fingerprint encrypt decrypt sign verify version help" + + if [[ $cword -eq 1 ]]; then + COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) + return + fi + + local cmd="${words[1]}" + case "$cmd" in + keygen) + COMPREPLY=( $(compgen -W "--out -o" -- "$cur") ) + ;; + pubkey) + case "$prev" in + --identity|--out|-o) _filedir ; return ;; + esac + COMPREPLY=( $(compgen -W "--identity --out -o" -- "$cur") ) + ;; + fingerprint|list) + case "$prev" in + --identity) _filedir ; return ;; + esac + COMPREPLY=( $(compgen -W "--identity" -- "$cur") ) + ;; + encrypt) + case "$prev" in + -r|--recipient|-i|--in|-o|--out) _filedir ; return ;; + esac + COMPREPLY=( $(compgen -W "-r --recipient -i --in -o --out -s --symmetric" -- "$cur") ) + ;; + decrypt) + case "$prev" in + -i|--in|-o|--out|--identity|--pgp-key) _filedir ; return ;; + esac + COMPREPLY=( $(compgen -W "-i --in -o --out --identity --pgp-key --pgp-pass -s --symmetric" -- "$cur") ) + ;; + sign) + case "$prev" in + -i|--in|-o|--out|--identity|--pgp-key) _filedir ; return ;; + esac + COMPREPLY=( $(compgen -W "-i --in -o --out --identity --pgp-key --pgp-pass" -- "$cur") ) + ;; + verify) + case "$prev" in + -i|--in|--pubkey|--pgp-pubkey) _filedir ; return ;; + verify) _filedir ; return ;; + esac + COMPREPLY=( $(compgen -W "-i --in --pubkey --pgp-pubkey" -- "$cur") ) + ;; + esac +} +complete -F _jpgp jpgp new file mode 100644 --- /dev/null +++ b/doc/jpgp.1 @@ -0,0 +1,194 @@ +.Dd May 15, 2026 +.Dt JPGP 1 +.Os +.Sh NAME +.Nm jpgp +.Nd modern PGP replacement with age + Ed25519 + OpenPGP interop +.Sh SYNOPSIS +.Nm +.Ar command +.Op Ar options +.Sh DESCRIPTION +.Nm +is a small command-line tool for encrypting, signing, and verifying +data. +It uses +.Xr age 1 +for native encryption, +.Xr ed25519 7 +for native signatures, and rPGP for OpenPGP interoperability with +.Xr gpg 1 . +.Pp +By default, identities live in +.Pa ~/.jpgp/identity.age +encrypted with a user-supplied passphrase. +Each identity bundles one age secret key and one Ed25519 signing key. +.Sh COMMANDS +.Bl -tag -width "encrypt" +.It Cm keygen Op Fl -out Ar PATH +Generate a new identity. +Prompts for a passphrase. +.It Cm pubkey Oo Fl -identity Ar PATH Oc Oo Fl -out Ar PATH Oc +Print the +.Sq jpgp1 +public-key line for this identity. +.It Cm fingerprint Op Fl -identity Ar PATH +Print the SSH-style +.Sq SHA256: +fingerprint of this identity's public key line. +.It Cm list Op Fl -identity Ar PATH +Show identity paths, age pubkey, fingerprint, and full public line. +.It Xo +.Cm encrypt +.Op Fl r Ar RECIPIENT ... +.Op Fl s | -symmetric +.Op Fl i Ar IN +.Op Fl o Ar OUT +.Xc +Encrypt +.Ar IN +to one or more recipients. +.Ar RECIPIENT +may be a bare +.Sq age1... +string, a path to a +.Sq jpgp1 +public-key line, or a path to an armored OpenPGP public key. +Multiple +.Fl r +flags may be given for age/jpgp recipients (all share one ciphertext). +OpenPGP recipients require a single +.Fl r +and produce gpg-compatible output (SEIPDv1 / RFC 4880). +With +.Fl -symmetric , +prompts for a passphrase instead of using recipient keys. +.It Xo +.Cm decrypt +.Op Fl s | -symmetric +.Op Fl i Ar IN +.Op Fl o Ar OUT +.Op Fl -identity Ar PATH +.Op Fl -pgp-key Ar SECRET.asc +.Op Fl -pgp-pass Ar PASS +.Xc +Decrypt +.Ar IN . +Defaults to identity mode (age payload). +.Pp +With +.Fl -symmetric , +prompts for a passphrase. +With +.Fl -pgp-key , +decrypts an OpenPGP message using the armored secret key at the given +path. +.Nm +parses SEIPDv1 packets only (the format gpg emits when its recipient +keys do not advertise AEAD prefs); the gnupg-proprietary OCB packet +.Pq packet 20 +is not supported. +.It Xo +.Cm sign +.Op Fl i Ar IN +.Op Fl o Ar OUT +.Op Fl -identity Ar PATH +.Op Fl -pgp-key Ar SECRET.asc +.Op Fl -pgp-pass Ar PASS +.Xc +Sign +.Ar IN +with this identity. +Default output is a +.Sq jpgp +signature blob over +.Xr ed25519 7 . +With +.Fl -pgp-key , +produces a gpg-compatible detached OpenPGP signature. +.It Xo +.Cm verify +.Ar SIG +.Op Fl i Ar IN +.Op Fl -pubkey Ar FILE +.Op Fl -pgp-pubkey Ar PUB.asc +.Xc +Verify the signature at +.Ar SIG +against +.Ar IN . +The format is auto-detected by armor header: +jpgp blobs are verified with the embedded pubkey +.Pq or one supplied via Fl -pubkey , +and OpenPGP signatures with +.Fl -pgp-pubkey . +Exit status is 0 on success, 2 on bad signature. +.It Cm version +Print version. +.El +.Pp +For every command, +.Sq Fl i Sq - +means stdin and +.Sq Fl o Sq - +means stdout. +.Sh FILES +.Bl -tag -width "~/.jpgp/identity.age" +.It Pa ~/.jpgp/identity.age +Default identity file (age-armored, passphrase-encrypted). +.El +.Sh EXAMPLES +Generate an identity and print its fingerprint: +.Bd -literal -offset indent +$ jpgp keygen +$ jpgp fingerprint +SHA256:9p+rmTm... +.Ed +.Pp +Encrypt a file to a friend's age recipient: +.Bd -literal -offset indent +$ jpgp encrypt -r age1xyz... -i secrets.txt -o secrets.age +.Ed +.Pp +Encrypt to a colleague who uses gpg: +.Bd -literal -offset indent +$ jpgp encrypt -r colleague.asc -i memo.txt -o memo.asc +.Ed +.Pp +Sign a release with an OpenPGP key, gpg-compatible: +.Bd -literal -offset indent +$ jpgp sign --pgp-key release.sec.asc \\ + -i release.tar.gz -o release.tar.gz.asc +.Ed +.Sh EXIT STATUS +.Ex -std +A bad signature in +.Cm verify +exits with status 2. +.Sh INTEROPERABILITY +.Nm +encrypts to OpenPGP recipients using SEIPDv1 +.Pq RFC 4880 . +GnuPG 2.5 defaults to its proprietary OCB encrypted data packet +.Pq packet 20 , +which rPGP cannot parse. +For +.Cm decrypt +to work on a gpg-produced ciphertext, the recipient key's +self-signature must not advertise AEAD support: either generate the gpg +key with +.Sq features +byte +.Sq 0x05 +.Pq instead of the default 0x07 , +or strip the +.Sq pref-aead-algos +subpacket from the key prefs. +See +.Pa test/interop-gpg.sh +in the source tree for a working configuration. +.Sh SEE ALSO +.Xr age 1 , +.Xr gpg 1 +.Sh AUTHORS +Written by Jaime Fournier and Claude. --- a/pgp/ffi.ss +++ b/pgp/ffi.ss @@ -79,13 +79,20 @@ (define here (or (getenv "JPGP_DIR") (current-directory))) + ;; JPGP_NATIVE_LIB, if set, is tried first — used by the standalone + ;; binary to point at a sibling-installed dylib. (define *jpgp-lib-paths* - (list - (string-append here "/pgp-native/target/release/libjpgp_native" ext) - (string-append here "/pgp-native/target/debug/libjpgp_native" ext) - (string-append "/usr/local/lib/libjpgp_native" ext) - (string-append "/opt/homebrew/lib/libjpgp_native" ext) - (string-append "libjpgp_native" ext))) + (let ([base + (list + (string-append here "/pgp-native/target/release/libjpgp_native" ext) + (string-append here "/pgp-native/target/debug/libjpgp_native" ext) + (string-append "/usr/local/lib/libjpgp_native" ext) + (string-append "/opt/homebrew/lib/libjpgp_native" ext) + (string-append (or (getenv "HOME") ".") "/.local/lib/libjpgp_native" ext) + (string-append "libjpgp_native" ext))]) + (cond + [(getenv "JPGP_NATIVE_LIB") => (lambda (p) (cons p base))] + [else base]))) (define *jpgp-loaded?* (let loop ([ps *jpgp-lib-paths*]) --- a/pgp/util.ss +++ b/pgp/util.ss @@ -63,8 +63,12 @@ (lambda (p) (put-bytevector p bv))))) (def (write-stdout-bytes bv) - (put-bytevector (current-output-port) bv) - (flush-output-port (current-output-port))) + ;; current-output-port is textual; standard-output-port is the + ;; underlying binary port. Use the binary port directly so we can + ;; write raw bytevectors. + (let ([p (standard-output-port (buffer-mode block))]) + (put-bytevector p bv) + (flush-output-port p))) (def (string->bv s) (string->utf8 s)) (def (bv->string bv) (utf8->string bv)) new file mode 100644 --- /dev/null +++ b/support/binary-entry.ss @@ -0,0 +1,26 @@ +#!chezscheme +;;; binary-entry.ss — Entry point compiled into the static `jpgp` binary. +;;; +;;; Compiled by support/build-binary.sh into a single ELF/Mach-O binary that +;;; bundles petite.boot, scheme.boot, and the whole-program-optimised jpgp +;;; libraries. The native crypto remains in libjpgp_native.{dylib,so} and is +;;; loaded at runtime — set JPGP_NATIVE_LIB to override the search. + +(import (except (chezscheme) + make-hash-table hash-table? + sort sort! + printf fprintf + path-extension path-absolute? + with-input-from-string with-output-to-string + iota 1+ 1- + partition + make-date make-time) + (except (jerboa prelude) meta atom?) + (pgp cli)) + +;; Chez --script leaves a literal "--" in command-line-arguments when invoked +;; with `-- $(ARGS)`. Strip it once here so the dispatcher sees clean argv. +(let ([args (command-line-arguments)]) + (run-cli (if (and (pair? args) (string=? (car args) "--")) + (cdr args) + args))) new file mode 100755 --- /dev/null +++ b/support/build-binary.sh @@ -0,0 +1,154 @@ +#!/bin/sh +# build-binary.sh — Build a self-contained jpgp binary. +# +# Thin wrapper around Jerboa's support/build-binary.sh that passes both +# the project libdirs ($CURDIR) and the Jerboa lib so WPO can resolve all +# imports. Native crypto (libjpgp_native.{dylib,so}) is still loaded at +# runtime from /usr/local/lib, /opt/homebrew/lib, the build tree, or +# $JPGP_NATIVE_LIB. + +set -eu + +JPGP_REPO="${JPGP_REPO:-$(cd "$(dirname "$0")/.." && pwd)}" +JERBOA_HOME="${JERBOA_HOME:-$(cd "$JPGP_REPO/../jerboa" && pwd)}" +SCHEME="${SCHEME:-$JERBOA_HOME/.chez/bin/scheme}" +JERBOA_CHEZ_PREFIX="${JERBOA_CHEZ_PREFIX:-$JERBOA_HOME/.chez}" + +ENTRY="${1:-$JPGP_REPO/support/binary-entry.ss}" +OUTPUT="${2:-jpgp-bin}" + +# Detect OS and toolchain (mirrors Jerboa's build-binary.sh exactly). +OS=$(uname -s) +case "$OS" in + FreeBSD) CC_DEFAULT=cc; OS_LIBS="-lm -lpthread -lncurses -L/usr/local/lib -liconv" ;; + Darwin) CC_DEFAULT=cc; OS_LIBS="-lm -lpthread -lncurses -liconv" ;; + Linux) + CC_DEFAULT=gcc + if [ -f /usr/lib/x86_64-linux-gnu/libtinfo.so ] || [ -f /usr/lib64/libtinfo.so ]; then + OS_LIBS="-lm -ldl -lpthread -ltinfo" + else + OS_LIBS="-lm -ldl -lpthread -lncurses" + fi + ;; + *) CC_DEFAULT=cc; OS_LIBS="-lm -lpthread -lncurses" ;; +esac +CC="${CC:-$CC_DEFAULT}" + +MACHINE_TYPE=$("$SCHEME" -q <<'EOF' +(display (machine-type)) (exit) +EOF +) + +CSV_DIR="" +for prefix in "$JERBOA_CHEZ_PREFIX/lib" /usr/local/lib /usr/lib /usr/lib64 /opt/homebrew/lib /opt/local/lib; do + for d in "$prefix"/csv*/"$MACHINE_TYPE"; do + if [ -f "$d/libkernel.a" ] && [ -f "$d/scheme.h" ] && [ -f "$d/petite.boot" ]; then + CSV_DIR="$d"; break 2 + fi + done +done +if [ -z "$CSV_DIR" ]; then + echo "ERROR: cannot find Chez install for machine-type '$MACHINE_TYPE'." >&2 + exit 1 +fi + +echo "=== jpgp binary build: $ENTRY -> $OUTPUT ===" +echo " OS: $OS" +echo " CC: $CC" +echo " Chez: $CSV_DIR" +echo "" + +# Step 1: WPO compile — needs BOTH $JPGP_REPO (for (pgp ...) libs) and +# $JERBOA_HOME/lib (for (jerboa prelude), (std ...), etc.). +WPO_SO="${OUTPUT}.wp.so" +OBJ_DIR=$(mktemp -d "/tmp/jpgp-bin-obj.XXXXXX") +trap 'rm -rf "$OBJ_DIR" "$WPO_SO" petite_boot.h scheme_boot.h program_boot.h "${OUTPUT}-main.c"' EXIT + +echo "==> [1/4] WPO compile" +"$SCHEME" --libdirs "$JPGP_REPO:$JERBOA_HOME/lib" \ + --script "$JERBOA_HOME/support/build-boot.ss" "$ENTRY" "$WPO_SO" "$OBJ_DIR" +echo "" + +# Step 2: embed boot files +echo "==> [2/4] Embedding boot files as C arrays" +embed() { + in="$1"; stem="$2" + var="${stem}_data"; sz="${stem}_size"; out="$stem.h" + printf 'static const unsigned char %s[] = {\n' "$var" > "$out" + od -An -tx1 -v "$in" \ + | sed -e '/^[[:space:]]*$/d' \ + -e 's/^ *//;s/ *$//;s/ */ /g;s/ /,0x/g;s/^/0x/;s/$/,/' >> "$out" + printf '};\nstatic const unsigned int %s = sizeof(%s);\n' "$sz" "$var" >> "$out" +} +embed "$CSV_DIR/petite.boot" petite_boot +embed "$CSV_DIR/scheme.boot" scheme_boot +embed "$WPO_SO" program_boot +echo "" + +# Step 3: generate main.c +echo "==> [3/4] Generate ${OUTPUT}-main.c" +cat > "${OUTPUT}-main.c" <<'CMAIN' +/* jpgp binary entry point */ +#include "scheme.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/types.h> + +#include "petite_boot.h" +#include "scheme_boot.h" +#include "program_boot.h" + +static const char *write_program_tmpfile(void) { + static char path[] = "/tmp/jpgp-prog-XXXXXX"; + int fd = mkstemp(path); + if (fd < 0) { perror("mkstemp"); exit(1); } + ssize_t n = write(fd, program_boot_data, program_boot_size); + if (n != (ssize_t)program_boot_size) { + perror("write"); close(fd); unlink(path); exit(1); + } + close(fd); + return path; +} + +int main(int argc, const char *argv[]) { + Sscheme_init(NULL); + Sregister_boot_file_bytes("petite", + (void *)petite_boot_data, petite_boot_size); + Sregister_boot_file_bytes("scheme", + (void *)scheme_boot_data, scheme_boot_size); + Sbuild_heap(NULL, NULL); + + const char *prog_path = write_program_tmpfile(); + Sscheme_program(prog_path, argc, argv); + unlink(prog_path); + + Sscheme_deinit(); + return 0; +} +CMAIN +echo "" + +# Step 4: compile + link +echo "==> [4/4] Compile + link -> $OUTPUT" +EXTRA_ARCHIVES="" +for a in liblz4.a libz.a; do + if [ -f "$CSV_DIR/$a" ]; then + EXTRA_ARCHIVES="$EXTRA_ARCHIVES $CSV_DIR/$a" + fi +done + +# shellcheck disable=SC2086 +$CC -I"$CSV_DIR" -O2 \ + -o "$OUTPUT" \ + "${OUTPUT}-main.c" \ + "$CSV_DIR/libkernel.a" \ + $EXTRA_ARCHIVES \ + $OS_LIBS + +echo "" +echo "=== Build complete ===" +ls -lh "$OUTPUT" +file "$OUTPUT" 2>/dev/null || true