#! /bin/sh -e
###
### Dump Lisp images for faster script execution
###
### (c) 2020 Mark Wooding
###
###----- Licensing notice ---------------------------------------------------
###
### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
###
### Runlisp is free software: you can redistribute it and/or modify it
### under the terms of the GNU General Public License as published by the
### Free Software Foundation; either version 3 of the License, or (at your
### option) any later version.
###
### Runlisp is distributed in the hope that it will be useful, but WITHOUT
### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
### for more details.
###
### You should have received a copy of the GNU General Public License
### along with Runlisp. If not, see .
###--------------------------------------------------------------------------
### Build-time configuration.
VERSION=@VERSION@
imagedir=@imagedir@
eclopt=@ECLOPT@
###--------------------------------------------------------------------------
### Random utilities.
prog=${0##*/}
## Report a fatal error.
lose () { echo >&2 "$prog: $*"; exit 2; }
## Quote a string so that Lisp will understand it.
lisp_quote () { printf "%s\n" "$1" | sed 's/[\\"]/\\&/g'; }
## Mention that we're running a program.
run () { echo "$*"; $lbuf "$@"; }
## Figure out whether we can force line-buffering.
if stdbuf --version >/dev/null 2>&1; then lbuf="stdbuf -oL --"
else lbuf=""; fi
## Copy stdin to stdout, one line at a time. This is important in the shell
## game below, to prevent lines from two incoming streams being interleaved
## in the log file.
copy () { while IFS= read -r line; do printf "%s %s\n" "$1" "$line"; done; }
###--------------------------------------------------------------------------
### Lisp runes.
## Load and upgrade ASDF.
load_asdf_rune="(require \"asdf\")"
upgrade_asdf_rune="(asdf:upgrade-asdf)"
## Ignore `#!' lines. (We force this so as to provide a uniform environment,
## even though some Lisp implementations take special action when they know
## they're running scripts.)
ignore_shebang_rune="\
(set-dispatch-macro-character
#\\# #\\!
(lambda (stream #1=#:char #2=#:arg)
(declare (ignore #1# #2#))
(values (read-line stream))))"
## Push `:runlisp-script' into the `*features*' list.
set_script_feature_rune="(pushnew :runlisp-script *features*)"
## All of the above.
common_prelude_rune="\
(progn
$upgrade_asdf_rune
$ignore_shebang_rune
$set_script_feature_rune)"
###--------------------------------------------------------------------------
### Explain how to dump the various Lisp systems.
## Maintain the master tables.
unset lisps
deflisp () { lisps=${lisps+$lisps }$1; eval ${1}_image=\$2; }
## Steel Bank Common Lisp.
deflisp sbcl sbcl+asdf.core
dump_sbcl () {
image=$(lisp_quote "$1")
run "${SBCL-sbcl}" --noinform --no-userinit --no-sysinit \
--disable-debugger \
--eval "$load_asdf_rune" \
--eval "$common_prelude_rune" \
--eval "(sb-ext:save-lisp-and-die \"$image\")"
}
## Clozure Common Lisp.
deflisp ccl ccl+asdf.image
dump_ccl () {
image=$(lisp_quote "$1")
## A snaglet occurs here. CCL wants to use the image name as a clue to
## where the rest of its installation is; but in fact the image is
## nowhere near its installation. So we must hack...
run "${CCL-ccl}" -b -n -Q \
-e "$load_asdf_rune" \
-e "$common_prelude_rune" \
-e "(ccl::in-development-mode
(let ((#1=#:real-ccl-dir (ccl::ccl-directory)))
(defun ccl::ccl-directory ()
(let* ((#2=#:dirpath (ccl:getenv \"CCL_DEFAULT_DIRECTORY\")))
(if (and #2# (plusp (length (namestring #2#))))
(ccl::native-to-directory-pathname #2#)
#1#))))
(compile 'ccl::ccl-directory))" \
-e "(ccl:save-application \"$image\"
:init-file nil
:error-handler :quit)"
}
## GNU CLisp.
deflisp clisp clisp+asdf.mem
dump_clisp () {
image=$(lisp_quote "$1")
run "${CLISP-clisp}" -norc -q -q \
-x "$load_asdf_rune" \
-x "$common_prelude_rune" \
-x "(ext:saveinitmem \"$image\"
:norc t
:script t)" \
-- wrong arguments
}
## Embeddable Common Lisp.
deflisp ecl ecl+asdf
dump_ecl () {
image=$1
set -e
## Start by compiling a copy of ASDF.
cat >"$tmp/ecl-build.lisp" <"$tmp/ecl-run.lisp" < $out"
done
exit 0
;;
o) outfile=$OPTARG out=t; dir= ;;
u) update=t ;;
v) verbose=t ;;
*) bogus=t ;;
esac
done
shift $(( $OPTIND - 1 ))
## If the destination is a directory then notice this.
case $out in
t) if [ -d "$outfile" ]; then dir=${outfile%/}/; out=nil; fi ;;
esac
## Check that everything matches.
case $#,$all,$out in
0,nil,*) lose "no Lisp systems to dump" ;;
0,t,nil) set -- $lisps ;;
*,t,*) lose "\`-a' makes no sense with explicit list" ;;
1,nil,t) ;;
*,*,t) lose "can't name explicit output file for multiple Lisp systems" ;;
esac
## Check that the Lisp systems named are actually known.
for lisp in "$@"; do
case " $lisps " in
*" $lisp "*) ;;
*) echo >&2 "$prog: unknown Lisp \`$lisp'"; exit 2 ;;
esac
done
## Complain if there were problems.
case $bogus in t) usage >&2; exit 2 ;; esac
###--------------------------------------------------------------------------
### Dump the images.
## Establish a temporary directory to work in.
i=0
while :; do
tmp=${TMPDIR-/tmp}/runlisp-tmp.$$.
if mkdir "$tmp" >/dev/null 2>&1; then break; fi
case $i in 64) lose "failed to create temporary directory" ;; esac
i=$(expr $i + 1)
done
trap 'rm -rf "$tmp"' EXIT INT TERM HUP
## Send stdout to stderr or the log, depending on verbosity.
output () {
case $verbose in
nil) $lbuf cat -u >"$tmp/log" ;;
t) $lbuf cat >&2 ;;
esac
}
## Work through each requested Lisp system.
exit=0
for lisp in "$@"; do
## Figure out the output file to use.
case $out in nil) eval outfile=\$dir\$${lisp}_image ;; esac
## Maybe we skip this one if the output already exists.
case $update in
t)
if [ -f "$outfile" ]; then
case $verbose in
t)
echo >&2 "$prog: \`$outfile' already exists: skipping \`$lisp'"
;;
esac
continue
fi
;;
esac
## If we're doing all the Lisps, then skip systems which aren't actually
## installed.
case $checkinst in
t)
LISP=$(echo $lisp | tr a-z A-Z)
eval lispprog=\${$LISP-$lisp}
if ! type >/dev/null 2>&1 $lispprog; then
case $verbose in
t)
echo >&2 "$prog: command \`$LISP' not found: skipping \`$lisp'"
;;
esac
continue
fi
;;
esac
## Dump the Lisp, capturing its potentially drivellous output in a log
## (unless we're being verbose). Be careful to keep stdout and stderr
## separate.
rc=$(
{ { { { echo "dumping $lisp to \`$outfile'..."
set +e; dump_$lisp "$outfile" 4>&- 5>&-
echo $? >&5; } |
copy "|" >&4; } 2>&1 |
copy "*" >&4; } 4>&1 |
output; } 5>&1 &2 "$tmp/log" ;; esac; exit=2 ;;
esac
done
## All done.
exit $exit
###----- That's all, folks --------------------------------------------------