@@@ more wip
[runlisp] / dump-runlisp-image.in
CommitLineData
e29834b8
MW
1#! /bin/sh -e
2###
3### Dump Lisp images for faster script execution
4###
5### (c) 2020 Mark Wooding
6###
7
8###----- Licensing notice ---------------------------------------------------
9###
10### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
11###
12### Runlisp is free software: you can redistribute it and/or modify it
13### under the terms of the GNU General Public License as published by the
14### Free Software Foundation; either version 3 of the License, or (at your
15### option) any later version.
16###
17### Runlisp is distributed in the hope that it will be useful, but WITHOUT
18### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
20### for more details.
21###
22### You should have received a copy of the GNU General Public License
23### along with Runlisp. If not, see <https://www.gnu.org/licenses/>.
24
25###--------------------------------------------------------------------------
26### Build-time configuration.
27
28VERSION=@VERSION@
29imagedir=@imagedir@
30eclopt=@ECLOPT@
31
32###--------------------------------------------------------------------------
33### Random utilities.
34
35prog=${0##*/}
36
37## Report a fatal error.
38lose () { echo >&2 "$prog: $*"; exit 2; }
39
40## Quote a string so that Lisp will understand it.
41lisp_quote () { printf "%s\n" "$1" | sed 's/[\\"]/\\&/g'; }
42
43## Mention that we're running a program.
44run () { echo "$*"; $lbuf "$@"; }
45
46## Figure out whether we can force line-buffering.
47if stdbuf --version >/dev/null 2>&1; then lbuf="stdbuf -oL --"
48else lbuf=""; fi
49
50## Copy stdin to stdout, one line at a time. This is important in the shell
51## game below, to prevent lines from two incoming streams being interleaved
52## in the log file.
53copy () { while IFS= read -r line; do printf "%s %s\n" "$1" "$line"; done; }
54
55###--------------------------------------------------------------------------
56### Lisp runes.
57
58## Load and upgrade ASDF.
59load_asdf_rune="(require \"asdf\")"
60upgrade_asdf_rune="(asdf:upgrade-asdf)"
61
62## Ignore `#!' lines. (We force this so as to provide a uniform environment,
63## even though some Lisp implementations take special action when they know
64## they're running scripts.)
65ignore_shebang_rune="\
66(set-dispatch-macro-character
67 #\\# #\\!
68 (lambda (stream #1=#:char #2=#:arg)
69 (declare (ignore #1# #2#))
70 (values (read-line stream))))"
71
72## Push `:runlisp-script' into the `*features*' list.
73set_script_feature_rune="(pushnew :runlisp-script *features*)"
74
75## All of the above.
76common_prelude_rune="\
77(progn
78 $upgrade_asdf_rune
79 $ignore_shebang_rune
80 $set_script_feature_rune)"
81
82###--------------------------------------------------------------------------
83### Explain how to dump the various Lisp systems.
84
85## Maintain the master tables.
86unset lisps
87deflisp () { lisps=${lisps+$lisps }$1; eval ${1}_image=\$2; }
88
89## Steel Bank Common Lisp.
90deflisp sbcl sbcl+asdf.core
91dump_sbcl () {
92 image=$(lisp_quote "$1")
93 run "${SBCL-sbcl}" --noinform --no-userinit --no-sysinit \
94 --disable-debugger \
95 --eval "$load_asdf_rune" \
96 --eval "$common_prelude_rune" \
97 --eval "(sb-ext:save-lisp-and-die \"$image\")"
98}
99
100## Clozure Common Lisp.
101deflisp ccl ccl+asdf.image
102dump_ccl () {
103 image=$(lisp_quote "$1")
104 ## A snaglet occurs here. CCL wants to use the image name as a clue to
105 ## where the rest of its installation is; but in fact the image is
106 ## nowhere near its installation. So we must hack...
107
108 run "${CCL-ccl}" -b -n -Q \
109 -e "$load_asdf_rune" \
110 -e "$common_prelude_rune" \
111 -e "(ccl::in-development-mode
112 (let ((#1=#:real-ccl-dir (ccl::ccl-directory)))
113 (defun ccl::ccl-directory ()
114 (let* ((#2=#:dirpath (ccl:getenv \"CCL_DEFAULT_DIRECTORY\")))
115 (if (and #2# (plusp (length (namestring #2#))))
116 (ccl::native-to-directory-pathname #2#)
117 #1#))))
118 (compile 'ccl::ccl-directory))" \
119 -e "(ccl:save-application \"$image\"
120 :init-file nil
121 :error-handler :quit)"
122}
123
124## GNU CLisp.
125deflisp clisp clisp+asdf.mem
126dump_clisp () {
127 image=$(lisp_quote "$1")
128 run "${CLISP-clisp}" -norc -q -q \
129 -x "$load_asdf_rune" \
130 -x "$common_prelude_rune" \
131 -x "(ext:saveinitmem \"$image\"
132 :norc t
133 :script t)" \
134 -- wrong arguments
135}
136
137## Embeddable Common Lisp.
138deflisp ecl ecl+asdf
139dump_ecl () {
140 image=$1
141 set -e
142
143 ## Start by compiling a copy of ASDF.
144 cat >"$tmp/ecl-build.lisp" <<EOF
145(require "asdf")
146
147(defparameter *asdf* (asdf:find-system "asdf"))
148
149(defun right-here (pathname pattern)
150 (declare (ignore pattern))
151 (merge-pathnames
152 (make-pathname :name (concatenate 'string
153 (string-downcase
154 (lisp-implementation-type))
155 "-"
156 (pathname-name pathname))
157 :type nil
158 :version nil
159 :defaults *default-pathname-defaults*)
160 pathname))
161(asdf:initialize-output-translations '(:output-translations
162 ((#p"/" :**/ :*.*.*)
163 (:function right-here))
164 :ignore-inherited-configuration))
165
166(asdf:operate 'asdf:lib-op *asdf*)
167(si:quit 0)
168EOF
169 (cd "$tmp" && run "${ECL-ecl}" ${eclopt}norc ${eclopt}load "ecl-build.lisp")
170
171 ## And now compile our driver code.
172 cat >"$tmp/ecl-run.lisp" <<EOF
173(cl:defpackage #:runlisp
174 (:use #:common-lisp))
175(cl:in-package #:runlisp)
176
177(defun main ()
178 $ignore_shebang_rune
179 (asdf:register-immutable-system "asdf")
180 (let ((pkg (find-package "COMMON-LISP-USER")))
181 (with-package-iterator (next pkg :internal)
182 (loop (multiple-value-bind (anyp sym how) (next)
183 (declare (ignore how))
184 (unless anyp (return))
185 (unintern sym pkg)))))
186 $set_script_feature_rune
187 (let ((winning t) (script nil) (marker nil)
188 (prog (file-namestring (si:argv 0))) (i 1) (argc (si:argc)))
189 (labels ((lose (msg &rest args)
190 (format *error-output* "~&~A: ~?~%" prog msg args)
191 (setf winning nil))
192 (quit (rc)
193 (si:quit rc))
194 (usage (stream)
195 (format stream "~&usage: ~A -s SCRIPT -- ARGS~%"
196 prog))
197 (getarg ()
198 (and (< i argc) (prog1 (si:argv i) (incf i)))))
199 (loop (let ((arg (getarg)))
200 (cond ((null arg) (return))
201 ((string= arg "--") (setf marker t) (return))
202 ((string= arg "-s") (setf script (getarg)))
203 ((string= arg "-h") (usage *standard-output*) (quit 0))
204 (t (lose "unrecognized option \`~A'" arg)))))
205 (unless script (lose "nothing to do"))
206 (unless marker (lose "unexpected end of options (missing \`--'?)"))
207 (unless winning (usage *error-output*) (quit 255))
208 (handler-case
209 (let ((*package* (find-package "COMMON-LISP-USER")))
210 (load script :verbose nil :print nil))
211 (error (err)
212 (format *error-output* "~&~A (uncaught error): ~A~%" prog err)
213 (quit 255)))
214 (quit 0))))
215(main)
216EOF
217 (cd "$tmp" && run "${ECL-ecl}" ${eclopt}norc ${eclopt}load "ecl-asdf.fas" \
218 -s -o "ecl-run.o" ${eclopt}compile "ecl-run.lisp")
219
220 ## Finally link everything together.
221 run "${ECL-ecl}" ${eclopt}norc -o "$image"\
222 ${eclopt}link "$tmp/ecl-asdf.o" "$tmp/ecl-run.o"
223}
224
225## Carnegie--Mellon University Common Lisp.
226deflisp cmucl cmucl+asdf.core
227dump_cmucl () {
228 image=$(lisp_quote "$1")
229 run "${CMUCL-cmucl}" -batch -noinit -nositeinit -quiet \
230 -eval "$load_asdf_rune" \
231 -eval "$common_prelude_rune" \
232 -eval "(ext:save-lisp \"$image\"
233 :batch-mode t :print-herald nil
234 :site-init nil :load-init-file nil)"
235}
236
237###--------------------------------------------------------------------------
238### Command-line processing.
239
240usage () { echo "usage: $prog [-acluv] [-o FILE] [LISP ...]"; }
241version () { echo "$prog, runlisp version $VERSION"; }
242help () {
243 version; echo; usage; cat <<EOF
244
245Options:
246 -h Show this help text and exit successfully.
247 -V Show the version number and exit successfully.
248 -a Dump all installed Lisp implementations.
249 -c Check that Lisp systems are installed before
250 trying to dump.
251 -l List known Lisp systems and default image filenames.
252 -o OUT Store images in OUT (file or directory); default
253 is \`\$RUNLISP_IMAGEDIR' or \`$imagedir'
254 -u Only dump images which don't exist already.
255 -v Be verbose, even if things go well.
256EOF
257}
258
259unset outfile; dir=${RUNLISP_IMAGEDIR-$imagedir}; dir=${dir%/}/
260all=nil checkinst=nil bogus=nil out=nil update=nil verbose=nil
261
262## Parse the options.
263while getopts "hVaclo:uv" opt; do
264 case $opt in
265 h) help; exit 0 ;;
266 V) version; exit 0 ;;
267 a) all=t checkinst=t ;;
268 l)
269 for i in $lisps; do
270 eval out=\$${i}_image
271 echo "$i -> $out"
272 done
273 exit 0
274 ;;
275 o) outfile=$OPTARG out=t; dir= ;;
276 u) update=t ;;
277 v) verbose=t ;;
278 *) bogus=t ;;
279 esac
280done
281shift $(( $OPTIND - 1 ))
282
283## If the destination is a directory then notice this.
284case $out in
285 t) if [ -d "$outfile" ]; then dir=${outfile%/}/; out=nil; fi ;;
286esac
287
288## Check that everything matches.
289case $#,$all,$out in
290 0,nil,*) lose "no Lisp systems to dump" ;;
291 0,t,nil) set -- $lisps ;;
292 *,t,*) lose "\`-a' makes no sense with explicit list" ;;
293 1,nil,t) ;;
294 *,*,t) lose "can't name explicit output file for multiple Lisp systems" ;;
295esac
296
297## Check that the Lisp systems named are actually known.
298for lisp in "$@"; do
299 case " $lisps " in
300 *" $lisp "*) ;;
301 *) echo >&2 "$prog: unknown Lisp \`$lisp'"; exit 2 ;;
302 esac
303done
304
305## Complain if there were problems.
306case $bogus in t) usage >&2; exit 2 ;; esac
307
308###--------------------------------------------------------------------------
309### Dump the images.
310
311## Establish a temporary directory to work in.
312i=0
313while :; do
314 tmp=${TMPDIR-/tmp}/runlisp-tmp.$$.
315 if mkdir "$tmp" >/dev/null 2>&1; then break; fi
316 case $i in 64) lose "failed to create temporary directory" ;; esac
317 i=$(expr $i + 1)
318done
319trap 'rm -rf "$tmp"' EXIT INT TERM HUP
320
321## Send stdout to stderr or the log, depending on verbosity.
322output () {
323 case $verbose in
324 nil) $lbuf cat -u >"$tmp/log" ;;
325 t) $lbuf cat >&2 ;;
326 esac
327}
328
329## Work through each requested Lisp system.
330exit=0
331for lisp in "$@"; do
332
333 ## Figure out the output file to use.
334 case $out in nil) eval outfile=\$dir\$${lisp}_image ;; esac
335
336 ## Maybe we skip this one if the output already exists.
337 case $update in
338 t)
339 if [ -f "$outfile" ]; then
340 case $verbose in
341 t)
342 echo >&2 "$prog: \`$outfile' already exists: skipping \`$lisp'"
343 ;;
344 esac
345 continue
346 fi
347 ;;
348 esac
349
350 ## If we're doing all the Lisps, then skip systems which aren't actually
351 ## installed.
352 case $checkinst in
353 t)
354 LISP=$(echo $lisp | tr a-z A-Z)
355 eval lispprog=\${$LISP-$lisp}
356 if ! type >/dev/null 2>&1 $lispprog; then
357 case $verbose in
358 t)
359 echo >&2 "$prog: command \`$LISP' not found: skipping \`$lisp'"
360 ;;
361 esac
362 continue
363 fi
364 ;;
365 esac
366
367 ## Dump the Lisp, capturing its potentially drivellous output in a log
368 ## (unless we're being verbose). Be careful to keep stdout and stderr
369 ## separate.
370 rc=$(
371 { { { { echo "dumping $lisp to \`$outfile'..."
372 set +e; dump_$lisp "$outfile" 4>&- 5>&-
373 echo $? >&5; } |
374 copy "|" >&4; } 2>&1 |
375 copy "*" >&4; } 4>&1 |
376 output; } 5>&1 </dev/null
377 )
378
379 ## If it failed, and we didn't already spray the output to the terminal,
380 ## then do that now; also record that we encountered a problem.
381 case $rc in
382 0) ;;
383 *) case $verbose in nil) cat >&2 "$tmp/log" ;; esac; exit=2 ;;
384 esac
385done
386
387## All done.
388exit $exit
389
390###----- That's all, folks --------------------------------------------------