Fix extras targets and binary use loading
ober
d4d9bb0a9a22a2f1df434ef2c71e653e8402e0b8
--- a/.gitignore +++ b/.gitignore @@ -16,8 +16,10 @@ /jsh-macos /jsh-macos.sha256 /ffi-shim-symbols.list +/.jsh-extras-embed.conf # Generated source/staging +/jsh-generated.ss /jsh-src/ /_jerbuild-stage/ /src/ @@ -28,6 +30,7 @@ # Vendored external checkouts and local caches /_vendor/ /vendor/ +/rust-coreutils/target/ /.jerboa/bin/ /.jcode/ /.claude/ --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ JERBOA_VERSION ?= v0.2.0 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin JERBOA_SHELL_EXTRAS_URL ?= https://git.sr.ht/~lisp/jerboa-shell-extras JERBOA_SHELL_EXTRAS_DIR ?= $(CURDIR)/_vendor/jerboa-shell-extras +JSH_EXTRAS_EMBED_CONF ?= $(CURDIR)/.jsh-extras-embed.conf +JSH_EXTRAS_DEFAULT_EMBED ?= $(JERBOA_SHELL_EXTRAS_DIR)/embed JERBUILD ?= $(shell if [ -x ./jerbuild ]; then echo ./jerbuild; \ elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ]; then echo "$(JERBOA_TOOL_DIR)/jerbuild"; \ elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; \ @@ -31,6 +33,9 @@ JERBOA_NATIVE_HOST_LIB ?= $(JERBOA_NATIVE_RS_DIR)/target/release/libjerboa_nativ JSH_NATIVE_NO_SQLITE_FEATURES ?= tls crypto JERBOA_NATIVE_HOST_FEATURES ?= $(JSH_NATIVE_NO_SQLITE_FEATURES) JSH_CROSS_FEATURES ?= coreutils +# jsh evaluates Jerboa source at runtime via ,use. Jerbuild's deterministic +# FASL metadata strip removes import information needed by that runtime path. +JSH_JERBUILD_DETERMINISTIC_IDS ?= 0 JERBOA_SSH_REPO ?= $(VENDOR)/jerboa-ssh JSQLITE_REPO ?= $(VENDOR)/jsqlite JSQLITE_URL ?= https://git.sr.ht/~lisp/jsqlite @@ -142,11 +147,75 @@ ensure-shell-extras: fi @test -f "$(JERBOA_SHELL_EXTRAS_DIR)/Makefile" -extras: ensure-shell-extras - @$(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" extras \ +configure: ensure-shell-extras + @$(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" configure \ JERBOA_SHELL_DIR="$(CURDIR)" \ - JSH_EMBED="$(JERBOA_SHELL_EXTRAS_DIR)/embed" \ EXTRAS_OUTPUT="$(CURDIR)/jsh-extras" + @tools/select-extras-embed.sh \ + --features "$(JERBOA_SHELL_EXTRAS_DIR)/features.conf" \ + --state "$(JSH_EXTRAS_EMBED_CONF)" \ + --default "$(JSH_EXTRAS_DEFAULT_EMBED)" \ + --prompt >/dev/null + +features: configure + +features-all: ensure-shell-extras + @$(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" features-all \ + JERBOA_SHELL_DIR="$(CURDIR)" \ + EXTRAS_OUTPUT="$(CURDIR)/jsh-extras" + @tools/select-extras-embed.sh \ + --features "$(JERBOA_SHELL_EXTRAS_DIR)/features.conf" \ + --state "$(JSH_EXTRAS_EMBED_CONF)" \ + --default "$(JSH_EXTRAS_DEFAULT_EMBED)" \ + --prompt >/dev/null + +features-minimal: ensure-shell-extras + @$(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" features-minimal \ + JERBOA_SHELL_DIR="$(CURDIR)" \ + EXTRAS_OUTPUT="$(CURDIR)/jsh-extras" + @tools/select-extras-embed.sh \ + --features "$(JERBOA_SHELL_EXTRAS_DIR)/features.conf" \ + --state "$(JSH_EXTRAS_EMBED_CONF)" \ + --default "$(JSH_EXTRAS_DEFAULT_EMBED)" \ + --no-prompt >/dev/null + +show-features: ensure-shell-extras + @$(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" show-features \ + JERBOA_SHELL_DIR="$(CURDIR)" \ + EXTRAS_OUTPUT="$(CURDIR)/jsh-extras" + @tools/select-extras-embed.sh \ + --features "$(JERBOA_SHELL_EXTRAS_DIR)/features.conf" \ + --state "$(JSH_EXTRAS_EMBED_CONF)" \ + --default "$(JSH_EXTRAS_DEFAULT_EMBED)" \ + --show \ + --no-prompt + +extras: configure + @embed_path="$$(tools/select-extras-embed.sh \ + --features "$(JERBOA_SHELL_EXTRAS_DIR)/features.conf" \ + --state "$(JSH_EXTRAS_EMBED_CONF)" \ + --default "$(JSH_EXTRAS_DEFAULT_EMBED)" \ + --no-prompt)"; \ + mkdir -p "$$embed_path"; \ + echo "=== Fetching dependencies for the selected jerboa-shell extras ==="; \ + $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" vendor-deps \ + JERBOA_SHELL_DIR="$(CURDIR)" \ + JSH_EMBED="$$embed_path" \ + EXTRAS_OUTPUT="$(CURDIR)/jsh-extras"; \ + echo "=== Building jerboa-shell with selected extras ==="; \ + $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" binary \ + JERBOA_SHELL_DIR="$(CURDIR)" \ + JSH_EMBED="$$embed_path" \ + EXTRAS_OUTPUT="$(CURDIR)/jsh-extras"; \ + case "$$(uname -s)" in \ + Darwin) extras_bin="jsh-macos" ;; \ + Linux) if [ -n "$$TERMUX_VERSION" ] || [ -d /data/data/com.termux ]; then extras_bin="jsh-android"; else extras_bin="jsh-linux-$$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"; fi ;; \ + FreeBSD) extras_bin="jsh-freebsd-$$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" ;; \ + *) echo "Unsupported platform: $$(uname -s)" >&2; exit 1 ;; \ + esac; \ + cp -f "$(JERBOA_SHELL_EXTRAS_DIR)/$$extras_bin" "$(CURDIR)/jsh-extras"; \ + echo "=== Extras shell ready: $(CURDIR)/jsh-extras ==="; \ + echo "=== JSH_EMBED: $$embed_path ===" ensure-jsqlite: @if [ ! -f "$(JSQLITE_DIR)/jsqlite/api.ss" ]; then \ @@ -185,7 +254,7 @@ jerboa: ensure-jerboa-tools jsh-macos: ensure-jerboa-tools ffi-shim-symbols.list jerboa @case "$(UNAME_S)" in Darwin) ;; *) echo "ERROR: jsh-macos must be built on macOS" >&2; exit 1 ;; esac @echo "=== Building jsh-macos ===" - JERBUILD="$(JERBUILD)" $(JERBUILD) build --config .jerbuild + JERBUILD="$(JERBUILD)" JERBOA_BINARY_DETERMINISTIC_IDS="$(JSH_JERBUILD_DETERMINISTIC_IDS)" $(JERBUILD) build --config .jerbuild @shasum -a 256 jsh-macos > jsh-macos.sha256 macos: jsh-macos @@ -207,17 +276,6 @@ jsh-macos-minimal: @echo "=== jsh-macos-minimal is a compatibility alias; building with JSH_FEATURES=none ===" @$(MAKE) jsh-macos-base -features-all: - @echo "=== features-all compatibility target: use JSH_FEATURES=all make <target> ===" - -configure features: features-all - -features-minimal: - @echo "=== features-minimal compatibility target: use JSH_FEATURES=none make <target> ===" - -show-features: - @echo "JSH_FEATURES=$${JSH_FEATURES:-$${JERBOA_FEATURES:-all/default by target}}" - compile: ensure-jerboa-tools stage-jsqlite jerboa gen-embed @echo "=== Compiling .sls -> .so ===" $(JERBUILD) exec --libdirs "$(LIBDIRS_JSH)" build-all.ss @@ -716,7 +774,9 @@ install-hooks: @if [ ! -d .git ]; then echo "not a git checkout -- cannot install hooks" >&2; exit 1; fi @mkdir -p .git/hooks @ln -sf ../../tools/pre-commit .git/hooks/pre-commit + @ln -sf ../../tools/pre-push .git/hooks/pre-push @echo "Installed pre-commit hook -> tools/pre-commit" + @echo "Installed pre-push hook -> tools/pre-push" learn-codex-shim: @echo "ERROR: tools/jsh-learn-policy is not present in this repo; use obersh for learn-codex-shim." >&2 @@ -728,7 +788,10 @@ run: ensure-jerboa-tools $(FFI_LIB) jerboa security: @REPO_ROOT="$(CURDIR)" sh tools/security-check.sh -test: ensure-jerboa-tools $(FFI_LIB) jerboa +test-make-extras: + @bash test/test-make-extras.sh + +test: ensure-jerboa-tools $(FFI_LIB) jerboa test-make-extras @echo "=== Running unit tests ===" $(JSH_FFI_ENV) $(JERBUILD) exec --libdirs "$(LIBDIRS)" test/test-jsh.ss --- a/main.ss +++ b/main.ss @@ -26,6 +26,7 @@ :jsh/startup :jsh/arithmetic :jsh/ffi + (only (std os platform) platform-cpu-count) (only (compat gambit) tty-mode-set!)) ;;; --- Session state parameters --- @@ -373,6 +374,209 @@ (tty-mode-set! (current-input-port) #t #t #f #f 0)) (loop (+ cmd-num 1)))))))) +(def (output-to-string proc) + (let ([p (open-output-string)]) + (proc p) + (get-output-string p))) + +(def (jerboa-preprocess-brackets str) + (let* ([len (string-length str)] + [out (open-output-string)]) + (let loop ([i 0] [in-string #f] [in-line-comment #f]) + (if (>= i len) + (get-output-string out) + (let ([ch (string-ref str i)]) + (cond + [in-line-comment + (write-char ch out) + (loop (+ i 1) in-string (not (char=? ch #\newline)))] + [in-string + (write-char ch out) + (cond + [(char=? ch #\\) + (when (< (+ i 1) len) + (write-char (string-ref str (+ i 1)) out)) + (loop (+ i 2) in-string #f)] + [(char=? ch #\") (loop (+ i 1) #f #f)] + [else (loop (+ i 1) in-string #f)])] + [(and (char=? ch #\#) + (< (+ i 1) len) + (char=? (string-ref str (+ i 1)) #\\)) + (write-char ch out) + (write-char (string-ref str (+ i 1)) out) + (if (>= (+ i 2) len) + (loop (+ i 2) #f #f) + (let ([nc (string-ref str (+ i 2))]) + (if (not (char-alphabetic? nc)) + (begin (write-char nc out) (loop (+ i 3) #f #f)) + (let char-loop ([j (+ i 2)]) + (if (>= j len) + (loop j #f #f) + (let ([ac (string-ref str j)]) + (if (or (char-whitespace? ac) + (memv ac '(#\( #\) #\[ #\] #\; #\"))) + (loop j #f #f) + (begin (write-char ac out) (char-loop (+ j 1))))))))))] + [(and (char=? ch #\#) + (< (+ i 1) len) + (char=? (string-ref str (+ i 1)) #\|)) + (write-char ch out) + (write-char (string-ref str (+ i 1)) out) + (let block-loop ([j (+ i 2)] [depth 1]) + (if (or (>= j len) (= depth 0)) + (loop j #f #f) + (let ([bc (string-ref str j)]) + (cond + [(and (char=? bc #\|) + (< (+ j 1) len) + (char=? (string-ref str (+ j 1)) #\#)) + (write-char bc out) + (write-char (string-ref str (+ j 1)) out) + (block-loop (+ j 2) (- depth 1))] + [(and (char=? bc #\#) + (< (+ j 1) len) + (char=? (string-ref str (+ j 1)) #\|)) + (write-char bc out) + (write-char (string-ref str (+ j 1)) out) + (block-loop (+ j 2) (+ depth 1))] + [else + (write-char bc out) + (block-loop (+ j 1) depth)]))))] + [(char=? ch #\;) + (write-char ch out) + (loop (+ i 1) #f #t)] + [(char=? ch #\") + (write-char ch out) + (loop (+ i 1) #t #f)] + [(and (char=? ch #\[) + (< (+ i 1) len) + (char=? (string-ref str (+ i 1)) #\])) + (display "'()" out) + (loop (+ i 2) #f #f)] + [(char=? ch #\[) (write-char #\( out) (loop (+ i 1) #f #f)] + [(char=? ch #\]) (write-char #\) out) (loop (+ i 1) #f #f)] + [else (write-char ch out) (loop (+ i 1) #f #f)])))))) + +(def (jerboa-read-all-from-string str) + (let* ([processed (jerboa-preprocess-brackets str)] + [p (open-input-string (string-append "#!chezscheme\n" processed))]) + (let loop ([forms '()]) + (let ([form (read p)]) + (if (eof-object? form) + (reverse forms) + (loop (cons form forms))))))) + +(def (jerboa-read-all port) + (let ([raw (let ([p (open-output-string)]) + (let loop () + (let ([ch (read-char port)]) + (unless (eof-object? ch) + (write-char ch p) + (loop)))) + (get-output-string p))]) + (jerboa-read-all-from-string raw))) + +(def (jerboa-body->expr body) + (cond + [(null? body) '(void)] + [(null? (cdr body)) (car body)] + [else `(begin ,@body)])) + +(def (jerboa-lower-top form) + (cond + [(and (pair? form) (eq? (car form) 'def) (pair? (cdr form))) + (let ([target (cadr form)] + [body (cddr form)]) + (cond + [(symbol? target) `(define ,target ,(jerboa-body->expr body))] + [(pair? target) `(define ,target ,@body)] + [else form]))] + [else form])) + +(def *jerboa-use-env-ready* #f) + +(def (ensure-jerboa-use-env!) + (unless *jerboa-use-env-ready* + (let ([ienv (interaction-environment)]) + (define (import-lib! lib) + (eval `(import ,lib) ienv)) + (define (try-import-lib! lib) + (guard (e (#t #f)) + (import-lib! lib) + #t)) + (for-each + import-lib! + '((jerboa runtime) + (std error) + (std misc string) + (std misc list) + (std os path) + (std os platform) + (std format) + (std sort) + (std pregexp) + (std misc thread))) + (for-each + try-import-lib! + '((std text glob) + (std misc lru-cache) + (std misc trie))) + (eval '(define (force-output . args) + (flush-output-port + (if (null? args) (current-output-port) (car args)))) ienv) + (eval '(define cpu-count platform-cpu-count) ienv) + (eval '(define processor-count platform-cpu-count) ienv) + (eval `(define ,(string->symbol "##current-vm-processor-count") + (let ([count ,(platform-cpu-count)]) + (lambda () count))) ienv) + (eval `(define (,(string->symbol "##set-parallelism-level!") n) (void)) ienv) + (eval `(define (,(string->symbol "##startup-parallelism!")) (void)) ienv) + (set! *jerboa-use-env-ready* #t)))) + +(def (jerboa-eval-forms forms) + (ensure-jerboa-use-env!) + (let ([ienv (interaction-environment)]) + (let loop ([fs forms] [last (void)]) + (if (null? fs) + last + (let ([form (car fs)]) + (cond + [(and (pair? form) (eq? (car form) 'export)) + (loop (cdr fs) last)] + [(and (pair? form) (eq? (car form) 'import)) + (loop (cdr fs) last)] + [else + (loop (cdr fs) (eval (jerboa-lower-top form) ienv))])))))) + +(def (format-jerboa-result result) + (cond + [(eq? result (void)) ""] + [else + (output-to-string (lambda (port) (write result port)))])) + +(def (handle-use-command path-str) + (let ([path (string-trim-whitespace path-str)]) + (unless (file-exists? path) + (error 'use (string-append "file not found: " path))) + (let* ([port (open-input-file path)] + [forms (jerboa-read-all port)]) + (close-input-port port) + (let ([result (jerboa-eval-forms forms)]) + (fprintf (current-error-port) "loaded: ~a (~a forms)~n" path (length forms)) + result)))) + +(def (execute-comma-input input env) + (let ([expr (string-trim-whitespace (substring input 1 (string-length input)))]) + (cond + [(string-prefix? "use " expr) + (let ([result (handle-use-command (substring expr 4 (string-length expr)))]) + (let ([text (format-jerboa-result result)]) + (unless (string=? text "") (displayln text))) + 0)] + [else + (fprintf (current-error-port) "jsh: unknown meta-command: ,~a~n" expr) + 1]))) + (def (execute-input input env) ;; Parse and execute a line of input (with-catch @@ -393,7 +597,10 @@ (string-prefix? "bad substitution: unclosed" msg))) 2 1))))) (lambda () - (let ([cmd (with-catch + (if (and (> (string-length input) 0) + (char=? (string-ref input 0) #\,)) + (execute-comma-input input env) + (let ([cmd (with-catch (lambda (e) (fprintf (current-error-port) "jsh: syntax error: ~a~n" (exception-message e)) @@ -405,7 +612,7 @@ (cond ((eq? cmd 'error) 2) ((not cmd) 0) - (else (execute-command cmd env))))))) + (else (execute-command cmd env)))))))) ;;; --- Trap processing --- --- a/support/stage-for-jerbuild.sh +++ b/support/stage-for-jerbuild.sh @@ -4,6 +4,9 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FUSE_SSL_PREFIX="$(brew --prefix openssl@3 2>/dev/null || echo /opt/homebrew/opt/openssl@3)" +FUSE_SSL_INC="" +[ -d "${FUSE_SSL_PREFIX}/include" ] && FUSE_SSL_INC="-I${FUSE_SSL_PREFIX}/include" echo "==> stage-for-jerbuild: cleaning stale compiled artifacts" find "${ROOT}/src" -type f \( -name '*.so' -o -name '*.wpo' \ --- a/test/test-binary.sh +++ b/test/test-binary.sh @@ -48,6 +48,22 @@ check "subshell" "inner" -c "(echo inner)" check_rc "false status" 1 -c "false" check_rc "syntax error status" 2 -c "if" +use_tmp="$(mktemp -d "${TMPDIR:-/tmp}/jsh-use-test.XXXXXX")" +cat > "$use_tmp/use-smoke.ss" <<'EOF' +(import (jerboa prelude)) +(def (jsh-use-smoke x y) (+ x y)) +(displayln "jsh-use-smoke=" (jsh-use-smoke 2 3)) +EOF +got="$(printf ',use %s/use-smoke.ss\n' "$use_tmp" | "$BINARY" 2>&1)" +rc=$? +if [ "$rc" -eq 0 ] && printf '%s\n' "$got" | grep -Fq "jsh-use-smoke=5"; then + pass=$((pass + 1)) +else + fail=$((fail + 1)) + printf 'FAIL: ,use loads Jerboa script\n rc=%s\n got=%s\n' "$rc" "$got" +fi +rm -rf "$use_tmp" + rm -f /tmp/jsh-test.out /tmp/jsh-test.err echo "Binary tests ($BINARY): $pass passed, $fail failed" [ "$fail" -eq 0 ] new file mode 100755 --- /dev/null +++ b/test/test-make-extras.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Regression coverage for make extras wiring and JSH_EMBED selection. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +target_exists() { + local target="$1" + awk -v t="$target" ' + /^[A-Za-z0-9_.-]+[[:space:]]*:/ { + name = $1 + sub(/:.*/, "", name) + if (name == t) found = 1 + } + END { exit found ? 0 : 1 } + ' Makefile +} + +for target in extras configure features features-all features-minimal show-features build binary test-binary; do + target_exists "$target" || fail "missing Makefile target: $target" +done + +if grep -Fq 'JSH_EMBED="$(JERBOA_SHELL_EXTRAS_DIR)/embed"' Makefile; then + fail "make extras must not force JSH_EMBED to the extras repo embed directory" +fi + +tmp="$(mktemp -d "${TMPDIR:-/tmp}/jsh-extras-test.XXXXXX")" +trap 'rm -rf "$tmp"' EXIT + +cat > "$tmp/features-with-embed.conf" <<'EOF' +(features + coreutils + embed +) +EOF + +chosen="$tmp/custom embed" +got="$(JSH_EMBED="$chosen" tools/select-extras-embed.sh \ + --features "$tmp/features-with-embed.conf" \ + --state "$tmp/embed-state.conf" \ + --default "$tmp/default" \ + --no-prompt)" +[ "$got" = "$chosen" ] || fail "JSH_EMBED env override not preserved" +grep -Fqx "JSH_EMBED=$chosen" "$tmp/embed-state.conf" || fail "JSH_EMBED state not written" + +unset JSH_EMBED +got="$(tools/select-extras-embed.sh \ + --features "$tmp/features-with-embed.conf" \ + --state "$tmp/embed-state.conf" \ + --default "$tmp/default" \ + --no-prompt)" +[ "$got" = "$chosen" ] || fail "saved JSH_EMBED state not reused" + +cat > "$tmp/features-without-embed.conf" <<'EOF' +(features + coreutils +) +EOF + +got="$(tools/select-extras-embed.sh \ + --features "$tmp/features-without-embed.conf" \ + --state "$tmp/no-state.conf" \ + --default "$tmp/default" \ + --no-prompt)" +[ "$got" = "$tmp/default" ] || fail "non-embed selection should use default embed dir" + +show="$(tools/select-extras-embed.sh \ + --features "$tmp/features-with-embed.conf" \ + --state "$tmp/embed-state.conf" \ + --default "$tmp/default" \ + --show \ + --no-prompt)" +case "$show" in + *"Embed feature: enabled"*"$chosen"*) ;; + *) fail "--show did not report enabled embed path" ;; +esac + +echo "PASS: make extras wiring" new file mode 100755 --- /dev/null +++ b/tools/pre-push @@ -0,0 +1,18 @@ +#!/bin/sh +# Pre-push hook: prove the native build and binary smoke tests before push. + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +echo "=== pre-push: Makefile/extras regression tests ===" +make test-make-extras + +echo "=== pre-push: native binary build ===" +make binary + +echo "=== pre-push: binary functional tests ===" +make test-binary + +echo "=== pre-push: all checks passed ===" new file mode 100755 --- /dev/null +++ b/tools/select-extras-embed.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# Resolve the JSH_EMBED source directory for make extras. + +set -euo pipefail + +FEATURES_CONF="" +STATE_FILE=".jsh-extras-embed.conf" +DEFAULT_EMBED="${HOME}/.embed" +PROMPT=0 +SHOW=0 +NO_PROMPT=0 + +usage() { + cat <<EOF +Usage: tools/select-extras-embed.sh --features PATH [options] + +Options: + --state PATH Persist selected JSH_EMBED path in PATH + --default PATH Default embed path when none is selected yet + --prompt Prompt for path when the embed feature is selected + --no-prompt Never prompt; print saved/default path + --show Print current embed selection details +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --features) + shift + [ "$#" -gt 0 ] || { echo "ERROR: --features needs a path" >&2; exit 2; } + FEATURES_CONF="$1" + ;; + --state) + shift + [ "$#" -gt 0 ] || { echo "ERROR: --state needs a path" >&2; exit 2; } + STATE_FILE="$1" + ;; + --default) + shift + [ "$#" -gt 0 ] || { echo "ERROR: --default needs a path" >&2; exit 2; } + DEFAULT_EMBED="$1" + ;; + --prompt) + PROMPT=1 + ;; + --no-prompt) + NO_PROMPT=1 + ;; + --show) + SHOW=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +[ -n "$FEATURES_CONF" ] || { echo "ERROR: --features is required" >&2; exit 2; } + +expand_path() { + case "$1" in + "~") printf '%s\n' "$HOME" ;; + "~/"*) printf '%s/%s\n' "$HOME" "${1#~/}" ;; + *) printf '%s\n' "$1" ;; + esac +} + +config_tokens() { + [ -f "$FEATURES_CONF" ] || return 0 + awk ' + { + raw = $0 + sub(/;.*/, "", raw) + if (raw ~ /^[[:space:]]*\(features/) { + in_features = 1 + sub(/^[[:space:]]*\(features/, "", raw) + } + if (in_features) { + done = (raw ~ /\)/) + gsub(/[()]/, " ", raw) + n = split(raw, parts, /[[:space:]]+/) + for (i = 1; i <= n; i++) { + if (parts[i] != "") print parts[i] + } + if (done) in_features = 0 + } + } + ' "$FEATURES_CONF" +} + +embed_selected() { + local token saw=0 + while IFS= read -r token; do + saw=1 + case "$token" in + embed|all|full) return 0 ;; + none|minimal|core) return 1 ;; + esac + done < <(config_tokens) + [ "$saw" -eq 0 ] && return 1 + return 1 +} + +read_state() { + [ -f "$STATE_FILE" ] || return 1 + awk -F= '$1 == "JSH_EMBED" { sub(/^JSH_EMBED=/, ""); print; found = 1; exit } END { exit found ? 0 : 1 }' "$STATE_FILE" +} + +write_state() { + local path="$1" dir tmp + dir="$(dirname "$STATE_FILE")" + mkdir -p "$dir" + tmp="${STATE_FILE}.tmp" + { + echo "# Generated by tools/select-extras-embed.sh" + printf 'JSH_EMBED=%s\n' "$path" + } > "$tmp" + mv "$tmp" "$STATE_FILE" +} + +choose_path() { + local current input + if [ -n "${JSH_EMBED:-}" ]; then + current="$(expand_path "$JSH_EMBED")" + write_state "$current" + printf '%s\n' "$current" + return + fi + + current="$(read_state || true)" + if [ -z "$current" ]; then + current="$DEFAULT_EMBED" + fi + current="$(expand_path "$current")" + + if [ "$PROMPT" -eq 1 ] && [ "$NO_PROMPT" -eq 0 ] && embed_selected && [ -r /dev/tty ] && [ -w /dev/tty ]; then + { + echo + echo "Embed feature selected." + printf 'JSH_EMBED source directory [%s]: ' "$current" + } > /dev/tty + IFS= read -r input < /dev/tty || input="" + if [ -n "$input" ]; then + current="$(expand_path "$input")" + fi + fi + + write_state "$current" + printf '%s\n' "$current" +} + +path="$(choose_path)" + +if [ "$SHOW" -eq 1 ]; then + if embed_selected; then + printf 'Embed feature: enabled\n' + else + printf 'Embed feature: disabled\n' + fi + printf 'JSH_EMBED: %s\n' "$path" + printf 'Embed config: %s\n' "$STATE_FILE" +else + printf '%s\n' "$path" +fi