From: Mark Wooding Date: Sat, 15 Aug 2020 23:10:59 +0000 (+0100) Subject: New version. X-Git-Url: https://git.distorted.org.uk/~mdw/runlisp/commitdiff_plain/e29834b853038e8c90dcfe8377f02431cad42fc5 New version. This is a complete rewrite, and rather more competently done. --- e29834b853038e8c90dcfe8377f02431cad42fc5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..767821d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +Makefile.in +*.tex +*.pdf +/_inst/ +/aclocal.m4 +/autom4te.cache/ +/config/compile +/config/config.h.in +/config/depcomp +/config/install-sh +/config/missing +/configure +/t/testsuite diff --git a/.skelrc b/.skelrc new file mode 100644 index 0000000..bb13575 --- /dev/null +++ b/.skelrc @@ -0,0 +1,9 @@ +;;; -*-emacs-lisp-*- + +(setq skel-alist + (append + '((author . "Mark Wooding") + (full-title . "Runlisp, a tool for invoking Common Lisp scripts") + (program . "Runlisp") + (licence-text . "[[gpl-3]]")) + skel-alist)) diff --git a/COPYING b/COPYING new file mode 120000 index 0000000..8161f30 --- /dev/null +++ b/COPYING @@ -0,0 +1 @@ +.ext/cfd/licence/GPL-3 \ No newline at end of file diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..87cf2b6 --- /dev/null +++ b/HACKING @@ -0,0 +1,116 @@ +# -*-org-*- +#+TITLE: Hacking on =runlisp= +#+AUTHOR: Mark Wooding +#+LaTeX_CLASS: strayman + +* Adding a new Lisp implementation + +When a program needs to know about a bunch of /things/, I generally try +to arrange that there's exactly one place where you put all of the +knowledge about each particular /thing/. In the case of ~runlisp~, I've +failed rather abjectly. Sorry. + +So, here's the list of places which need to be modified in order to +teach ~runlisp~ about a new Lisp system. + + + The main C source file ~runlisp.c~ has a master list macro named + ~LISP_SYSTEMS~, which just contains an entry ~_(foo)~ for each Lisp + system. Add a new entry for your new system here. This list + ordered according to my personal preference -- the /opinionated + order/. + + + There's also a function ~run_foo~ defined in ~runlisp.c~ for each + Lisp system ~foo~. These are defined in a section headed `Invoking + Lisp systems', in the opinionated order. + + + The manual page ~runlisp.1~ lists each supported Lisp system by name + in the section `Supported Common Lisp implementations'. These are + listed in alphabetical order by command name (so GNU CLisp is + ~clisp~, and therefore comes before ~ecl~) -- the /command order/. + + + The ~README.org~ file also has a list of supported Lisp systems, + again in command order. + + + In ~configure.ac~, there's a line ~mdw_CHECK_LISP([FOO], [foo])~ for + each known Lisp system in the `Checking for Lisp implementations' + section, in opinionated order. + + + If the Lisp system needs any additional configure-time hacking, then + that goes at the end of the section. Currently only ECL needs + special treatment here, but these are notionally in opinionated + order. + + + The file ~vars.am~ builds a list ~LISPS~ of the supported Lisp + systems in opinionated order. + + + For each Lisp system that can have a custom image dumped, there's a + paragraph in the `Image dumping' section of ~Makefile.am~, which + says + + : if DUMP_FOO + : image_DATA += foo+asdf.dump + : CLEANFILES += foo+asdf.dump + : foo+asdf.dump: dump-runlisp-image + : (v_dump)./dump-runlisp-image -o$@ foo + : endif + + The ~DUMP_FOO~ conditional is already set up by ~mdw_CHECK_LISP~. + The ~.dump~ suffix should be whatever extension your Lisp system + usually uses to mark its image files. These paragraphs are in + opinionated order. + + + For each Lisp system that can be dumped, there's a section in + ~dump-runlisp-image.in~ which goes + + : ## Foo Common Lisp. + : deflisp foo foo+asdf.dump + : dump_foo () { + : ## ... + : } + + These sections are in opinionated order. + + + The ~tests.at~ file has /five/ lists of Lisp systems. + + - The first, named ~LISP_SYSTEMS~ has a pair of entries, ~foo~, + ~foo/noimage~ for each Lisp system, in opinionated order. + + - The second is in the macro ~WHICH_LISP~, which contains an entry + ~#+foo "foo"~ for each system, in opinionated order. The former + symbol is the Lisp system's (preferred) ~*features*~ keyword + name, which is usually the same as its command name, but, for + example, is ~cmu~ rather than ~cmucl~ for CMU CL. + + - The third is a ~case~ block in the ~smoke~ test, which contains + an entry + + : foo) initfile=.foorc ;; + + naming the system's user initialization file, relative to the + user's home directory. (If your Lisp doesn't have one of these, + then this can be anything you like.) + + - The fourth is another ~case~ block in the ~smoke~ test, which + contains an entry + + : foo) impl="Foo Common Lisp" ;; + + giving the Lisp system's ~lisp-implementation-type~ string. + + - The fifth is in the ~preferences~ test: there's a ~set~ line + which simply lists the Lisp systems' command names. This is in + order of increasing startup time, because the test will be + running lots of trivial scripts, simply checking that the right + Lisp system is being run, so it's valuable to choose fast Lisps. + + + The script ~bench/massage-benchmarks~ has a hash ~%LISP~ mapping + Lisp command names to short labels to use in graphs, in opinionated + order. Add an entry + + : "foo" => "Foo CL", + + to this hash. + +And now the actual pain: the benchmarks need to be run again, and the +data and graphs in ~README.org~ need to be updated. Leave this to me. + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..02e9b42 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,121 @@ +### -*-makefile-*- +### +### Build script for `runlisp' +### +### (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 . + +include $(top_srcdir)/vars.am + +SUBDIRS = + +pkgdata_DATA = +image_DATA = +image_SCRIPTS = + +SUBDIRS += . + +ACLOCAL_AMFLAGS = -Im4 + +###-------------------------------------------------------------------------- +### The main driver program. + +bin_PROGRAMS += runlisp +runlisp_SOURCES = runlisp.c +man_MANS += runlisp.1 + +###-------------------------------------------------------------------------- +### Additional machinery. + +pkgdata_DATA += eval.lisp +EXTRA_DIST += eval.lisp + +###-------------------------------------------------------------------------- +### Image dumping. + +nodist_bin_SCRIPTS += dump-runlisp-image +man_MANS += dump-runlisp-image.1 + +v_dump = $(v_dump_@AM_V@) +v_dump_ = $(v_dump_@AM_DEFAULT_V@) +v_dump_0 = @echo " DUMP $@"; + +EXTRA_DIST += dump-runlisp-image.in +CLEANFILES += dump-runlisp-image +dump-runlisp-image: dump-runlisp-image.in + $(SUBST) $(srcdir)/dump-runlisp-image.in >$@.new \ + $(SUBSTITUTIONS) && \ + chmod +x $@.new && mv $@.new $@ + +if DUMP_SBCL +image_DATA += sbcl+asdf.core +CLEANFILES += sbcl+asdf.core +sbcl+asdf.core: dump-runlisp-image + $(v_dump)./dump-runlisp-image -o$@ sbcl +endif + +if DUMP_CCL +image_DATA += ccl+asdf.image +CLEANFILES += ccl+asdf.image +ccl+asdf.image: dump-runlisp-image + $(v_dump)./dump-runlisp-image -o$@ ccl +endif + +if DUMP_CLISP +image_DATA += clisp+asdf.mem +CLEANFILES += clisp+asdf.mem +clisp+asdf.mem: dump-runlisp-image + $(v_dump)./dump-runlisp-image -o$@ clisp +endif + +if DUMP_ECL +image_SCRIPTS += ecl+asdf +CLEANFILES += ecl+asdf +ecl+asdf: dump-runlisp-image + $(v_dump)./dump-runlisp-image -o$@ ecl +endif + +if DUMP_CMUCL +image_DATA += cmucl+asdf.core +CLEANFILES += cmucl+asdf.core +cmucl+asdf.core: dump-runlisp-image + $(v_dump)./dump-runlisp-image -o$@ cmucl +endif + +###-------------------------------------------------------------------------- +### Benchmarking and testing. + +if BENCHMARK +SUBDIRS += bench +endif + +SUBDIRS += t + +###-------------------------------------------------------------------------- +### Distribution. + +## Release number. +dist-hook:: + echo $(VERSION) >$(distdir)/RELEASE + +## Additional build tools. +EXTRA_DIST += config/auto-version + +###----- That's all, folks -------------------------------------------------- diff --git a/README.org b/README.org new file mode 100644 index 0000000..0465005 --- /dev/null +++ b/README.org @@ -0,0 +1,446 @@ +# -*-org-*- +#+TITLE: ~runlisp~ -- run scripts written in Common Lisp +#+AUTHOR: Mark Wooding +#+LaTeX_CLASS: strayman +#+LaTeX_HEADER: \usepackage{tikz, gnuplot-lua-tikz} + +~runlisp~ is a small C program intended to be run from a script ~#!~ +line. It selects and invokes a Common Lisp implementation, so as to run +the script. In this sense, ~runlisp~ is a partial replacement for +~cl-launch~. + +Currently, the following Lisp implementations are supported: + + + Armed Bear Common Lisp (~abcl~), + + Clozure Common Lisp (~ccl~), + + GNU CLisp (~clisp~), + + Carnegie--Mellon Univerity Common Lisp (~cmucl~), and + + Embeddable Common Lisp (~ecl~), and + + Steel Bank Common Lisp (~sbcl~). + +I'm happy to take patches to support additional free Lisp +implementations. I'm not interested in supporting non-free Lisp +systems. + + +* Writing scripts in Lisp + +** Basic use + +The obvious way to use ~runlisp~ is in a shebang (~#!~) line at the top +of a script. For example: + +: #! /usr/local/bin/runlisp +: (format t "Hello from Lisp!~%") + +Script interpreters must be named with absolute pathnames in shebang +lines; if your ~runlisp~ is installed somewhere other than +~/usr/local/bin/~ then you'll need to write something different. +Alternatively, a common hack involves abusing the ~env~ program as a +script interpreter, because it will do a path search for the program +it's supposed to run: + +: #! /usr/bin/env runlisp +: (format t "Hello from Lisp!~%") + +** Specific Lisps + +Lisp implementations are not created equal -- for good reason. If your +script depends on the features of some particular Lisp implementation, +then you can tell ~runlisp~ that it must use that implementation to run +your script using the ~-L~ option; for example: + +: #! /usr/local/bin/runlisp -Lsbcl +: (format t "Hello from Steel Bank Common Lisp!~%") + +If your script supports several Lisps, but not all, then list them all +in the ~-L~ option, separated by commas: + +: #! /usr/local/bin/runlisp -Lsbcl,ccl +: (format t #.(concatenate 'string +: "Hello from " +: #+sbcl "Steel Bank" +: #+ccl "Clozure" +: #-(or sbcl ccl) "an unexpected" +: " Common Lisp!~%")) + +** Embedded options + +If your script requires features of particular Lisp implementations +/and/ you don't want to hardcode an absolute path to ~runlisp~, then you +have a problem. Most Unix-like operating systems will parse a shebang +line into the initial ~#!~, the pathname to the interpreter program, +and a /single/ optional argument: any further spaces don't separate +further arguments: they just get included in the first argument, all the +way up to the end of the line. So + +: #! /usr/bin/env runlisp -Lsbcl +: (format t "Hello from Steel Bank Common Lisp!~%") + +won't work: it'll just try to run a program named ~runlisp -Lsbcl~, with +a space in the middle of its name, and that's quite unlikely to exist. + +To help with this situation, ~runlisp~ reads /embedded options/ from +your script. Specifically, if the script's second line contains the +token ~@RUNLISP:~ then ~runlisp~ will parse additional options from this +line. So the following will work properly. + +: #! /usr/bin/env runlisp +: ;;; @RUNLISP: -Lsbcl +: (format t "Hello from Steel Bank Common Lisp!~%") + +Embedded options are split at spaces properly. Spaces can be escaped or +quoted in (an approximation to) the usual shell manner, should that be +necessary. See the manpage for the gory details. + +** Common environment + +~runlisp~ puts some effort into making sure that Lisp scripts get the +same view of the world regardless of which implementation is running +them. + +For example: + + + The ~asdf~ and ~uiop~ systems are loaded and ready for use. + + + The script's command-line arguments are available in + ~uiop:*command-line-arguments*~. Its name can be found by calling + ~(uiop:argv0)~ -- though it's probably also in ~*load-pathname*~. + + + The prevailing Unix standard input, output, and error files are + available through the Lisp ~*standard-input*~, ~*standard-output*~, + and ~*error-ouptut*~ streams, respectively. (This is, alas, not a + foregone conclusion.) + + + The keyword ~:runlisp-script~ is added to the ~*features*~ list. + This means that your script can tell whether it's being run from the + command line, and should therefore do its thing and then quit; or + merely being loaded into a Lisp system, e.g., for debugging or + development, and should sit still and not do anything until it's + asked. + +See the manual for the complete list of guarantees. + + +* Invoking Lisp implementations + +** Basic use + +A secondary use of ~runlisp~ is in build scripts for Lisp programs. If +the entire project is just a Lisp library, then it's possibly acceptable +to just provide an ASDF system definition and expect users to type +~(asdf:load-system "mumble")~ to use it. If it's a program, or there +are things other than Lisp which ASDF can't or shouldn't handle -- +significant pieces in other languages, or a Lisp executable image to +make and install -- then it seems sensible to make the project's main +build system be something language-agnostic, say Unix ~make~, and +arrange for that to invoke ASDF at the appropriate time. + +But how should that be arranged? It's relatively easy for a project' +Lisp code to support multiple Lisp implementation; but each +implementation wants different runes for evaluating Lisp forms from the +command line, and some of them don't provide an ideal environment for +integrating into a build system. So ~runlisp~ provides a simple common +command-line interface for evaluating Lisp forms. For example: + +: $ runlisp -e '(format t "~A~%" (+ 1 2))' +: 3 + +If your build script needs to get information out of Lisp, then wrapping +~format~, or even ~prin1~, around forms is annoying; so ~runlisp~ has a +~-p~ option which prints the values of the forms it evaluates. + +: $ runlisp -e '(+ 1 2)' +: 3 + +If a form produces multiple values, then ~-p~ will print all of them +separated by spaces, on a single line: + +: $ runlisp -p '(floor 5 2)' +: 2 1 + +In addition to evaluating forms with ~-e~, and printing their values +with ~-p~, you can also load a file of Lisp code using ~-l~. + +When ~runlisp~ is acting on ~-e~, ~-p~, and/or ~-l~ options, it's said +to be running in /eval/ mode, rather than its usual /script/ mode. In +script mode, it /doesn't/ set ~:runlisp-script~ in ~*features*~. + +You can still insist that ~runlisp~ use a particular Lisp +implementation, or one of a subset of implementations, using the ~-L~ +option mentioned above. + +: $ runlisp -Lsbcl -p "(lisp-implementation-type)" +: "SBCL" + +** Command-line processing + +When scripting a Lisp -- as opposed to running a Lisp script -- it's not +necessarily the case that your script knows in advance exactly what it +needs to ask Lisp to do. For example, it might need to tell Lisp to +install a program in a particular directory, determined by Autoconf. +While it's certainly /possible/ to quote such data and splice them into +Lisp forms, it's more convenient to pass them in separately. So +~runlisp~ ensures that the command-line options are available to Lisp +forms via ~uiop:*command-line-arguments*~, as they are to a Lisp script. + +: $ runlisp -p "uiop:*command-line-arguments*" one two three +: ("one" "two" "three") + +When running Lisp forms like this, ~(uiop:argv0)~ isn't very +meaningful. (Currently, it reveals the name of the script which +~runlisp~ uses to implement this feature.) + + +* Configuring =runlisp= + +** Where =runlisp= looks for configuration + +You can influence which Lisp implementations are chosen by ~runlisp~ by +writing a configuration file, and/or setting an environment variable. + +~runlisp~ looks for configuration in ~~/.runlisprc~, and in +~~/.config/runlisprc~. You could put configuration in both, but that +doesn't seem like a great idea. A configuration file just contains +blank lines, comments, and command-line options, just as you'd write +them to the shell. Simple quoting and escaping is provided: see the +manual page for the full details. Each line is processed independently, +so it doesn't work to write an option on one line and then its argument +on the next. + +The environment variable ~RUNLISP_OPTIONS~ is processed /after/ reading +the configuration file(s), if any. Again, it should contain +command-line options, as you'd write them to the shell. + +** Deciding which Lisp implementation to use + +The most useful option to use here is ~-P~, which builds up a +/preference list/, in order. The argument to ~-P~ is a comma-separated +list of Lisp implementation names, just like you'd give to ~-L~. + +If you provide multiple ~-P~ options (e.g., on different lines of your +configuration file, or separately in the configuration file and +environment variable, then the lists are concatenated. Since the +environment variable is processed after the configuration file, this +means that + +When deciding which Lisp implementation to use, ~runlisp~ works as +follows. It builds a list of /acceptable/ Lisp implementations from the +~-L~ options, and a list of /preferred/ Lisp implementations from the +~-P~ options. If there aren't any ~-L~ options, then it assumes that +/all/ Lisp implementations are acceptable; but if there are no ~-P~ +options then it assumes that /no/ Lisp implementations are preferred. +It then works through the preferred list in order: if it finds an +implementation which is installed and acceptable, then it uses that one. +If that doesn't work, then it works through the acceptable +implementations that it hasn't tried yet, in order, and if it finds one +of those that's installed, then it runs that one. Otherwise it reports +an error and gives up. + +** Clearing the preferred list + +Since the environment variable is processed after the configuration +files, it can only append more Lisp implementations to the end of the +preferred list, which may well not be so helpful. There's an additional +option ~-C~, which completely clears the preferred list. The idea is +that you can write ~-C~ at the start of your ~RUNLISP_OPTIONS~ +environment variable to temporarily override your usual configuration +for some special effect. + + +* What's wrong with =cl-launch=? + +The short version is that ~cl-launch~ is slow and inconvenient. +~cl-launch~ is a big, complicated Common Lisp/Bourne shell polyglot +which tries to do everything but doesn't quite succeed. + +** It's slow. + +I took a trivial Lisp script: + +: (format t "Hello from ~A!~%~ +: Script = `~A'~%~ +: Arguments = (~{`~A'~^, ~})~%" +: (lisp-implementation-type) +: (uiop:argv0) +: uiop:*command-line-arguments*) + +I timed how long it took to run on all of ~runlisp~'s supported Lisp +implementations, and compared them to how long ~cl-launch~ took: the +results are shown in table [[tab:runlisp-vanilla]]. ~runlisp~ is /at least/ +two and half times faster at running this script than ~cl-launch~ on all +implementations except Clozure CL[fn:slow-ccl], and approaching four and +a half times faster on SBCL. + +#+CAPTION: ~cl-launch~ vs ~runlisp~ (with vanilla images) +#+NAME: tab:runlisp-vanilla +#+ATTR_LATEX: :float t :placement [tbp] +|------------------+-------------------+-----------------+----------------------| +| *Implementation* | *~cl-launch~ (s)* | *~runlisp~ (s)* | *~runlisp~ (factor)* | +|------------------+-------------------+-----------------+----------------------| +| ABCL | 7.3036 | 2.6027 | 2.806 | +| Clozure CL | 1.2769 | 0.9678 | 1.319 | +| GNU CLisp | 1.2498 | 0.2659 | 4.700 | +| CMU CL | 0.9665 | 0.3065 | 3.153 | +| ECL | 0.8025 | 0.3173 | 2.529 | +| SBCL | 0.3266 | 0.0739 | 4.419 | +|------------------+-------------------+-----------------+----------------------| +#+TBLFM: $4=$2/$3;%.3f + +But this is using the `vanilla' Lisp images installed with the +implementations. ~runlisp~ by default builds custom images for most +Lisp implementations, which improves startup performance significantly; +see table [[tab:runlisp-custom]]. (I don't currently know how to build a +useful custom image for ABCL. ~runlisp~ does build a custom image for +ECL, but it doesn't help significantly.) These results are summarized +in figure [[fig:lisp-graph]]. + +#+CAPTION: ~cl-launch~ vs ~runlisp~ (with custom images) +#+NAME: tab:runlisp-custom +#+ATTR_LATEX: :float t :placement [tbp] +|------------------+-------------------+-----------------+----------------------| +| *Implementation* | *~cl-launch~ (s)* | *~runlisp~ (s)* | *~runlisp~ (factor)* | +|------------------+-------------------+-----------------+----------------------| +| ABCL | 7.3036 | 2.5873 | 2.823 | +| Clozure CL | 1.2769 | 0.0088 | 145.102 | +| GNU CLisp | 1.2498 | 0.0146 | 85.603 | +| CMU CL | 0.9665 | 0.0063 | 153.413 | +| ECL | 0.8025 | 0.3185 | 2.520 | +| SBCL | 0.3266 | 0.0077 | 42.416 | +|------------------+-------------------+-----------------+----------------------| +#+TBLFM: $4=$2/$3;%.3f + +#+CAPTION: Comparison of ~runlisp~ and ~cl-launch~ times +#+NAME: fig:lisp-graph +#+ATTR_LATEX: :float t :placement [tbp] +[[file:doc/lisp-graph.tikz]] + +Unlike ~cl-launch~, with some Lisp implementations at least, ~runlisp~ +startup performance is usefully comparable to other popular scripting +language implementations. I wrote similarly trivial scripts in a number +of other languages, and timed them; the results are tabulated in table +[[tab:runlisp-interp]] and graphed in figure [[fig:interp-graph]]. + +#+CAPTION: ~runlisp~ vs other interpreters +#+NAME: tab:runlisp-interp +#+ATTR_LATEX: :float t :placement [tbp] +|------------------------------+-------------| +| *Implementation* | *Time (ms)* | +|------------------------------+-------------| +| Clozure CL | 8.8 | +| GNU CLisp | 14.6 | +| CMU CL | 6.3 | +| SBCL | 7.7 | +|------------------------------+-------------| +| Perl | 1.2 | +| Python | 10.3 | +|------------------------------+-------------| +| Debian Almquist shell (dash) | 1.4 | +| GNU Bash | 2.0 | +| Z Shell | 4.1 | +|------------------------------+-------------| +| Tiny C (compile & run) | 1.2 | +| GCC (precompiled) | 0.5 | +|------------------------------+-------------| + +#+CAPTION: Comparison of ~runlisp~ and other script interpreters +#+NAME: fig:interp-graph +#+Attr_latex: :float t :placement [tbp] +[[file:doc/interp-graph.tikz]] + +(All the timings in this section were performed on the same 2020 Dell +XPS13 laptop running Debian `buster'. The tools used to make the +measurements are included in the source distribution, in the ~bench/~ +subdirectory.) + +[fn:slow-ccl] I don't know why Clozure CL shows such a small difference +here. + +** It's inconvenient + +~cl-launch~ has this elaborate machinery which reads shell script +fragments from various places and sets variables like ~$LISPS~, but it +doesn't quite work. + +Unlike other scripting languages such as Perl or Python, Common Lisp has +lots of implementations, and they all have various unique features (and +bugs) which a script might rely on (or need to avoid). Also, a user +might have preferences about which Lisps to use. ~cl-launch~'s approach +to this problem is a ~system_preferred_lisps~ shell function which can +be used in ~~/.cl-launchrc~ to select a Lisp system for a particular +`software system', though this notion doesn't appear to be well-defined, +but this all works by editing a single ~$LISPS~ shell variable. By +contrast, ~runlisp~ has a ~-L~ option with which scripts can specify the +Lisp systems they support (in a preference order), and a ~-P~ option +with which users can express their own preferences (e.g., in the +environment or a configuration file): ~runlisp~ will never choose a Lisp +system which the script can't deal with, but it will respect the user's +relative preferences. + +** It doesn't establish a (useful) common environment + +A number of Lisp systems are annoyingly deficient in their handling of +scripts. + +For example, when GNU CLisp's ~-x~ option is used, it rebinds +~*standard-input*~ to an internal string stream holding the expression +passed in on the command line, leaving the process's actual stdin nearly +impossible to access. + +: $ date | cl-launch -l sbcl -i "(princ (read-line nil nil))" # expected +: Sun 9 Aug 14:39:10 BST 2020 +: $ date | cl-launch -l clisp -i "(princ (read-line nil nil))" # bug! +: NIL + +As another example, Armed Bear Common Lisp doesn't seem to believe in +the stderr stream: when it starts up, ~*error-ouptut*~ is bound to the +standard output, just like ~*standard-output*~. Also, ~cl-launch~ +loading ASDF causes a huge number of ~style-warning~ messages to be +written to stdout, making ABCL pretty much useless for writing filter +scripts. + +: $ cl-launch -l sbcl -i '(progn +: (format *standard-output* "output~%") +: (format *error-output* "error~%"))' \ +: > >(sed 's/^/stdout: /') 2> >(sed 's/^/stderr: /') +: stdout: output +: stderr: error +: $ cl-launch -l abcl -i '(progn +: (format *standard-output* "output~%") +: (format *error-output* "error~%"))' \ +: > >(sed 's/^/stdout: /') 2> >(sed 's/^/stderr: /') +: [1813 lines of compiler warnings tagged `stdout:'] +: stdout: output +: stdout: error + +~runlisp~ takes care of all of this, providing a basic but useful common +level of shell integration for all its supported Lisp implementations. +In particular: + + + It ensures that the standard Unix `stdin', `stdout', and `stdarr' + file descriptors are hooked up to the Lisp ~*standard-input*~, + ~*standard-output*~, and ~*error-output*~ streams. + + + It ensures that starting a script doesn't write a deluge of + diagnostic drivel. + +The complete details are given in ~runlisp~'s manpage. + +** Why might one prefer =cl-launch= anyway? + +On the other hand, ~cl-launch~ is well established and full-featured. + +~cl-launch~ compiles scripts before trying to run them, so they'll run +faster on Lisps which use an interpreter by default. It has a caching +feature so running a script a second time doesn't need to recompile it. +If your scripts are compute-intensive and benefit from ahead-of-time +compilation then maybe ~cl-launch~ is preferable. + +~cl-launch~ supports more Lisp systems. I only have six installed on my +development machine at the moment, so those are the ones that ~runlisp~ +supports. If you want your scripts to be able to run on other Lisps, +then ~cl-launch~ is the way to do that. Of course, I welcome patches to +help ~runlisp~ support other free Lisp implementations. ~cl-launch~ +also supports proprietary Lisps: I have very little interest in these, +so if you want to run scripts using Allegro or LispWorks then +~cl-launch~ is your only choice. diff --git a/bench/Makefile.am b/bench/Makefile.am new file mode 100644 index 0000000..cb5eef7 --- /dev/null +++ b/bench/Makefile.am @@ -0,0 +1,135 @@ +### -*-makefile-*- +### +### Build script for start-up benchmarks +### +### (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 . + +include $(top_srcdir)/vars.am + +GNUPLOT = gnuplot + +FORCE = +FORCE: +.PHONY: FORCE + +###-------------------------------------------------------------------------- +### Preliminaries. + +v_bench = $(v_bench_@AM_V@) +v_bench_ = $(v_bench_@AM_DEFAULT_V@) +v_bench_0 = @echo " BENCH $@"; + +BENCHES = +bench: $(BENCHES) + +noinst_PROGRAMS += timeit +timeit_SOURCES = timeit.c + +CLEANFILES += *.out *.bench + +###-------------------------------------------------------------------------- +### Lisp systems using `runlisp'. + +RUNLISP = $(top_builddir)/runlisp -I$(top_builddir)/ +EXTRA_DIST += t.lisp + +RUNLISP_BENCHES = $(foreach l,$(LISPS), runlisp.$l.bench) +BENCHES += $(RUNLISP_BENCHES) +$(RUNLISP_BENCHES): runlisp.%.bench: timeit $(FORCE) + $(v_bench)./timeit $(RUNLISP) -L$* -- $(srcdir)/t.lisp a b c >runlisp.$*.out 2>$@ + +RUNLISP_NOIMAGE_BENCHES = $(foreach l,$(LISPS), runlisp-noimage.$l.bench) +BENCHES += $(RUNLISP_NOIMAGE_BENCHES) +$(RUNLISP_NOIMAGE_BENCHES): runlisp-noimage.%.bench: timeit $(FORCE) + $(v_bench)./timeit $(RUNLISP) -D -L$* -- $(srcdir)/t.lisp a b c >runlisp-noimage.$*.out 2>$@ + +###-------------------------------------------------------------------------- +### Lisp systems using `cl-launch'. + +CL_LAUNCH_BENCHES = $(foreach l,$(LISPS), cl-launch.$l.bench) +BENCHES += $(CL_LAUNCH_BENCHES) +$(CL_LAUNCH_BENCHES): cl-launch.%.bench: timeit $(FORCE) + $(v_bench)./timeit cl-launch -X -l $* -- $(srcdir)/t.lisp a b c >cl-launch.$*.out 2>$@ + +###-------------------------------------------------------------------------- +### C programs (as a baseline). + +BENCHES += c.tcc.bench +c.tcc.bench: timeit $(FORCE) + $(v_bench)./timeit tcc -run $(srcdir)/t.c a b c >c.tcc.out 2>$@ + +BENCHES += c.gcc.bench +noinst_PROGRAMS += t.c.gcc +t_c_gcc_SOURCES = t.c +c.gcc.bench: t.c.gcc timeit $(FORCE) + $(v_bench)./timeit ./t.c.gcc a b c >c.gcc.out 2>$@ + +###-------------------------------------------------------------------------- +### Other scripting languages. + +BENCHES += perl.bench +EXTRA_DIST += t.pl +perl.bench: timeit $(FORCE) + $(v_bench)./timeit perl -- $(srcdir)/t.pl a b c >perl.out 2>$@ + +BENCHES += python.bench +EXTRA_DIST += t.py +python.bench: timeit $(FORCE) + $(v_bench)./timeit python -- $(srcdir)/t.py a b c >python.out 2>$@ + +SHELLS = dash bash zsh +EXTRA_DIST += t.sh +SHELL_BENCHES = $(foreach s,$(SHELLS), shell.$s.bench) +BENCHES += $(SHELL_BENCHES) +$(SHELL_BENCHES): shell.%.bench: timeit $(FORCE) + $(v_bench)TEST_SHELL=$* ./timeit $* -- $(srcdir)/t.sh a b c >shell.$*.out 2>$@ + +###-------------------------------------------------------------------------- +### Reporting. + +GRAPHS = +noinst_DATA += $(GRAPHS) +CLEANFILES += $(GRAPHS) + +v_massage = $(v_massage_@AM_V@) +v_massage_ = $(v_massage_@AM_DEFAULT_V@) +v_massage_0 = @echo " MASSAGE $@"; + +v_gnuplot = $(v_gnuplot_@AM_V@) +v_gnuplot_ = $(v_gnuplot_@AM_DEFAULT_V@) +v_gnuplot_0 = @echo " GNUPLOT $@"; + +CLEANFILES += bench.data +bench.data: $(BENCHES) massage-benchmarks + $(v_massage)$(srcdir)/massage-benchmarks >$@.new && mv $@.new $@ + +GRAPHS += lisp-graph.tikz +lisp-graph.tikz: lisp-graph.gp bench.data + $(v_gnuplot)$(GNUPLOT) $< >$@.new && mv $@.new $@ + +GRAPHS += interp-graph.tikz +interp-graph.tikz: interp-graph.gp bench.data + $(v_gnuplot)$(GNUPLOT) $< >$@.new && mv $@.new $@ + +graphs: $(GRAPHS) +.PHONY: graphs + +###----- That's all, folks -------------------------------------------------- diff --git a/bench/interp-graph.gp b/bench/interp-graph.gp new file mode 100644 index 0000000..1066f1f --- /dev/null +++ b/bench/interp-graph.gp @@ -0,0 +1,15 @@ +### -*-gnuplot-*- + +set terminal tikz + +set style data histogram +set xtic rotate by -35 offset -1, 0 scale 0 +set style fill solid +set style histogram cluster gap 1 + +unset key +set border 3 +set tics nomirror +set ylabel "Time (ms) to run trivial script" + +plot "bench.data" index "> interp" using (1000*$2):xtic(1) diff --git a/bench/lisp-graph.gp b/bench/lisp-graph.gp new file mode 100644 index 0000000..6b9550f --- /dev/null +++ b/bench/lisp-graph.gp @@ -0,0 +1,15 @@ +### -*-gnuplot-*- + +set terminal tikz + +set style data histogram +set xtics rotate by -35 scale 0 +set style fill solid +set style histogram cluster + +set border 3 +set tics nomirror +set ylabel "Time (s) to run trivial script" + +plot for [i = 2:4] "bench.data" index "> lisp" using i:xtic(1) \ + title columnheader(i) diff --git a/bench/massage-benchmarks b/bench/massage-benchmarks new file mode 100755 index 0000000..9470b84 --- /dev/null +++ b/bench/massage-benchmarks @@ -0,0 +1,56 @@ +#! /usr/bin/perl + +use autodie; + +my %LISP = + ("sbcl" => "SBCL", + "ccl" => "Clozure CL", + "ecl" => "ECL", + "clisp" => "GNU CLisp", + "cmucl" => "CMU CL", + "abcl" => "ABCL"); +my %LABEL = + ("perl" => "Perl", + "python" => "Python", + "c.tcc" => "Tiny C", + "c.gcc" => "GCC", + "shell.dash" => "dash", + "shell.bash" => "GNU Bash", + "shell.zsh" => "Z Shell"); + +for my $l (keys %LISP) { $LABEL{"runlisp.$l"} = $LISP{$l}; } + +{ + my %d; + + sub timing ($) { + my ($f) = @_; + return $d{$f} if exists $d{$f}; + open my $fh, "<", "$f.bench"; + (my $data = readline $fh) =~ s/^.* elapsed = ([0-9.]+)s.*$/$1/; + return $d{$f} = $data; + } +} + +print < lisp +"Lisp system" "\\\\texttt{cl-launch}" "\\\\texttt{runlisp} (vanilla image)" "\\\\texttt{runlisp} (custom image)" +EOF +for my $l (sort keys %LISP) { + printf "\"%s\" %.4f %.4f %.4f\n", + $LISP{$l}, + timing("cl-launch.$l"), + timing("runlisp-noimage.$l"), + timing("runlisp.$l"); +} +print "\n\n"; + +print < interp +EOF +for my $i + ("runlisp.ccl", "runlisp.clisp", "runlisp.cmucl", "runlisp.sbcl", + "perl", "python", + "shell.dash", "shell.bash", "shell.zsh", + "c.tcc", "c.gcc") + { printf "\"%s\" %.4f\n", $LABEL{$i}, timing $i; } diff --git a/bench/t.c b/bench/t.c new file mode 100644 index 0000000..7b06527 --- /dev/null +++ b/bench/t.c @@ -0,0 +1,26 @@ +#include + +#if __clang__ +# define IMPL "Clang" +#elif __TINYC__ +# define IMPL "TCC" +#elif __GNUC__ +# define IMPL "GCC" +#else +# define IMPL "an unknown C implementation" +#endif + +int main(int argc, char *argv[]) +{ + int i; + + puts("Hello from " IMPL "!"); + printf("Script = `%s'\n", argv[0]); + fputs("Arguments = (", stdout); + for (i = 1; i < argc; i++) { + if (i > 1) fputs(", ", stdout); + printf("`%s'", argv[i]); + } + putchar(')'); putchar('\n'); + return (0); +} diff --git a/bench/t.lisp b/bench/t.lisp new file mode 100755 index 0000000..0a9010e --- /dev/null +++ b/bench/t.lisp @@ -0,0 +1,8 @@ +#! /usr/bin/runlisp + +(format t "Hello from ~A!~%~ + Script = `~A'~%~ + Arguments = (~{`~A'~^, ~})~%" + (lisp-implementation-type) + (uiop:argv0) + uiop:*command-line-arguments*) diff --git a/bench/t.pl b/bench/t.pl new file mode 100755 index 0000000..8997da2 --- /dev/null +++ b/bench/t.pl @@ -0,0 +1,7 @@ +#! /usr/bin/perl + +printf < +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static const char *progname = "timeit"; + +static void set_progname(const char *prog) +{ + const char *p; + + p = strrchr(prog, '/'); + progname = p ? p + 1 : progname; +} + +static void lose(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + fprintf(stderr, "%s: ", progname); + vfprintf(stderr, msg, ap); + fputc('\n', stderr); + va_end(ap); + exit(127); +} + +static double timeval_to_float(const struct timeval *tv) + { return (tv->tv_sec + tv->tv_usec*1e-6); } + +int main(int argc, char *argv[]) +{ + struct rusage ru; + struct timeval t0, t1, t; + pid_t kid; + int i, st; + + set_progname(argv[0]); + gettimeofday(&t0, 0); + kid = fork(); if (kid < 0) lose("fork failed: %s", strerror(errno)); + if (!kid) { + execvp(argv[1], argv + 1); + lose("exec (`%s') failed: %s", argv[1], strerror(errno)); + } + if (wait4(kid, &st, 0, &ru) < 0) lose("wait failed: %s", strerror(errno)); + gettimeofday(&t1, 0); + if (st) { + if (WIFSIGNALED(st)) + lose("program killed by signal %d\n", WTERMSIG(st)); + else if (WIFEXITED(st)) + lose("program failed with status %d\n", WEXITSTATUS(st)); + else + lose("program exited with incomprehensible status 0x%04x\n", st); + } + + if (t0.tv_usec > t1.tv_usec) { + t.tv_sec = t1.tv_sec - t0.tv_sec - 1; + t.tv_usec = t1.tv_usec + 1000000 - t0.tv_usec; + } else { + t.tv_sec = t1.tv_sec - t0.tv_sec; + t.tv_usec = t1.tv_usec - t0.tv_usec; + } + + for (i = 1; i < argc; i++) { + if (i > 1) fputc(' ', stderr); + fputs(argv[i], stderr); + } + fprintf(stderr, ": elapsed = %.4fs; user = %.4fs; system = %.4fs\n", + timeval_to_float(&t), + timeval_to_float(&ru.ru_utime), + timeval_to_float(&ru.ru_stime)); + return (0); +} diff --git a/config/auto-version b/config/auto-version new file mode 120000 index 0000000..652e105 --- /dev/null +++ b/config/auto-version @@ -0,0 +1 @@ +../.ext/cfd/build/auto-version \ No newline at end of file diff --git a/config/confsubst b/config/confsubst new file mode 120000 index 0000000..8e7de22 --- /dev/null +++ b/config/confsubst @@ -0,0 +1 @@ +../.ext/cfd/build/confsubst \ No newline at end of file diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..042df98 --- /dev/null +++ b/configure.ac @@ -0,0 +1,118 @@ +dnl -*-autoconf-*- +dnl +dnl Configuration script for `runlisp' +dnl +dnl (c) 2020 Mark Wooding +dnl + +dnl----- Licensing notice --------------------------------------------------- +dnl +dnl This file is part of Runlisp, a tool for invoking Common Lisp scripts. +dnl +dnl Runlisp is free software: you can redistribute it and/or modify it +dnl under the terms of the GNU General Public License as published by the +dnl Free Software Foundation; either version 3 of the License, or (at your +dnl option) any later version. +dnl +dnl Runlisp is distributed in the hope that it will be useful, but WITHOUT +dnl ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +dnl FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +dnl for more details. +dnl +dnl You should have received a copy of the GNU General Public License +dnl along with Runlisp. If not, see . + +dnl-------------------------------------------------------------------------- +dnl Initialization. + +mdw_AUTO_VERSION +AC_INIT([runlisp], AUTO_VERSION, [mdw@distorted.org.uk]) +AC_CONFIG_SRCDIR([runlisp.c]) +AC_CONFIG_AUX_DIR([config]) +AM_INIT_AUTOMAKE([foreign]) +mdw_SILENT_RULES + +AC_PROG_CC +AX_CFLAGS_WARN_ALL + +AC_CHECK_PROGS([AUTOM4TE], [autom4te]) + +dnl-------------------------------------------------------------------------- +dnl Checking for Lisp implementations. + +imagedir=$localstatedir/$PACKAGE_NAME; AC_SUBST(imagedir) +mdw_DEFINE_PATHS([ + mdw_DEFINE_PATH([IMAGEDIR], [$imagedir]) + mdw_DEFINE_PATH([DATADIR], [$datadir/$PACKAGE_NAME])]) + +AC_ARG_ENABLE([imagedump], + [AS_HELP_STRING([--enable-imagedump[=SYSTEMS]], + [make dumps of Lisp SYSTEMS with ASDF etc. preloaded; + SYSTEMS is `yes', `no', or a comma-separated list of + system names])], + [], [enable_imagedump=yes]) + +AC_DEFUN([mdw_CHECK_LISP], +[AC_CHECK_PROGS([$1], [$2]) +AC_ARG_VAR([$1], [Path to the $1 Lisp system.]) +case ,$enable_imagedump, in + ,yes, | *,$2,*) dump=t ;; + *) dump=nil ;; +esac +AM_CONDITIONAL([DUMP_$1], [test $dump = t])]) + +mdw_CHECK_LISP([SBCL], [sbcl]) +mdw_CHECK_LISP([CCL], [ccl]) +mdw_CHECK_LISP([CLISP], [clisp]) +mdw_CHECK_LISP([ECL], [ecl]) +mdw_CHECK_LISP([CMUCL], [cmucl]) +mdw_CHECK_LISP([ABCL], [abcl]) + +dnl ECL is changing its command-line option syntax, because that will make +dnl things much better or something. So we need to figure out which version +dnl of the syntax to use. +mdw_ecl_opts=hunoz +if test "x$ECL" != x; then + AC_MSG_CHECKING([ECL command-line option flavour]) + ver=$($ECL --version) + case $ver in + [ECL\ [0-9].*] | [ECL\ 1[0-5].*]) mdw_ecl_opts=trad ;; + [ECL\ 1[6-9].*] | [ECL\ [2-9][0-9].*]) mdw_ecl_opts=gnu ;; + *) AC_MSG_ERROR([unsupported ECL version \`$ver']) ;; + esac + AC_MSG_RESULT([$mdw_ecl_opts]) + case $mdw_ecl_opts in + gnu) AC_DEFINE([ECL_OPTIONS_GNU], [1], + [Define 1 if ECL uses GNU-style `--FOO' options]) ;; + esac +fi +case $mdw_ecl_opts in + gnu) ECLOPT=-- ;; + trad) ECLOPT=- ;; + *) AC_MSG_ERROR([internal error: unexpected value for `$mdw_ecl_opts']) ;; +esac +AC_SUBST([ECLOPT]) + +dnl-------------------------------------------------------------------------- +dnl Benchmarking support. + +dnl This has lots of random dependencies, and isn't really very useful. Turn +dnl it off unless the user is very keen. +AC_ARG_ENABLE([benchmark], + [AS_HELP_STRING([--enable-benchmark], + [turn on script-startup benchmark machinery])], + [mdw_bench=$enableval], [mdw_bench=no]) +AM_CONDITIONAL([BENCHMARK], [test "$mdw_bench" = yes]) + +dnl-------------------------------------------------------------------------- +dnl Produce output. + +AC_CONFIG_HEADER([config/config.h]) +AC_CONFIG_TESTDIR([t]) + +AC_CONFIG_FILES([Makefile] + [bench/Makefile doc/Makefile] + [t/Makefile t/atlocal]) +AC_OUTPUT + +dnl----- That's all, folks -------------------------------------------------- diff --git a/doc/Makefile.am b/doc/Makefile.am new file mode 100644 index 0000000..6bab640 --- /dev/null +++ b/doc/Makefile.am @@ -0,0 +1,32 @@ +### -*-makefile-*- +### +### Additional documentation files +### +### (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 . + +include $(top_srcdir)/vars.am + +EXTRA_DIST += bench.data + +EXTRA_DIST += lisp-graph.tikz +EXTRA_DIST += interp-graph.tikz + +###----- That's all, folks -------------------------------------------------- diff --git a/doc/bench.data b/doc/bench.data new file mode 100644 index 0000000..25d0a83 --- /dev/null +++ b/doc/bench.data @@ -0,0 +1,22 @@ +#> lisp +"Lisp system" "\\texttt{cl-launch}" "\\texttt{runlisp} (vanilla image)" "\\texttt{runlisp} (custom image)" +"ABCL" 7.3036 2.6027 2.5873 +"Clozure CL" 1.2769 0.9678 0.0088 +"GNU CLisp" 1.2498 0.2659 0.0146 +"CMU CL" 0.9665 0.3065 0.0063 +"ECL" 0.8025 0.3173 0.3185 +"SBCL" 0.3266 0.0739 0.0077 + + +#> interp +"Clozure CL" 0.0088 +"GNU CLisp" 0.0146 +"CMU CL" 0.0063 +"SBCL" 0.0077 +"Perl" 0.0012 +"Python" 0.0103 +"dash" 0.0014 +"GNU Bash" 0.0020 +"Z Shell" 0.0041 +"Tiny C" 0.0012 +"GCC" 0.0005 diff --git a/doc/interp-graph.tikz b/doc/interp-graph.tikz new file mode 100644 index 0000000..ac39f8a --- /dev/null +++ b/doc/interp-graph.tikz @@ -0,0 +1,68 @@ +\begin{tikzpicture}[gnuplot] +%% generated with GNUPLOT 5.2p6 (Lua 5.3; terminal rev. Nov 2018, script rev. 107) +%% Sat 15 Aug 2020 14:07:28 BST +\path (0.000,0.000) rectangle (12.500,8.750); +\gpcolor{color=gp lt color border} +\gpsetlinetype{gp lt border} +\gpsetdashtype{gp dt solid} +\gpsetlinewidth{1.00} +\draw[gp path] (1.136,1.363)--(1.316,1.363); +\node[gp node right] at (0.952,1.363) {$0$}; +\draw[gp path] (1.136,2.248)--(1.316,2.248); +\node[gp node right] at (0.952,2.248) {$2$}; +\draw[gp path] (1.136,3.133)--(1.316,3.133); +\node[gp node right] at (0.952,3.133) {$4$}; +\draw[gp path] (1.136,4.017)--(1.316,4.017); +\node[gp node right] at (0.952,4.017) {$6$}; +\draw[gp path] (1.136,4.902)--(1.316,4.902); +\node[gp node right] at (0.952,4.902) {$8$}; +\draw[gp path] (1.136,5.787)--(1.316,5.787); +\node[gp node right] at (0.952,5.787) {$10$}; +\draw[gp path] (1.136,6.672)--(1.316,6.672); +\node[gp node right] at (0.952,6.672) {$12$}; +\draw[gp path] (1.136,7.556)--(1.316,7.556); +\node[gp node right] at (0.952,7.556) {$14$}; +\draw[gp path] (1.136,8.441)--(1.316,8.441); +\node[gp node right] at (0.952,8.441) {$16$}; +\node[gp node left,rotate=-35] at (1.853,1.179) {Clozure CL}; +\node[gp node left,rotate=-35] at (2.754,1.179) {GNU CLisp}; +\node[gp node left,rotate=-35] at (3.655,1.179) {CMU CL}; +\node[gp node left,rotate=-35] at (4.556,1.179) {SBCL}; +\node[gp node left,rotate=-35] at (5.457,1.179) {Perl}; +\node[gp node left,rotate=-35] at (6.358,1.179) {Python}; +\node[gp node left,rotate=-35] at (7.258,1.179) {dash}; +\node[gp node left,rotate=-35] at (8.159,1.179) {GNU Bash}; +\node[gp node left,rotate=-35] at (9.060,1.179) {Z Shell}; +\node[gp node left,rotate=-35] at (9.961,1.179) {Tiny C}; +\node[gp node left,rotate=-35] at (10.862,1.179) {GCC}; +\draw[gp path] (1.136,8.441)--(1.136,1.363)--(11.947,1.363); +\node[gp node center,rotate=-270] at (0.276,4.902) {Time (ms) to run trivial script}; +\gpfill{rgb color={0.580,0.000,0.827}} (1.812,1.363)--(2.263,1.363)--(2.263,5.257)--(1.812,5.257)--cycle; +\gpcolor{rgb color={0.580,0.000,0.827}} +\draw[gp path] (1.812,1.363)--(1.812,5.256)--(2.262,5.256)--(2.262,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (2.713,1.363)--(3.164,1.363)--(3.164,7.823)--(2.713,7.823)--cycle; +\draw[gp path] (2.713,1.363)--(2.713,7.822)--(3.163,7.822)--(3.163,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (3.614,1.363)--(4.065,1.363)--(4.065,4.151)--(3.614,4.151)--cycle; +\draw[gp path] (3.614,1.363)--(3.614,4.150)--(4.064,4.150)--(4.064,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (4.514,1.363)--(4.966,1.363)--(4.966,4.770)--(4.514,4.770)--cycle; +\draw[gp path] (4.514,1.363)--(4.514,4.769)--(4.965,4.769)--(4.965,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (5.415,1.363)--(5.867,1.363)--(5.867,1.895)--(5.415,1.895)--cycle; +\draw[gp path] (5.415,1.363)--(5.415,1.894)--(5.866,1.894)--(5.866,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (6.316,1.363)--(6.768,1.363)--(6.768,5.920)--(6.316,5.920)--cycle; +\draw[gp path] (6.316,1.363)--(6.316,5.919)--(6.767,5.919)--(6.767,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (7.217,1.363)--(7.669,1.363)--(7.669,1.983)--(7.217,1.983)--cycle; +\draw[gp path] (7.217,1.363)--(7.217,1.982)--(7.668,1.982)--(7.668,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (8.118,1.363)--(8.570,1.363)--(8.570,2.249)--(8.118,2.249)--cycle; +\draw[gp path] (8.118,1.363)--(8.118,2.248)--(8.569,2.248)--(8.569,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (9.019,1.363)--(9.470,1.363)--(9.470,3.178)--(9.019,3.178)--cycle; +\draw[gp path] (9.019,1.363)--(9.019,3.177)--(9.469,3.177)--(9.469,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (9.920,1.363)--(10.371,1.363)--(10.371,1.895)--(9.920,1.895)--cycle; +\draw[gp path] (9.920,1.363)--(9.920,1.894)--(10.370,1.894)--(10.370,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (10.821,1.363)--(11.272,1.363)--(11.272,1.585)--(10.821,1.585)--cycle; +\draw[gp path] (10.821,1.363)--(10.821,1.584)--(11.271,1.584)--(11.271,1.363)--cycle; +\gpcolor{color=gp lt color border} +\draw[gp path] (1.136,8.441)--(1.136,1.363)--(11.947,1.363); +%% coordinates of the plot area +\gpdefrectangularnode{gp plot 1}{\pgfpoint{1.136cm}{1.363cm}}{\pgfpoint{11.947cm}{8.441cm}} +\end{tikzpicture} +%% gnuplot variables diff --git a/doc/lisp-graph.tikz b/doc/lisp-graph.tikz new file mode 100644 index 0000000..2a6ea13 --- /dev/null +++ b/doc/lisp-graph.tikz @@ -0,0 +1,90 @@ +\begin{tikzpicture}[gnuplot] +%% generated with GNUPLOT 5.2p6 (Lua 5.3; terminal rev. Nov 2018, script rev. 107) +%% Sat 15 Aug 2020 14:07:24 BST +\path (0.000,0.000) rectangle (12.500,8.750); +\gpcolor{color=gp lt color border} +\gpsetlinetype{gp lt border} +\gpsetdashtype{gp dt solid} +\gpsetlinewidth{1.00} +\draw[gp path] (0.952,1.363)--(1.132,1.363); +\node[gp node right] at (0.768,1.363) {$0$}; +\draw[gp path] (0.952,2.248)--(1.132,2.248); +\node[gp node right] at (0.768,2.248) {$1$}; +\draw[gp path] (0.952,3.133)--(1.132,3.133); +\node[gp node right] at (0.768,3.133) {$2$}; +\draw[gp path] (0.952,4.017)--(1.132,4.017); +\node[gp node right] at (0.768,4.017) {$3$}; +\draw[gp path] (0.952,4.902)--(1.132,4.902); +\node[gp node right] at (0.768,4.902) {$4$}; +\draw[gp path] (0.952,5.787)--(1.132,5.787); +\node[gp node right] at (0.768,5.787) {$5$}; +\draw[gp path] (0.952,6.672)--(1.132,6.672); +\node[gp node right] at (0.768,6.672) {$6$}; +\draw[gp path] (0.952,7.556)--(1.132,7.556); +\node[gp node right] at (0.768,7.556) {$7$}; +\draw[gp path] (0.952,8.441)--(1.132,8.441); +\node[gp node right] at (0.768,8.441) {$8$}; +\node[gp node left,rotate=-35] at (2.523,1.179) {ABCL}; +\node[gp node left,rotate=-35] at (4.093,1.179) {Clozure CL}; +\node[gp node left,rotate=-35] at (5.664,1.179) {GNU CLisp}; +\node[gp node left,rotate=-35] at (7.235,1.179) {CMU CL}; +\node[gp node left,rotate=-35] at (8.806,1.179) {ECL}; +\node[gp node left,rotate=-35] at (10.376,1.179) {SBCL}; +\draw[gp path] (0.952,8.441)--(0.952,1.363)--(11.947,1.363); +\node[gp node center,rotate=-270] at (0.276,4.902) {Time (s) to run trivial script}; +\node[gp node right] at (10.479,8.107) {\texttt{cl-launch}}; +\gpfill{rgb color={0.580,0.000,0.827}} (10.663,8.030)--(11.579,8.030)--(11.579,8.184)--(10.663,8.184)--cycle; +\gpcolor{rgb color={0.580,0.000,0.827}} +\draw[gp path] (10.663,8.030)--(11.579,8.030)--(11.579,8.184)--(10.663,8.184)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (2.209,1.363)--(2.524,1.363)--(2.524,7.826)--(2.209,7.826)--cycle; +\draw[gp path] (2.209,1.363)--(2.209,7.825)--(2.523,7.825)--(2.523,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (3.779,1.363)--(4.094,1.363)--(4.094,2.494)--(3.779,2.494)--cycle; +\draw[gp path] (3.779,1.363)--(3.779,2.493)--(4.093,2.493)--(4.093,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (5.350,1.363)--(5.665,1.363)--(5.665,2.470)--(5.350,2.470)--cycle; +\draw[gp path] (5.350,1.363)--(5.350,2.469)--(5.664,2.469)--(5.664,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (6.921,1.363)--(7.236,1.363)--(7.236,2.219)--(6.921,2.219)--cycle; +\draw[gp path] (6.921,1.363)--(6.921,2.218)--(7.235,2.218)--(7.235,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (8.491,1.363)--(8.807,1.363)--(8.807,2.074)--(8.491,2.074)--cycle; +\draw[gp path] (8.491,1.363)--(8.491,2.073)--(8.806,2.073)--(8.806,1.363)--cycle; +\gpfill{rgb color={0.580,0.000,0.827}} (10.062,1.363)--(10.377,1.363)--(10.377,1.653)--(10.062,1.653)--cycle; +\draw[gp path] (10.062,1.363)--(10.062,1.652)--(10.376,1.652)--(10.376,1.363)--cycle; +\gpcolor{color=gp lt color border} +\node[gp node right] at (10.479,7.799) {\texttt{runlisp} (vanilla image)}; +\gpfill{rgb color={0.000,0.620,0.451}} (10.663,7.722)--(11.579,7.722)--(11.579,7.876)--(10.663,7.876)--cycle; +\gpcolor{rgb color={0.000,0.620,0.451}} +\draw[gp path] (10.663,7.722)--(11.579,7.722)--(11.579,7.876)--(10.663,7.876)--cycle; +\gpfill{rgb color={0.000,0.620,0.451}} (2.523,1.363)--(2.838,1.363)--(2.838,3.667)--(2.523,3.667)--cycle; +\draw[gp path] (2.523,1.363)--(2.523,3.666)--(2.837,3.666)--(2.837,1.363)--cycle; +\gpfill{rgb color={0.000,0.620,0.451}} (4.093,1.363)--(4.409,1.363)--(4.409,2.220)--(4.093,2.220)--cycle; +\draw[gp path] (4.093,1.363)--(4.093,2.219)--(4.408,2.219)--(4.408,1.363)--cycle; +\gpfill{rgb color={0.000,0.620,0.451}} (5.664,1.363)--(5.979,1.363)--(5.979,1.599)--(5.664,1.599)--cycle; +\draw[gp path] (5.664,1.363)--(5.664,1.598)--(5.978,1.598)--(5.978,1.363)--cycle; +\gpfill{rgb color={0.000,0.620,0.451}} (7.235,1.363)--(7.550,1.363)--(7.550,1.635)--(7.235,1.635)--cycle; +\draw[gp path] (7.235,1.363)--(7.235,1.634)--(7.549,1.634)--(7.549,1.363)--cycle; +\gpfill{rgb color={0.000,0.620,0.451}} (8.806,1.363)--(9.121,1.363)--(9.121,1.645)--(8.806,1.645)--cycle; +\draw[gp path] (8.806,1.363)--(8.806,1.644)--(9.120,1.644)--(9.120,1.363)--cycle; +\gpfill{rgb color={0.000,0.620,0.451}} (10.376,1.363)--(10.691,1.363)--(10.691,1.429)--(10.376,1.429)--cycle; +\draw[gp path] (10.376,1.363)--(10.376,1.428)--(10.690,1.428)--(10.690,1.363)--cycle; +\gpcolor{color=gp lt color border} +\node[gp node right] at (10.479,7.491) {\texttt{runlisp} (custom image)}; +\gpfill{rgb color={0.337,0.706,0.914}} (10.663,7.414)--(11.579,7.414)--(11.579,7.568)--(10.663,7.568)--cycle; +\gpcolor{rgb color={0.337,0.706,0.914}} +\draw[gp path] (10.663,7.414)--(11.579,7.414)--(11.579,7.568)--(10.663,7.568)--cycle; +\gpfill{rgb color={0.337,0.706,0.914}} (2.837,1.363)--(3.152,1.363)--(3.152,3.653)--(2.837,3.653)--cycle; +\draw[gp path] (2.837,1.363)--(2.837,3.652)--(3.151,3.652)--(3.151,1.363)--cycle; +\gpfill{rgb color={0.337,0.706,0.914}} (4.408,1.363)--(4.723,1.363)--(4.723,1.372)--(4.408,1.372)--cycle; +\draw[gp path] (4.408,1.363)--(4.408,1.371)--(4.722,1.371)--(4.722,1.363)--cycle; +\gpfill{rgb color={0.337,0.706,0.914}} (5.978,1.363)--(6.293,1.363)--(6.293,1.377)--(5.978,1.377)--cycle; +\draw[gp path] (5.978,1.363)--(5.978,1.376)--(6.292,1.376)--(6.292,1.363)--cycle; +\gpfill{rgb color={0.337,0.706,0.914}} (7.549,1.363)--(7.864,1.363)--(7.864,1.370)--(7.549,1.370)--cycle; +\draw[gp path] (7.549,1.363)--(7.549,1.369)--(7.863,1.369)--(7.863,1.363)--cycle; +\gpfill{rgb color={0.337,0.706,0.914}} (9.120,1.363)--(9.435,1.363)--(9.435,1.646)--(9.120,1.646)--cycle; +\draw[gp path] (9.120,1.363)--(9.120,1.645)--(9.434,1.645)--(9.434,1.363)--cycle; +\gpfill{rgb color={0.337,0.706,0.914}} (10.690,1.363)--(11.006,1.363)--(11.006,1.371)--(10.690,1.371)--cycle; +\draw[gp path] (10.690,1.363)--(10.690,1.370)--(11.005,1.370)--(11.005,1.363)--cycle; +\gpcolor{color=gp lt color border} +\draw[gp path] (0.952,8.441)--(0.952,1.363)--(11.947,1.363); +%% coordinates of the plot area +\gpdefrectangularnode{gp plot 1}{\pgfpoint{0.952cm}{1.363cm}}{\pgfpoint{11.947cm}{8.441cm}} +\end{tikzpicture} +%% gnuplot variables diff --git a/dump-runlisp-image.1 b/dump-runlisp-image.1 new file mode 100644 index 0000000..d6bd3f5 --- /dev/null +++ b/dump-runlisp-image.1 @@ -0,0 +1,272 @@ +.\" -*-nroff-*- +.\" +.\" Manual for `dump-runlisp-image' +.\" +.\" (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 . +. +.ie t \{\ +. ds o \(bu +. if \n(.g \{\ +. fam P +. ev an-1 +. fam P +. ev +. \} +.\} +.el \{\ +. ds o o +.\} +. +.de hP +.IP +\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c +.. +.ds , \h'.16667m' +. +.\"-------------------------------------------------------------------------- +.TH runlisp 1 "12 August 2020" "Mark Wooding" +.SH NAME +dump-runlisp-image \- dump Lisp images for faster script execution +. +.\"-------------------------------------------------------------------------- +.SH SYNOPSIS +. +.B dump-runlisp-image +.RB [ \-acluv ] +.RB [ \-o +.IR output ] +.RI [ lisp +\&...] +. +.\"-------------------------------------------------------------------------- +.SH DESCRIPTION +. +The +.B dump-runlisp-image +program builds custom images for use by +.BR runlisp (1). +For many Lisp implementation, +a custom image, +with ASDF already loaded, +can start scripts much more quickly +than the `vanilla' images installed by deafult. +The downside is that custom images may be rather large. +. +.SS "Supperted Common Lisp implementations" +The following Lisp implementations are currently supported. +.TP +.B "ccl" +Clozure Common Lisp. +The default image name is +.BR ccl+asdf.image ; +a typical image can be 20\(en30\*,MB in size. +.TP +.B "clisp" +GNU CLisp. +The default image name is +.BR clisp+asdf.mem ; +a typical image is about 10\*,MB in size. +.TP +.B "cmucl" +Carnegie\(enMellon University Common Lisp. +The default image name is +.BR cmucl+asdf.core ; +a typical image is about 35\*,MB in size. +.TP +.B "ecl" +Embeddable Common Lisp. +The default image name is +.BR ecl+asdf ; +images comparatively very small +\(en about 4\*,MB \(en +but, sadly, not very effective. +.TP +.B "sbcl" +Steel Bank Common Lisp. +The default image name is +.BR sbcl+asdf.core ; +a typical image is nearly 45\*,MB in size. +.PP +(Although +.BR runlisp (3) +also supports Armed Bear Common Lisp, +.B dump-runlisp-image +currently doesn't.) +. +.SS "Options" +The following options are accepted on the command line. +. +.TP +.B "\-h" +Write a synopsis of +.BR dump-runlisp-image 's +command-line syntax +and a description of the command-line options +to standard output +and immediately exit with status 0. +. +.TP +.B "\-v" +Write +.BR dump-runlisp-image 's +version number +to standard output +and immediately exit with status 0. +. +.TP +.B "\-a" +Dump images for all Lisp supported implementations +which are installed . +This implies +.BR \-c , +described below. +You can't set +.B \-a +and also list implementations explicitly on the command line. +. +.TP +.B "\-c" +Only dump images for Lisp implementations +which are actually installed +(and can be found). +. +.TP +.B "\-l" +List the supported implementations +and the names of the image files for each +to standard output, +and immediately exit with status 0. +. +.TP +.BI "\-o " output +If +.I output +names a directory, +then write images to that directory +with their default names +(as listed above). +Otherwise, +exactly one Lisp implementation may be named, and +the image is written to a file named +.IR output . +By default, +images are written to the directory which +.BR runlisp (1) +will look in when checking for custom images +(shown in +.B "runlisp \-\-help" +or +.BR "dump-runlisp-image \-h" ), +unless overridden by the +.B RUNLISP_IMAGEDIR +environment variable. +. +.TP +.BI "\-u" +Don't create Lisp images +if a file with the appropriate name +already exists. +. +.TP +.BI "\-v" +Be more verbose about the process of creating images. +Lisp implementations can be rather noisy: +by default, +.B dump-runlisp-image +runs silently unless something goes wrong, +in which case it prints the failed Lisp command line +and its output. +If you set +.B \-v +then +.B dump-runlisp-image +will show Lisp implementation's noise immediately, +without waiting to see whether it succeeds or fails. +.PP +The +.B dump-runlisp-image +program will dump an image for each of the named +.I lisp +implementations in turn, +or all Lisp implementations, if +.B \-a +is set. +.PP +This involves invoking the Lisp implementations. +The +.B dump-runlisp-image +program expects, by default, +to be able to run a Lisp system +as a program with the same name, +found by searching as directed by the +.B PATH +environment variable. +This can be overridden by setting an environment variable, +with the same name but in +.IR "upper case" , +to the actual name \(en +either a bare filename to be searched for on the +.BR PATH , +or a pathname containing a +.RB ` / ', +relative to the working directory or absolute, +to the program. +Note that the entire variable value is used as the program name: +it's not possible to provide custom arguments to a Lisp system +using this mechanism. +If you want to do that, +you must write a shell script to do the necessary work, +and point the environment variable +(or the +.BR PATH ) +at your script. +(This is the same convention as +.BR runlisp (1).) +.PP +If +.B \-a +or +.B \-c +is set, +then +.B dump-runlisp-image +will skip Lisp implementations +which can't actually be found +(by searching the +.B PATH +for its command name). +. +.\"-------------------------------------------------------------------------- +. +.SH "BUGS" +.hP \*o +There's no support for making images for ABCL. +I don't really know what this would look like, +but I suspect it wouldn't help very much. +ABCL is terribly slow to start up anyway. +. +.SH "SEE ALSO" +.BR runlisp (1). +. +.SH "AUTHOR" +Mark Wooding, +. +.\"----- That's all, folks -------------------------------------------------- diff --git a/dump-runlisp-image.in b/dump-runlisp-image.in new file mode 100644 index 0000000..a4bf87e --- /dev/null +++ b/dump-runlisp-image.in @@ -0,0 +1,390 @@ +#! /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 -------------------------------------------------- diff --git a/eval.lisp b/eval.lisp new file mode 100644 index 0000000..24cd107 --- /dev/null +++ b/eval.lisp @@ -0,0 +1,59 @@ +;;; -*-lisp-*- +;;; +;;; Evaluate expressions and run scripts +;;; +;;; (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 . + +(cl:defpackage #:runlisp + (:use #:common-lisp)) +(cl:in-package #:runlisp) + +(setf *features* (remove :runlisp-script *features*)) + +(let ((*package* (find-package "COMMON-LISP-USER"))) + (let ((token (cons 'token nil)) + (args uiop:*command-line-arguments*) + (list nil)) + (flet ((foreach-form (func arg) + (with-input-from-string (in arg) + (loop (let ((form (read in nil token))) + (when (eq form token) (return)) + (funcall func form))))) + (print-form (form) + (format t "~@[~{~S~^ ~}~%~]" + (multiple-value-list (eval form))))) + (loop (let ((arg (pop args))) + (when (or (null arg) (string= arg "--")) (return)) + (when (zerop (length arg)) + (error "empty argument (no indicator)")) + (let ((rest (subseq arg 1))) + (ecase (char arg 0) + (#\! (push (lambda () + (foreach-form #'eval rest)) + list)) + (#\? (push (lambda () + (foreach-form #'print-form rest)) + list)) + (#\< (push (lambda () + (load rest)) + list))))))) + (let ((uiop:*command-line-arguments* args)) + (mapc #'funcall (nreverse list))))) diff --git a/m4/mdw-auto-version.m4 b/m4/mdw-auto-version.m4 new file mode 120000 index 0000000..db358e4 --- /dev/null +++ b/m4/mdw-auto-version.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-auto-version.m4 \ No newline at end of file diff --git a/m4/mdw-decl-environ.m4 b/m4/mdw-decl-environ.m4 new file mode 120000 index 0000000..c9190c8 --- /dev/null +++ b/m4/mdw-decl-environ.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-decl-environ.m4 \ No newline at end of file diff --git a/m4/mdw-define-paths.m4 b/m4/mdw-define-paths.m4 new file mode 120000 index 0000000..59213bf --- /dev/null +++ b/m4/mdw-define-paths.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-define-paths.m4 \ No newline at end of file diff --git a/m4/mdw-dir-texmf.m4 b/m4/mdw-dir-texmf.m4 new file mode 120000 index 0000000..3290ae1 --- /dev/null +++ b/m4/mdw-dir-texmf.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-dir-texmf.m4 \ No newline at end of file diff --git a/m4/mdw-libtool-version-info.m4 b/m4/mdw-libtool-version-info.m4 new file mode 120000 index 0000000..3298202 --- /dev/null +++ b/m4/mdw-libtool-version-info.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-libtool-version-info.m4 \ No newline at end of file diff --git a/m4/mdw-manext.m4 b/m4/mdw-manext.m4 new file mode 120000 index 0000000..56bc718 --- /dev/null +++ b/m4/mdw-manext.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-manext.m4 \ No newline at end of file diff --git a/m4/mdw-silent-rules.m4 b/m4/mdw-silent-rules.m4 new file mode 120000 index 0000000..52d11e3 --- /dev/null +++ b/m4/mdw-silent-rules.m4 @@ -0,0 +1 @@ +../.ext/cfd/m4/mdw-silent-rules.m4 \ No newline at end of file diff --git a/runlisp.1 b/runlisp.1 new file mode 100644 index 0000000..35143f4 --- /dev/null +++ b/runlisp.1 @@ -0,0 +1,751 @@ +.\" -*-nroff-*- +.\" +.\" Manual for `runlisp' +.\" +.\" (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 . +. +.ie t \{\ +. ds o \(bu +. if \n(.g \{\ +. fam P +. ev an-1 +. fam P +. ev +. \} +.\} +.el \{\ +. ds o o +.\} +. +.de hP +.IP +\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c +.. +. +.\"-------------------------------------------------------------------------- +.TH runlisp 1 "2 August 2020" "Mark Wooding" +.SH NAME +runlisp \- run Common Lisp programs as scripts +. +.\"-------------------------------------------------------------------------- +.SH SYNOPSIS +. +.B runlisp +.RB [ \-CDEnqv ] +.RB [ \-I +.IR imagedir ] +.RB [ \-L +.IB sys , sys , \fR...] +.RB [ \-P +.IB sys , sys , \fR...] +.br +\h'8n' +.RB [ \-e +.IR form ] +.RB [ \-l +.IR file ] +.RB [ \-p +.IR form ] +.RB [ \-\- ] +.RI [ script ] +.RI [ arguments +\&...] +. +.\"-------------------------------------------------------------------------- +.SH DESCRIPTION +. +The +.B runlisp +program has two main functions. +.hP 1. +It can be used in a script's +.RB ` #! ' +line to run a Common Lisp script. +.hP 2. +It can be used in build scripts +to invoke a Common Lisp system, +e.g., to build a standalone program. +. +.SS "Supported Common Lisp implementations" +The following Lisp implementations are currently supported. +.TP +.B "abcl" +Armed Bear Common Lisp. +.TP +.B "ccl" +Clozure Common Lisp. +.TP +.B "clisp" +GNU CLisp. +.TP +.B "cmucl" +Carnegie\(enMellon University Common Lisp. +.TP +.B "ecl" +Embeddable Common Lisp. +.TP +.B "sbcl" +Steel Bank Common Lisp. +.PP +The +.B runlisp +program expects, by default, +to be able to run a Lisp system +as a program with the same name, +found by searching as directed by the +.B PATH +environment variable. +This can be overridden by setting an environment variable, +with the same name but in +.IR "upper case" , +to the actual name \(en +either a bare filename to be searched for on the +.BR PATH , +or a pathname containing a +.RB ` / ', +relative to the working directory or absolute, +to the program. +Note that the entire variable value is used as the program name: +it's not possible to provide custom arguments to a Lisp system +using this mechanism. +If you want to do that, +you must write a shell script to do the necessary work, +and point the environment variable +(or the +.BR PATH ) +at your script. +. +.SS "Options" +Options are read from the command line, as usual, +but also from a number of other sources; +these are, in order: +.hP \*o +If a +.I script +is named, +and the script's second line contains a +.RB ` @RUNLISP: ' +marker, +then text following the marker is parsed as options. +.hP \*o +If files named +.B ~/.runlisprc +and/or +.B ~/.config/runlisprc +exist, +then their contents are parsed as options. +.hP \*o +If an environment variable +.B RUNLISP_OPTIONS +is defined, +then its contents is parsed as options. +.PP +A simple quoting and escaping system is implemented +to allow spaces and other special characters +to be included in argument words +in the script, configuration files, and environment variable. +The details of all of this are given in the section +.B Operation +below. +. +.PP +The options accepted are as follows. +. +.TP +.B "\-\-help" +Write a synopsis of +.BR runlisp 's +command-line syntax +and a description of the command-line options +to standard output +and immediately exit with status 0. +. +.TP +.B "\-\-version" +Write +.BR runlisp 's +version number +to standard output +and immediately exit with status 0. +. +.TP +.B "\-C" +Clear the list of preferred Lisp implementations. +. +.TP +.B "\-D" +Don't check for a custom Lisp image. +Usually, +.B runlisp +tries to start Lisp systems using a custom image, +so that they'll start more quickly; +the +.RB ` \-D ' +option forces the use of the default `vanilla' image +provided with the system. +There's not usually any good reason to prefer the vanilla image, +except for performance comparisons, or debugging +.B runlisp +itself. +. +.TP +.B "\-E" +Don't read embedded options from the +second line of the +.I script +file. +This has no effect in eval mode. +. +.TP +.BI "\-I " imagedir +Look in +.I imagedir +for custom Lisp images. +This option overrides the default image directory, +which is set at compile time. +. +.TP +.BI "\-L " sys , sys ,\fR... +Use one of the named Lisp systems. +Each +.I sys +must name a supported Lisp system. +This option may be given more than once: +the effect is the same as a single option +listing all of the systems named, in the same order. +If a system is named more than once, +a warning is issued (at verbosity level 1 or higher), +and all but the first occurrence is ignored. +. +.TP +.BI "\-P " sys , sys ,\fR... +Set the relative preference order of Lisp systems: +systems listed earlier are more preferred. +Each +.I sys +must name a supported Lisp system. +This option may be given more than once: +the effect is the same as a single option +listing all of the systems named, in the same order. +If a system is named more than once, +a warning is issued (at verbosity level 1 or higher), +and all but the first occurrence is ignored. +Unmentioned systems are assigned lowest preference: +if a +.RB ` \-L ' +option is given, +then this provides a default preference ordering; +otherwise, an ordering hardcoded into the program is used. +The first acceptable Lisp system, +according to the preference order just described, +which actually exists, +is the one selected. +. +.TP +.BI "\-e " expr +Evaluate the expression(s) +.I expr +and discard the resulting values. +This option causes +.B runlisp +to execute in +.I eval +mode. +. +.TP +.BI "\-l " file +Read and evaluate forms from the +.IR file . +This option causes +.B runlisp +to execute in +.I eval +mode. +. +.TP +.B "\-n" +Don't actually start the Lisp environment. +This may be helpful for the curious, +in conjunction with +.RB ` \-v ' +to increase the verbosity. +. +.TP +.BI "\-p " expr +Evaluate the expression(s) +.I expr +and print the resulting value(s) +to standard output +(as if by +.BR prin1 ). +If a form produces multiple values, +they are printed on a single line, +separated by a single space character; +if a form produces no values at all, +then nothing is printed \(en not even a newline character. +This option causes +.B runlisp +to execute in +.I eval +mode. +. +.TP +.B "\-q" +Don't print warning messages. +This option may be repeated: +each use reduces verbosity by one step, +counteracting one +.RB ` \-v ' +option. +The default verbosity level is 1, +which prints only warning measages. +. +.TP +.B "\-v" +Print informational or debugging messages. +This option may be repeated: +each use increases verbosity by one step, +counteracting one +.RB ` \-q ' +option. +The default verbosity level is 1, +which prints only warning measages. +Higher verbosity levels print informational and debugging messages. +. +.PP +The +.RB ` \-e ', +.RB ` \-l ', +and +.RB ` \-p ' +options may only be given on the command-line itself, +not following a +.RB `@ RUNLISP: ' +marker in a script, +in a configuration file, +or in the +.B RUNLISP_OPTIONS +environment variable. +These options may be given multiple times: +they will be processed in the order given. +If any of these options is given, then no +.I script +name will be parsed; +instead, use +.RB ` \-l ' +to load code from files. +The +.IR arguments , +if any, +are still made available to the evaluated forms and loaded files. +. +.SS "Operation" +The +.B runlisp +program behaves as follows. +.PP +The first thing it does is parse its command line. +Options must precede positional arguments, +though the boundary may be marked explicitly using +.RB ` \-\- ' +if desired. +If the command line contains any of +.RB ` \-e ', +.RB ` \-l ', +or +.RB ` \-p ', +then +.B runlisp +treats all of its positional arguments as +.I arguments +to provide to the given forms and files, +and runs in +.I eval +mode; +otherwise, the first positional argument becomes the +.I script +name, the remaining ones become +.IR arguments , +and +.B runlisp +runs in +.I script +mode. +.PP +In +.I script +mode, +.B runlisp +reads the second line of the script file, +and checks to see if it contains the string +.RB ` @RUNLISP: '. +If so, then the following text is parsed +for +.IR "embedded options" , +as follows. +The text is split into words +separated by sequences of whitespace characters. +Whitespace, +and other special characters, +can be included in a word by +.I quoting +or +.IR escaping . +Text between single quotes +.BR ' ... ' +is included literally, without any further interpretation; +text between double quotes +.BR """" ... """" +is treated literally, +except that escaping can still be used +to escape (e.g.) double quotes and the escape character itself. +Outside of single quotes, a backslash +.RB ` \e ' +causes the following character to be included in a word +regardless of its usual meaning. +(None of this allows a newline character +to be included in a word: +this is simply not possible.) +A word which is +.RB ` \-\- ' +before processing quoting and escaping +marks the end of embedded options. +As a concession to Emacs users, +if the sequence +.RB ` \-*\- ' +appears at the start of a word +before processing quoting and escaping, +then everything up to and including the next occurrence of +.RB ` \-*\- ' +is ignored. +The resulting list of words +is processed as if it held further command-line options. +However, +.B runlisp +is now committed to +.I script +mode, so +.RB ` \-e ', +.RB ` \-l ', +and +.RB ` \-p ' +options may not appear in a script file's embedded options list. +(This feature allows scripts to provide options even if they use +.BR env (1) +to find +.B runlisp +on the +.BR PATH , +or to provide more than one option, +since many operating systems pass the text following +the interpreter name on a +.RB ` #! ' +line as a single argument, without further splitting it at spaces.) +.PP +If a file named +.B .runlisprc +exists in the user's home directory, +then this file is read to discover more options. +(If the variable +.B HOME +is set in the environment, +then its value is assumed to name the user's home directory; +otherwise, the home directory is determined by looking up +the process's real uid in the password database.) +Lines consisting entirely of whitespace, +and lines whose first whitespace character is either +.RB ` # ' +or +.RB ` ; ' +are ignored in this file. +Other lines are split into words, +and processed as additional command-line options, +as described for embedded options above, +except that: +a +.RB ` \-\- ' +marker does not terminate word splitting; and +Emacs-style +.RB ` \-*\- ... \-*\- ' +local variable lists are not ignored. +Each line is processed separately, +so an option and its argument must be written on the same line. +By this point +.B runlisp +will have committed to +.I script +or +.I eval +mode, +so +.RB ` \-e ', +.RB ` \-l ', +and +.RB ` \-p ' +options may not appear in a configuration file. +.PP +If a file +.B runlisprc +exists in the user's +.I "configuration home" +directory, +then it is processed as for +.B .runlisprc +above. +If a variable +.B XDG_CONFIG_HOME +is set in the environment, +then its value is assumed to name the configuration home; +otherwise, the configuration home is the directory +.B .config +in the user's home directory, as determined above. +.PP +If the variable +.B RUNLISP_OPTIONS +is set in the environment, +then its value is split into words +and processed as additional command-line options, +as for a line of a configuration file as described above. +.PP +The list of +.I "acceptable Lisp implementations" +is determined. +If any +.RB ` \-L ' +options have been issued, +then the list of acceptable implementations +consists of all of the implementations mentioned in +.RB ` -L ' +options +in any of the places +.B runlisp +looked for options, +in the order of their first occurrence. +(If an implementation is named more than once, +then +.B runlisp +prints a warning to stderr +and ignores all but the first occurrence.) +If no +.RB ` \-L ' +option is given, then +.B runlisp +uses a default list, +which consists of all of the supported Lisp implementations +in an hardcoded order which reflects +the author's arbitrary preferences. +.PP +The list of +.I "preferred Lisp implementations" +is determined. +If any +.RB ` \-P ' +options have been issued +.I "since the last" +.IB ` \-C ' +.IR "option" , +then the list of preferred implementations +consists of all of the implementations mentioned in +.RB ` \-P ' +options after the last occurrence of +.RB ` \-C ', +in the order of their first occurrences. +(If an implementation is named more than once, +then +.B runlisp +prints a warning to stderr +and ignores all but the first occurrence.) +If no +.RB ` \-P ' +option is given, +or a +.RB ` \-C ' +option appears after all of the +.RB ` \-P ' +options, +then the list of preferred implementations is empty. +.PP +Acceptable Lisp implementations are tried in turn. +First, the preferred implementations +which are also listed as acceptable implementations +are tried, in the order in which they appear +in the preferred implementations list; +then, the remaining acceptable implementations are tried +in the order in which they appear +in the acceptable implementations list. +To +.I try +a Lisp implementation means to construct a command line +(whose effect will be described below) +and pass it to the +.BR execvp (3) +function. +If that succeeds, the Lisp implementation runs; +if it fails with +.B ENOENT +then other Lisp systems are tried; +if it fails with some other error, then +.B runlisp +reports an error message to stderr +and exits unsuccessfully +(with code 127). +If the +.RB ` \-n ' +option was given, then +.B runlisp +just simulates the behaviour of +.BR execvp (3), +printing messages to stderr +if the verbosity level is sufficiently high, +and exits. +.PP +In +.I script +mode, +the script is invoked. +In +.I eval +mode, +the instructions given in +.RB ` \-e ', +.RB ` \-l ', +and +.RB ` \-p ' +options are carried out, +in the order in which the appeared in the command line. +The details of the environment +in which Lisp code is executed +are described next. +. +.SS "Script environment" +Code in scripts and forms invoked by +.B runlisp +may assume the following facts about their environment. +.hP \*o +The keyword +.B :runlisp-script +is added to the +.B *features* +list if +.B runlisp +is running in +.I script +mode. +.hP \*o +Most Lisp systems support a user initialization file +which they load before entering the REPL; +some also have a system initialization file. +The +.B runlisp +program arranges +.I not +to read these files, +so that the Lisp environment is reasonably predictable, +and to avoid slowing down script startup +with things which are convenient for use in an interactive session, +but can't be relied upon by a script anyway. +.hP \*o +The Unix standard input, standard output, and standard error files +are available through the Lisp +.BR *standard-input* , +.BR *standard-output* , +and +.BR *error-output* +streams, respectively. +.hP \*o +Both +.B *compile-verbose* +and +.B *load-verbose* +are set to nil. +On CMU\ CL, +.B ext:*require-verbose* +is also nil. +Alas, this is insufficient to muffle noise while loading add-on systems +on some implementations. +.hP \*o +If an error is signalled, and not caught by user code, +then the process will print a message to stderr +and exit with a nonzero status. +The reported message may be a long, ugly backtrace, +or a terse error report. +If no error is signalled but not caught, +then the process will exit with status 0. +.hP \*o +The initial package is +.BR COMMON-LISP-USER , +which has no symbols `present' (i.e., imported or interned). +.hP \*o +The +.B asdf +and +.B uiop +systems are already loaded. +Further systems can be loaded using +.B asdf:load-system +as usual. +The script name +(which is only meaningful if +.B runlisp +is in +.I script +mode, obviously) +and arguments are available through the +.B uiop:argv0 +function and +.B uiop:*command-line-arguments* +variable, respectively. +. +.\"-------------------------------------------------------------------------- +. +.SH "BUGS" +.hP \*o +Loading ASDF systems is irritatingly noisy +with some Lisp implementations. +Suggestions for how to improve this are welcome. +.hP \*o +More Lisp implementations should be supported. +I've supported the ones I have installed. +I'm not willing to put a great deal of effort into supporting +non-free Lisp implementations; +but help supporting free Lisps is much appreciated. +.hP \*o +The protocol for passing the script name through to +.B uiop +(specifically, through the +.B __CL_ARGV0 +environment variable) +is terribly fragile, +but supporting +.B uiop +is obviously a better approach than introducing a +.BR runlisp -specific +interface to the same information. +I don't know how to fix this: +suggestions are welcome. +. +.SH "SEE ALSO" +.BR dump-runlisp-image (1). +. +.SH "AUTHOR" +Mark Wooding, +. +.\"----- That's all, folks -------------------------------------------------- diff --git a/runlisp.c b/runlisp.c new file mode 100644 index 0000000..300c8ed --- /dev/null +++ b/runlisp.c @@ -0,0 +1,1248 @@ +/* -*-c-*- + * + * Invoke a Lisp script + * + * (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 . + */ + +/*----- Header files ------------------------------------------------------*/ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +/*----- Common Lisp runes -------------------------------------------------*/ + +/* A common preamble rune to do the necessary things. + * + * We need to ensure that `asdf' (and therefore `uiop') is loaded. And we + * should arrange for `:runlisp-script' to find its way into the `*features*' + * list so that scripts can notice that they're being invoked from the + * command line rather than loaded into a resident session, and actually do + * something useful. + */ +#define COMMON_PRELUDE_RUNE \ + "(progn " \ + "(setf *load-verbose* nil *compile-verbose* nil) " \ + "(require \"asdf\") " \ + "(funcall (intern \"REGISTER-IMMUTABLE-SYSTEM\" " \ + "(find-package \"ASDF\")) " \ + "\"asdf\") " \ + "(set-dispatch-macro-character " \ + "#\\# #\\! " \ + "(lambda (#1=#:stream #2=#:char #3=#:arg) " \ + "(declare (ignore #2# #3#)) " \ + "(values (read-line #1#)))) " \ + "(pushnew :runlisp-script *features*))" + +/* Get `uiop' to re-check the command-line arguments following an image + * restore. + */ +#define IMAGE_RESTORE_RUNE \ + "(uiop:call-image-restore-hook)" + +/* Some Lisps leave crud in the `COMMON-LISP-USER' package. Clear it out. */ +#define CLEAR_CL_USER_RUNE \ + "(let ((#4=#:pkg (find-package \"COMMON-LISP-USER\"))) " \ + "(with-package-iterator (#5=#:next #4# :internal) " \ + "(loop (multiple-value-bind (#6=#:anyp #7=#:sym #8=#:how) " \ + "(#5#) " \ + "(declare (ignore #8#)) " \ + "(unless #6# (return)) " \ + "(unintern #7# #4#)))))" + +/*----- Handy macros ------------------------------------------------------*/ + +#define N(v) (sizeof(v)/sizeof((v)[0])) + +#if defined(__GNUC__) +# define GCC_VERSION_P(maj, min) \ + (__GNUC__ > (maj) || (__GNUC__ == (maj) && __GNUC_MINOR__ >= (min))) +#else +# define GCC_VERSION_P(maj, min) 0 +#endif + +#ifdef __clang__ +# define CLANG_VERSION_P(maj, min) \ + (__clang_major__ > (maj) || (__clang_major__ == (maj) && \ + __clang_minor__ >= (min))) +#else +# define CLANG_VERSION_P(maj, min) 0 +#endif + +#if GCC_VERSION_P(2, 5) || CLANG_VERSION_P(3, 3) +# define NORETURN __attribute__((__noreturn__)) +# define PRINTF_LIKE(fix, aix) __attribute__((__format__(printf, fix, aix))) +#endif + +#if GCC_VERSION_P(4, 0) || CLANG_VERSION_P(3, 3) +# define EXECL_LIKE(ntrail) __attribute__((__sentinel__(ntrail))) +#endif + +#define CTYPE_HACK(func, ch) (func((unsigned char)(ch))) +#define ISSPACE(ch) CTYPE_HACK(isspace, ch) + +#define MEMCMP(x, op, y, n) (memcmp((x), (y), (n)) op 0) +#define STRCMP(x, op, y) (strcmp((x), (y)) op 0) +#define STRNCMP(x, op, y, n) (strncmp((x), (y), (n)) op 0) + +#define END ((const char *)0) + +/*----- The Lisp implementation table -------------------------------------*/ + +/* The systems, in decreasing order of (not quite my personal) preference. + * This list is used to initialize various tables and constants. + */ +#define LISP_SYSTEMS(_) \ + _(sbcl) \ + _(ccl) \ + _(clisp) \ + _(ecl) \ + _(cmucl) \ + _(abcl) + +enum { +#define DEFSYS(sys) sys##_INDEX, + LISP_SYSTEMS(DEFSYS) +#undef DEFSYS + NSYS +}; + +enum { +#define DEFFLAG(sys) sys##_FLAG = 1 << sys##_INDEX, + LISP_SYSTEMS(DEFFLAG) +#undef DEFFLAG + ALL_SYSTEMS = 0 +#define SETFLAG(sys) | sys##_FLAG + LISP_SYSTEMS(SETFLAG) +#undef SETFLAG +}; + +struct argstate; +struct argv; + +#define DECLENTRY(sys) \ +static void run_##sys(struct argstate *, const char *); + LISP_SYSTEMS(DECLENTRY) +#undef DECLENTRY + +static const struct systab { + const char *name; + unsigned f; + void (*run)(struct argstate *, const char *); +} systab[] = { +#define SYSENTRY(sys) { #sys, sys##_FLAG, run_##sys }, + LISP_SYSTEMS(SYSENTRY) +#undef SYSENTRY +}; + +/*----- Diagnostic utilities ----------------------------------------------*/ + +static const char *progname = "runlisp"; + +static void set_progname(const char *prog) +{ + const char *p; + + p = strrchr(prog, '/'); + progname = p ? p + 1 : progname; +} + +static void vmoan(const char *msg, va_list ap) +{ + fprintf(stderr, "%s: ", progname); + vfprintf(stderr, msg, ap); + fputc('\n', stderr); +} + +static PRINTF_LIKE(1, 2) void moan(const char *msg, ...) + { va_list ap; va_start(ap, msg); vmoan(msg, ap); va_end(ap); } + +static NORETURN PRINTF_LIKE(1, 2) void lose(const char *msg, ...) + { va_list ap; va_start(ap, msg); vmoan(msg, ap); va_end(ap); exit(127); } + +/*----- Memory allocation -------------------------------------------------*/ + +static void *xmalloc(size_t n) +{ + void *p; + + if (!n) return (0); + p = malloc(n); if (!p) lose("failed to allocate memory"); + return (p); +} + +static void *xrealloc(void *p, size_t n) +{ + if (!n) { free(p); return (0); } + else if (!p) return (xmalloc(n)); + p = realloc(p, n); if (!p) lose("failed to allocate memory"); + return (p); +} + +static char *xstrdup(const char *p) +{ + size_t n = strlen(p) + 1; + char *q = xmalloc(n); + + memcpy(q, p, n); + return (q); +} + +/*----- Dynamic strings ---------------------------------------------------*/ + +struct dstr { + char *p; + size_t len, sz; +}; +#define DSTR_INIT { 0, 0, 0 } + +/* +static void dstr_init(struct dstr *d) { d->p = 0; d->len = d->sz = 0; } +*/ + +static void dstr_reset(struct dstr *d) { d->len = 0; } + +static void dstr_ensure(struct dstr *d, size_t n) +{ + size_t need = d->len + n, newsz; + + if (need <= d->sz) return; + newsz = d->sz ? 2*d->sz : 16; + while (newsz < need) newsz *= 2; + d->p = xrealloc(d->p, newsz); d->sz = newsz; +} + +static void dstr_release(struct dstr *d) { free(d->p); } + +static void dstr_putm(struct dstr *d, const void *p, size_t n) + { dstr_ensure(d, n); memcpy(d->p + d->len, p, n); d->len += n; } + +static void dstr_puts(struct dstr *d, const char *p) +{ + size_t n = strlen(p); + + dstr_ensure(d, n + 1); + memcpy(d->p + d->len, p, n + 1); + d->len += n; +} + +static void dstr_putc(struct dstr *d, int ch) + { dstr_ensure(d, 1); d->p[d->len++] = ch; } + +static void dstr_putz(struct dstr *d) + { dstr_ensure(d, 1); d->p[d->len] = 0; } + +static int dstr_readline(struct dstr *d, FILE *fp) +{ + size_t n; + int any = 0; + + for (;;) { + dstr_ensure(d, 2); + if (!fgets(d->p + d->len, d->sz - d->len, fp)) break; + n = strlen(d->p + d->len); assert(n > 0); any = 1; + d->len += n; + if (d->p[d->len - 1] == '\n') { d->p[--d->len] = 0; break; } + } + + if (!any) return (-1); + else return (0); +} +/*----- Dynamic vectors of strings ----------------------------------------*/ + +struct argv { + const char **v; + size_t o, n, sz; +}; +#define ARGV_INIT { 0, 0, 0, 0 } + +/* +static void argv_init(struct argv *av) + { av->v = 0; av->o = av->n = av->sz = 0; } +*/ + +/* +static void argv_reset(struct argv *av) { av->o = av->n = 0; } +*/ + +static void argv_ensure(struct argv *av, size_t n) +{ + size_t need = av->n + av->o + n, newsz; + + if (need <= av->sz) return; + newsz = av->sz ? 2*av->sz : 8; + while (newsz < need) newsz *= 2; + av->v = xrealloc(av->v, newsz*sizeof(const char *)); av->sz = newsz; +} + +static void argv_ensure_offset(struct argv *av, size_t n) +{ + size_t newoff; + + /* Stupid version. We won't, in practice, be prepending lots of stuff, so + * avoid the extra bookkeeping involved in trying to make a double-ended + * extendable array asymptotically efficient. + */ + if (av->o >= n) return; + newoff = 16; + while (newoff < n) newoff *= 2; + argv_ensure(av, newoff - av->o); + memmove(av->v + newoff, av->v + av->o, av->n*sizeof(const char *)); + av->o = newoff; +} + +static void argv_release(struct argv *av) { free(av->v); } + +static void argv_append(struct argv *av, const char *p) + { argv_ensure(av, 1); av->v[av->n++ + av->o] = p; } + +static void argv_appendz(struct argv *av) + { argv_ensure(av, 1); av->v[av->n + av->o] = 0; } + +static void argv_appendn(struct argv *av, const char *const *v, size_t n) +{ + argv_ensure(av, n); + memcpy(av->v + av->n + av->o, v, n*sizeof(const char *)); + av->n += n; +} + +/* +static void argv_appendav(struct argv *av, const struct argv *bv) + { argv_appendn(av, bv->v + bv->o, bv->n); } +*/ + +/* +static void argv_appendv(struct argv *av, va_list ap) +{ + const char *p; + + for (;;) + { p = va_arg(ap, const char *); if (!p) break; argv_append(av, p); } +} +*/ + +/* +static EXECL_LIKE(0) void argv_appendl(struct argv *av, ...) + { va_list ap; va_start(ap, av); argv_appendv(av, ap); va_end(ap); } +*/ + +static void argv_prepend(struct argv *av, const char *p) + { argv_ensure_offset(av, 1); av->v[--av->o] = p; av->n++; } + +/* +static void argv_prependn(struct argv *av, const char *const *v, size_t n) +{ + argv_ensure_offset(av, 1); + av->o -= n; av->n += n; + memcpy(av->v + av->o, v, n*sizeof(const char *)); +} +*/ + +/* +static void argv_prependav(struct argv *av, const struct argv *bv) + { argv_prependn(av, bv->v + bv->o, bv->n); } +*/ + +static void argv_prependv(struct argv *av, va_list ap) +{ + const char *p, **v; + size_t n = 0; + + for (;;) { + p = va_arg(ap, const char *); if (!p) break; + argv_prepend(av, p); n++; + } + v = av->v + av->o; + while (n >= 2) { + p = v[0]; v[0] = v[n - 1]; v[n - 1] = p; + v++; n -= 2; + } +} + +static EXECL_LIKE(0) void argv_prependl(struct argv *av, ...) + { va_list ap; va_start(ap, av); argv_prependv(av, ap); va_end(ap); } + +/*----- Lisp system table (redux) -----------------------------------------*/ + +static const struct systab *find_system(const char *name) +{ + const struct systab *sys; + size_t i; + + for (i = 0; i < NSYS; i++) { + sys = &systab[i]; + if (STRCMP(name, ==, sys->name)) return (sys); + } + lose("unknown Lisp system `%s'", name); +} + +static void lisp_quote_string(struct dstr *d, const char *p) +{ + size_t n; + + for (;;) { + n = strcspn(p, "\"\\"); + if (n) { dstr_putm(d, p, n); p += n; } + if (!*p) break; + dstr_putc(d, '\\'); dstr_putc(d, *p++); + } + dstr_putz(d); +} + +static const char *expand_rune(struct dstr *d, const char *rune, ...) +{ + const struct argv *av; + va_list ap; + size_t i, n; + + va_start(ap, rune); + for (;;) { + n = strcspn(rune, "%"); + if (n) { dstr_putm(d, rune, n); rune += n; } + if (!*rune) break; + switch (*++rune) { + case '%': dstr_putc(d, '%'); break; + case 'e': lisp_quote_string(d, va_arg(ap, const char *)); break; + case 'E': + av = va_arg(ap, const struct argv *); + for (i = 0; i < av->n; i++) { + if (i) dstr_putc(d, ' '); + dstr_putc(d, '"'); + lisp_quote_string(d, av->v[i]); + dstr_putc(d, '"'); + } + break; + default: lose("*** BUG unknown expansion `%%%c'", *rune); + } + rune++; + } + dstr_putz(d); + return (d->p); +} + +/*----- Argument processing -----------------------------------------------*/ + +struct syslist { + const struct systab *sys[NSYS]; + size_t n; + unsigned f; +}; +#define SYSLIST_INIT { { 0 }, 0, 0 } + +struct argstate { + unsigned f; +#define F_BOGUS 1u +#define F_NOEMBED 2u +#define F_NOACT 4u +#define F_NODUMP 8u +#define F_AUX 16u + int verbose; + char *imagedir; + struct syslist allow, pref; + struct argv av; +}; +#define ARGSTATE_INIT { 0, 1, 0, SYSLIST_INIT, SYSLIST_INIT, ARGV_INIT } + +/*----- Running programs --------------------------------------------------*/ + +#define FEF_EXEC 1u +static int file_exists_p(const struct argstate *arg, const char *path, + unsigned f) +{ + struct stat st; + + if (stat(path, &st)) { + if (arg && arg->verbose > 2) moan("file `%s' not found", path); + return (0); + } else if (!(S_ISREG(st.st_mode))) { + if (arg && arg->verbose > 2) moan("`%s' is not a regular file", path); + return (0); + } else if ((f&FEF_EXEC) && access(path, X_OK)) { + if (arg && arg->verbose > 2) moan("file `%s' is not executable", path); + return (0); + } else { + if (arg && arg->verbose > 2) moan("found file `%s'", path); + return (1); + } +} + +static int found_in_path_p(const struct argstate *arg, const char *prog) +{ + struct dstr p = DSTR_INIT, d = DSTR_INIT; + const char *path; + char *q; + size_t n, avail, proglen; + int i; + + if (strchr(prog, '/')) return (file_exists_p(arg, prog, 0)); + path = getenv("PATH"); + if (path) + dstr_puts(&p, path); + else { + dstr_puts(&p, ".:"); + i = 0; + again: + avail = p.sz - p.len; + n = confstr(_CS_PATH, p.p + p.len, avail); + if (avail > n) { i++; assert(i < 2); dstr_ensure(&p, n); goto again; } + } + + q = p.p; proglen = strlen(prog); + for (;;) { + n = strcspn(q, ":"); + dstr_reset(&d); + if (q[n]) dstr_putm(&d, q, n); + else dstr_putc(&d, '.'); + dstr_putc(&d, '/'); + dstr_putm(&d, prog, proglen); + dstr_putz(&d); + if (file_exists_p(arg, d.p, FEF_EXEC)) { + if (arg->verbose == 2) moan("found program `%s'", d.p); + return (1); + } + q += n; if (!*q) break; else q++; + } + return (0); +} + +static void try_exec(const struct argstate *arg, struct argv *av) +{ + struct dstr d = DSTR_INIT; + size_t i; + + assert(av->n); argv_appendz(av); + if (arg->verbose > 1) { + for (i = 0; i < av->n; i++) { + if (i) { dstr_putc(&d, ','); dstr_putc(&d, ' '); } + dstr_putc(&d, '"'); + lisp_quote_string(&d, av->v[av->o + i]); + dstr_putc(&d, '"'); + } + dstr_putz(&d); + moan("trying %s...", d.p); + } + if (arg->f&F_NOACT) + { if (found_in_path_p(arg, av->v[av->o])) exit(0); } + else { + execvp(av->v[av->o], (/*unconst*/ char **)av->v + av->o); + if (errno != ENOENT) + lose("failed to exec `%s': %s", av->v[av->o], strerror(errno)); + } + if (arg->verbose > 1) moan("`%s' not found", av->v[av->o]); + dstr_release(&d); +} + +static const char *getenv_or_default(const char *var, const char *dflt) + { const char *p = getenv(var); return (p ? p : dflt); } + +/*----- Invoking Lisp systems ---------------------------------------------*/ + +/* Steel Bank Common Lisp. */ + +static void run_sbcl(struct argstate *arg, const char *script) +{ + struct dstr d = DSTR_INIT; + + argv_prependl(&arg->av, "--script", script, END); + + dstr_puts(&d, arg->imagedir); + dstr_putc(&d, '/'); + dstr_puts(&d, "sbcl+asdf.core"); + if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0)) + argv_prependl(&arg->av, + "--core", d.p, + "--eval", IMAGE_RESTORE_RUNE, + END); + else + argv_prependl(&arg->av, "--eval", COMMON_PRELUDE_RUNE, END); + + argv_prependl(&arg->av, getenv_or_default("SBCL", "sbcl"), + "--noinform", + END); + try_exec(arg, &arg->av); + dstr_release(&d); +} + +/* Clozure Common Lisp. */ + +#define CCL_QUIT_RUNE \ + "(ccl:quit)" + +static void run_ccl(struct argstate *arg, const char *script) +{ + struct dstr d = DSTR_INIT; + + argv_prependl(&arg->av, "-b", "-n", "-Q", + "-l", script, + "-e", CCL_QUIT_RUNE, + "--", + END); + + dstr_puts(&d, arg->imagedir); + dstr_putc(&d, '/'); + dstr_puts(&d, "ccl+asdf.image"); + if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0)) + argv_prependl(&arg->av, "-I", d.p, "-e", IMAGE_RESTORE_RUNE, END); + else + argv_prependl(&arg->av, "-e", COMMON_PRELUDE_RUNE, END); + + argv_prepend(&arg->av, getenv_or_default("CCL", "ccl")); + try_exec(arg, &arg->av); + dstr_release(&d); +} + +/* GNU CLisp. + * + * CLisp causes much sadness. Superficially, it's the most sensible of all + * of the systems supported here: you just run `clisp SCRIPT -- ARGS ...' and + * it works. + * + * The problems come when you want to do some preparatory work (e.g., load + * `asdf') and then run the script. There's a `-x' option to evaluate some + * Lisp code, but it has three major deficiencies. + * + * * It insists on printing the values of the forms it evaluates. It + * prints a blank line even if the form goes out of its way to produce no + * values at all. So the whole thing has to be a single top-level form + * which quits the Lisp rather than returning. + * + * * For some idiotic reason, you can have /either/ `-x' forms /or/ a + * script, but not both. So we have to include the `load' here + * explicitly. I suppose that was inevitable because we have to inhibit + * printing of the result forms, but it's still a separate source of + * annoyance. + * + * * The icing on the cake: the `-x' forms are collectively concatenated -- + * without spaces! -- and used to build a string stream, which is then + * assigned over the top of `*standard-input*', making the original stdin + * somewhat fiddly to track down. + * + * There's an `-i' option which will load a file without any of this + * stupidity, but nothing analogous for immediate expressions. + */ + +#define CLISP_COMMON_STARTUP_RUNES \ + "(setf *standard-input* (ext:make-stream :input)) " \ + "(load \"%e\" :verbose nil :print nil) " \ + "(ext:quit)" + +#define CLISP_STARTUP_RUNE \ + "(progn " \ + COMMON_PRELUDE_RUNE " " \ + CLISP_COMMON_STARTUP_RUNES ")" + +#define CLISP_STARTUP_IMAGE_RUNE \ + "(progn " \ + IMAGE_RESTORE_RUNE " " \ + CLISP_COMMON_STARTUP_RUNES ")" + +static void run_clisp(struct argstate *arg, const char *script) +{ + struct dstr d = DSTR_INIT, dd = DSTR_INIT; + + dstr_puts(&d, arg->imagedir); + dstr_putc(&d, '/'); + dstr_puts(&d, "clisp+asdf.mem"); + if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0)) + argv_prependl(&arg->av, "-M", d.p, "-q", + "-x", expand_rune(&dd, CLISP_STARTUP_IMAGE_RUNE, script), + "--", + END); + else + argv_prependl(&arg->av, "-norc", "-q", + "-x", expand_rune(&dd, CLISP_STARTUP_RUNE, script), + "--", + END); + + argv_prepend(&arg->av, getenv_or_default("CLISP", "clisp")); + try_exec(arg, &arg->av); + dstr_release(&d); + dstr_release(&dd); + +#undef f +} + +/* Embeddable Common Lisp. * + * + * ECL is changing its command-line option syntax in version 16. I have no + * idea why they think the result can ever be worth the pain of a transition. + */ + +#if ECL_OPTIONS_GNU +# define ECLOPT "--" +#else +# define ECLOPT "-" +#endif + +#define ECL_STARTUP_RUNE \ + "(progn " \ + COMMON_PRELUDE_RUNE " " \ + CLEAR_CL_USER_RUNE ")" + +static void run_ecl(struct argstate *arg, const char *script) +{ + struct dstr d = DSTR_INIT; + + dstr_puts(&d, arg->imagedir); + dstr_putc(&d, '/'); + dstr_puts(&d, "ecl+asdf"); + if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, FEF_EXEC)) { + argv_prependl(&arg->av, "-s", script, "--", END); + argv_prependl(&arg->av, d.p, END); + } else { + argv_prependl(&arg->av, ECLOPT "shell", script, "--", END); + argv_prependl(&arg->av, getenv_or_default("ECL", "ecl"), ECLOPT "norc", + ECLOPT "eval", ECL_STARTUP_RUNE, + END); + } + try_exec(arg, &arg->av); +} + +/* Carnegie--Mellon University Common Lisp. */ + +#define CMUCL_STARTUP_RUNE \ + "(progn " \ + "(setf ext:*require-verbose* nil) " \ + COMMON_PRELUDE_RUNE ")" +#define CMUCL_QUIT_RUNE \ + "(ext:quit)" + +static void run_cmucl(struct argstate *arg, const char *script) +{ + struct dstr d = DSTR_INIT; + + argv_prependl(&arg->av, + "-load", script, + "-eval", CMUCL_QUIT_RUNE, + "--", + END); + + dstr_puts(&d, arg->imagedir); + dstr_putc(&d, '/'); + dstr_puts(&d, "cmucl+asdf.core"); + if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0)) + argv_prependl(&arg->av, "-core", d.p, "-eval", IMAGE_RESTORE_RUNE, END); + else + argv_prependl(&arg->av, "-batch", "-noinit", "-nositeinit", "-quiet", + "-eval", CMUCL_STARTUP_RUNE, + END); + + argv_prepend(&arg->av, getenv_or_default("CMUCL", "cmucl")); + try_exec(arg, &arg->av); + dstr_release(&d); +} + +/* Armed Bear Common Lisp. * + * + * CLisp made a worthy effort, but ABCL still manages to take the price. + * + * * ABCL manages to avoid touching the `stderr' stream at all, ever. Its + * startup machinery finds `stdout' (as `java.lang.System.out'), wraps it + * up in a Lisp stream, and uses the result as `*standard-output*' and + * `*error-output*' (and a goodly number of other things too). So we + * must manufacture a working `stderr' the hard way. + * + * * There doesn't appear to be any easy way to prevent toplevel errors + * from invoking the interactive debugger. For extra fun, the debugger + * reads from `stdin' by default, so an input file which somehow manages + * to break the script can then take over its brain by providing Lisp + * forms for the debugger to evaluate. + */ + +#define ABCL_STARTUP_RUNE \ + "(let ((#9=#:script \"%e\")) " \ + COMMON_PRELUDE_RUNE " " \ + CLEAR_CL_USER_RUNE " " \ + \ + /* Replace the broken `*error-output*' stream with a working \ + * copy of `stderr'. \ + */ \ + "(setf *error-output* " \ + "(java:jnew \"org.armedbear.lisp.Stream\" " \ + "'sys::system-stream " \ + "(java:jfield \"java.lang.System\" \"err\") " \ + "'character " \ + "java:+true+)) " \ + \ + /* Trap errors signalled by the script and arrange for them \ + * to actually kill the process rather than ending up in the \ + * interactive debugger. \ + */ \ + "(handler-case (load #9# :verbose nil :print nil) " \ + "(error (error) " \ + "(format *error-output* \"~A (unhandled error): ~A~%%\" " \ + "#9# error) " \ + "(ext:quit :status 255))))" + +static void run_abcl(struct argstate *arg, const char *script) +{ + struct dstr d = DSTR_INIT; + + argv_prependl(&arg->av, getenv_or_default("ABCL", "abcl"), + "--batch", "--noinform", "--noinit", "--nosystem", + "--eval", expand_rune(&d, ABCL_STARTUP_RUNE, script), + "--", + END); + try_exec(arg, &arg->av); + dstr_release(&d); +} + +/*----- Main code ---------------------------------------------------------*/ + +static void version(FILE *fp) + { fprintf(fp, "%s, version %s\n", progname, PACKAGE_VERSION); } + +static void usage(FILE *fp) +{ + fprintf(fp, "usage: %s [-CDEnqv] [-I IMAGEDIR] " + "[-L SYS,SYS,...] [-P SYS,SYS,...]\n" + "\t[--] SCRIPT [ARGUMENTS ...] |\n" + "\t[-e EXPR] [-p EXPR] [-l FILE] [--] [ARGUMENTS ...]\n", + progname); +} + +static void help(FILE *fp) +{ + version(fp); fputc('\n', fp); usage(fp); + fputs("\n\ +Options:\n\ + --help Show this help text and exit successfully.\n\ + --version Show the version number and exit successfully.\n\ + -C Clear the list of preferred Lisp systems.\n\ + -D Run system Lisp images, rather than custom images.\n\ + -E Don't read embedded options from the script.\n\ + -I IMAGEDIR Look for custom images in IMAGEDIR rather than\n\ + `" IMAGEDIR "'.\n\ + -L SYS,SYS,... Only use the listed Lisp systems.the script.\n\ + -P SYS,SYS,... Prefer the listed Lisp systems.\n\ + -e EXPR Evaluate EXPR (can be repeated).\n\ + -l FILE Load FILE (can be repeated).\n\ + -n Don't actually run the script (useful with `-v')\n\ + -p EXPR Print (`prin1') EXPR (can be repeated).\n\ + -q Don't print warning messages.\n\ + -v Print informational messages (repeat for even more).\n", + fp); +} + +/* Parse a comma-separated list of system names SPEC, and add the named + * systems to LIST. + */ +static void parse_syslist(const char *spec, const struct argstate *arg, + struct syslist *list, const char *what) +{ + char *copy = xstrdup(spec), *p = copy, *q; + const struct systab *sys; + size_t n; + + for (;;) { + n = strcspn(p, ","); + if (p[n]) q = p + n + 1; + else q = 0; + p[n] = 0; sys = find_system(p); + if (list->f&sys->f) { + if (arg->verbose > 0) + moan("ignoring duplicate system `%s' in %s list", p, what); + } else { + list->sys[list->n++] = sys; + list->f |= sys->f; + } + if (!q) break; + p = q; + } + free(copy); +} + +static void push_eval_op(struct argstate *arg, char op, const char *val) +{ + char *p; + size_t n; + + if (arg->f&F_AUX) { + moan("must use `-e', `-p', or `-l' on command line"); + arg->f |= F_BOGUS; + return; + } + + n = strlen(val) + 1; + p = xmalloc(n + 1); + p[0] = op; memcpy(p + 1, val, n); + argv_append(&arg->av, p); +} + +/* Parse a vector ARGS of command-line arguments. Update ARG with the + * results. NARG is the number of arguments, and *I_INOUT is the current + * index into the vector, to be updated on exit to identify the first + * non-option argument (or the end of the vector). + */ +static void parse_arguments(struct argstate *arg, const char *const *args, + size_t nargs, size_t *i_inout) +{ + const char *o, *a; + char opt; + + for (;;) { + if (*i_inout >= nargs) break; + o = args[*i_inout]; + if (STRCMP(o, ==, "--help")) { help(stdout); exit(0); } + else if (STRCMP(o, ==, "--version")) { version(stdout); exit(0); } + if (!*o || *o != '-' || !o[1]) break; + (*i_inout)++; + if (STRCMP(o, ==, "--")) break; + o++; + while (o && *o) { + opt = *o++; + switch (opt) { + +#define GETARG do { \ + if (*o) \ + { a = o; o = 0; } \ + else { \ + if (*i_inout >= nargs) goto noarg; \ + a = args[(*i_inout)++]; \ + } \ +} while (0) + + case 'C': arg->pref.n = 0; arg->pref.f = 0; break; + case 'D': arg->f |= F_NODUMP; break; + case 'E': arg->f |= F_NOEMBED; break; + case 'e': GETARG; push_eval_op(arg, '!', a); break; + case 'p': GETARG; push_eval_op(arg, '?', a); break; + case 'l': GETARG; push_eval_op(arg, '<', a); break; + case 'n': arg->f |= F_NOACT; break; + case 'q': if (arg->verbose) arg->verbose--; break; + case 'v': arg->verbose++; break; + + case 'I': + free(arg->imagedir); + GETARG; arg->imagedir = xstrdup(a); + break; + + case 'L': + GETARG; + parse_syslist(a, arg, &arg->allow, "allowed"); + break; + + case 'P': + GETARG; + parse_syslist(a, arg, &arg->pref, "preferred"); + break; + + default: + moan("unknown option `%c'", opt); + arg->f |= F_BOGUS; + break; + +#undef GETARG + + } + } + } + goto end; + +noarg: + moan("missing argument for `-%c'", opt); + arg->f |= F_BOGUS; +end: + return; +} + +/* Parse a string P into words (destructively), and process them as + * command-line options, updating ARG. Non-option arguments are not + * permitted. If `SOSF_EMACS' is set in FLAGS, then ignore `-*- ... -*-' + * editor turds. If `SOSF_ENDOK' is set, then accept `--' and ignore + * whatever comes after; otherwise, reject all positional arguments. + */ +#define SOSF_EMACS 1u +#define SOSF_ENDOK 2u +static void scan_options_from_string(char *p, struct argstate *arg, + unsigned flags, + const char *what, const char *file) +{ + struct argv av = ARGV_INIT; + char *q; + size_t i; + int st = 0; + unsigned f = 0; +#define f_escape 1u + + for (;;) { + while (ISSPACE(*p)) p++; + if (!*p) break; + if ((flags&SOSF_EMACS) && p[0] == '-' && p[1] == '*' && p[2] == '-') { + p = strstr(p + 3, "-*-"); + if (!p) lose("unfinished local-variables list in %s `%s'", what, file); + p += 3; continue; + } + if ((flags&SOSF_ENDOK) && + p[0] == '-' && p[1] == '-' && (!p[2] || ISSPACE(p[2]))) + break; + argv_append(&av, p); q = p; + for (;;) { + if (!*p) break; + else if (f&f_escape) { *q++ = *p; f &= ~f_escape; } + else if (st && *p == st) st = 0; + else if (st != '\'' && *p == '\\') f |= f_escape; + else if (!st && (*p == '"' || *p == '\'')) st = *p; + else if (!st && ISSPACE(*p)) break; + else *q++ = *p; + p++; + } + if (*p) p++; + *q = 0; + if (f&f_escape) lose("unfinished escape in %s `%s'", what, file); + if (st) lose("unfinished `%c' string in %s `%s'", st, what, file); + } + + i = 0; parse_arguments(arg, av.v, av.n, &i); + if (i < av.n) + lose("positional argument `%s' in %s `%s'", av.v[i], what, file); + argv_release(&av); + +#undef f_escape +} + +/* Read SCRIPT, and check for a `@RUNLISP:' marker in the second line. If + * there is one, parse options from it, and update ARG. + */ +static void check_for_embedded_args(const char *script, struct argstate *arg) +{ + struct dstr d = DSTR_INIT; + char *p; + FILE *fp = 0; + + fp = fopen(script, "r"); + if (!fp) lose("can't read script `%s': %s", script, strerror(errno)); + + if (dstr_readline(&d, fp)) goto end; + dstr_reset(&d); if (dstr_readline(&d, fp)) goto end; + + p = strstr(d.p, "@RUNLISP:"); + if (p) + scan_options_from_string(p + 9, arg, SOSF_EMACS | SOSF_ENDOK, + "embedded options in script", script); + +end: + if (fp) { + if (ferror(fp)) + lose("error reading script `%s': %s", script, strerror(errno)); + fclose(fp); + } + dstr_release(&d); +} + +/* Read the file PATH (if it exists) and update ARG with the arguments parsed + * from it. Ignore blank lines and (Unix- or Lisp-style) comments. + */ +static void read_config_file(const char *path, struct argstate *arg) +{ + FILE *fp = 0; + struct dstr d = DSTR_INIT; + char *p; + + fp = fopen(path, "r"); + if (!fp) { + if (errno == ENOENT) { + if (arg->verbose > 2) + moan("ignoring nonexistent configuration file `%s'", path); + goto end; + } + lose("failed to open configuration file `%s': %s", + path, strerror(errno)); + } + if (arg->verbose > 1) + moan("reading configuration file `%s'", path); + for (;;) { + dstr_reset(&d); + if (dstr_readline(&d, fp)) break; + p = d.p; + while (ISSPACE(*p)) p++; + if (!*p || *p == ';' || *p == '#') continue; + scan_options_from_string(p, arg, 0, "configuration file `%s'", path); + } + if (arg->f&F_BOGUS) + lose("invalid options in configuration file `%s'", path); + +end: + if (fp) { + if (ferror(fp)) + lose("error reading configuration file `%s': %s", + path, strerror(errno)); + fclose(fp); + } + dstr_release(&d); +} + +int main(int argc, char *argv[]) +{ + struct dstr d = DSTR_INIT; + const char *script, *p; + const char *home; + struct passwd *pw; + char *t; + size_t i, n; + struct argstate arg = ARGSTATE_INIT; + + /* Scan the command line. This gets low priority, since it's probably + * from the script shebang. + */ + set_progname(argv[0]); i = 1; + parse_arguments(&arg, (const char *const *)argv, argc, &i); + arg.f |= F_AUX; + if ((i >= argc && !arg.av.n) || (arg.f&F_BOGUS)) + { usage(stderr); exit(255); } + + /* Prepare the argument vector. Keep track of the number of arguments + * here: we'll need to refer to this later. + */ + if (!arg.av.n) { + script = argv[i++]; + if (!(arg.f&F_NOEMBED)) check_for_embedded_args(script, &arg); + if (arg.f&F_BOGUS) + lose("invalid options in `%s' embedded option list", script); + } else { + script = getenv("RUNLISP_EVAL"); + if (!script) script = DATADIR "/eval.lisp"; + argv_append(&arg.av, "--"); + } + argv_appendn(&arg.av, (const char *const *)argv + i, argc - i); + n = arg.av.n; + + /* Find the user's home directory. (Believe them if they set something + * strange.) + */ + home = getenv("HOME"); + if (!home) { + pw = getpwuid(getuid()); + if (!pw) lose("can't find user in password database"); + home = pw->pw_dir; + } + + /* Check user configuration file `~/.runlisprc'. */ + dstr_reset(&d); + dstr_puts(&d, home); dstr_putc(&d, '/'); dstr_puts(&d, ".runlisprc"); + read_config_file(d.p, &arg); + + /* Check user configuration file `~/.config/runlisprc'. */ + dstr_reset(&d); + p = getenv("XDG_CONFIG_HOME"); + if (p) + dstr_puts(&d, p); + else + { dstr_puts(&d, home); dstr_putc(&d, '/'); dstr_puts(&d, ".config"); } + dstr_putc(&d, '/'); dstr_puts(&d, "runlisprc"); + read_config_file(d.p, &arg); + + /* Finally, check the environment variables. */ + p = getenv("RUNLISP_OPTIONS"); + if (p) { + t = xstrdup(p); + scan_options_from_string(t, &arg, 0, + "environment variable", "RUNLISP_OPTIONS"); + free(t); + } + if (arg.f&F_BOGUS) + lose("invalid options in environment variable `RUNLISP_OPTIONS'"); + if (!arg.imagedir) { + arg.imagedir = getenv("RUNLISP_IMAGEDIR"); + if (!arg.imagedir) arg.imagedir = IMAGEDIR; + } + + /* If no systems are listed as acceptable, try them all. */ + if (!arg.allow.n) { + if (arg.verbose > 1) + moan("no explicitly allowed implementations: allowing all"); + for (i = 0; i < NSYS; i++) arg.allow.sys[i] = &systab[i]; + arg.allow.n = NSYS; arg.allow.f = (1u << NSYS) - 1; + } + + /* Print what we're going to do. */ + if (arg.verbose > 2) { + dstr_reset(&d); p = ""; + for (i = 0; i < arg.allow.n; i++) + { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.allow.sys[i]->name); } + moan("permitted Lisps: %s", d.p); + + dstr_reset(&d); p = ""; + for (i = 0; i < arg.pref.n; i++) + { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.pref.sys[i]->name); } + moan("preferred Lisps: %s", d.p); + + dstr_reset(&d); p = ""; + for (i = 0; i < arg.pref.n; i++) + if (arg.pref.sys[i]->f&arg.allow.f) + { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.pref.sys[i]->name); } + for (i = 0; i < arg.allow.n; i++) + if (!(arg.allow.sys[i]->f&arg.pref.f)) + { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.allow.sys[i]->name); } + moan("overall preference order: %s", d.p); + } + + /* Inform `uiop' of the script name. + * + * As an aside, this is a terrible interface. It's too easy to forget to + * set it. (To illustrate this, `cl-launch -x' indeed forgets to set it.) + * If you're lucky, the script just thinks that its argument is `nil', in + * which case maybe it can use `*load-pathname*' as a fallback. If you're + * unlucky, your script was invoked (possibly indirectly) by another + * script, and now you've accidentally inherited the calling script's name. + * + * It would have been far better simply to repeat the script name as the + * first user argument, if nothing else had come readily to mind. + */ + if (setenv("__CL_ARGV0", script, 1)) + lose("failed to set script-name environment variable"); + + /* Work through the list of preferred Lisp systems, trying the ones which + * are allowed. + */ + for (i = 0; i < arg.pref.n; i++) + if (arg.pref.sys[i]->f&arg.allow.f) { + arg.av.o += arg.av.n - n; arg.av.n = n; + arg.pref.sys[i]->run(&arg, script); + } + + /* That didn't work. Try the remaining allowed systems, in the given + * order. + */ + for (i = 0; i < arg.allow.n; i++) + if (!(arg.allow.sys[i]->f&arg.pref.f)) { + arg.av.o += arg.av.n - n; arg.av.n = n; + arg.allow.sys[i]->run(&arg, script); + } + + /* No joy. Give up. */ + argv_release(&arg.av); + lose("no supported Lisp systems found"); +} + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/t/Makefile.am b/t/Makefile.am new file mode 100644 index 0000000..1c706d5 --- /dev/null +++ b/t/Makefile.am @@ -0,0 +1,31 @@ +### -*-makefile-*- +### +### Build script for tests +### +### (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 . + +include autotest.am +autotest_TESTS = +TEST_ARGS = -j8 + +autotest_TESTS += $(top_srcdir)/tests.at + +###----- That's all, folks -------------------------------------------------- diff --git a/t/atlocal.in b/t/atlocal.in new file mode 100644 index 0000000..638aace --- /dev/null +++ b/t/atlocal.in @@ -0,0 +1,36 @@ +### -*-sh-*- +### +### Configuration variables interesting to the test suite +### +### (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 . + +###-------------------------------------------------------------------------- +### Configuration snippets. + +## Lisp systems. +SBCL=@SBCL@ +CCL=@CCL@ +CLISP=@CLISP@ +ECL=@ECL@ +CMUCL=@CMUCL@ +ABCL=@ABCL@ + +###----- That's all, folks -------------------------------------------------- diff --git a/t/autotest.am b/t/autotest.am new file mode 120000 index 0000000..2309b1e --- /dev/null +++ b/t/autotest.am @@ -0,0 +1 @@ +../.ext/cfd/build/autotest.am \ No newline at end of file diff --git a/t/package.m4 b/t/package.m4 new file mode 100644 index 0000000..e43d152 --- /dev/null +++ b/t/package.m4 @@ -0,0 +1,6 @@ +### package information +m4_define([AT_PACKAGE_NAME], [runlisp]) +m4_define([AT_PACKAGE_TARNAME], [runlisp]) +m4_define([AT_PACKAGE_VERSION], [UNKNOWN]) +m4_define([AT_PACKAGE_STRING], [runlisp UNKNOWN]) +m4_define([AT_PACKAGE_BUGREPORT], [mdw@distorted.org.uk]) diff --git a/t/tests.m4 b/t/tests.m4 new file mode 100644 index 0000000..acb8e73 --- /dev/null +++ b/t/tests.m4 @@ -0,0 +1 @@ +TESTS([..], [tests.at]) diff --git a/t/testsuite.at b/t/testsuite.at new file mode 120000 index 0000000..78fa5b5 --- /dev/null +++ b/t/testsuite.at @@ -0,0 +1 @@ +../.ext/cfd/build/testsuite.at \ No newline at end of file diff --git a/tests.at b/tests.at new file mode 100644 index 0000000..3eee970 --- /dev/null +++ b/tests.at @@ -0,0 +1,348 @@ +### -*-autotest-*- +### +### Test script for `runlisp' +### +### (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 . + +m4_define([RUNLISP_PATH], [$abs_top_builddir/runlisp]) + +m4_define([_FOREACH], [dnl +m4_if([$#], [1], [_foreach_func($1)], + [_foreach_func($1)[]_FOREACH(m4_shift($@))])]) +m4_define([FOREACH], [dnl +m4_pushdef([_foreach_func], [$2])dnl +_FOREACH($1)[]dnl +m4_popdef([_foreach_func])]) + +m4_define([LISP_SYSTEMS], + [sbcl, sbcl/noimage, + ccl, ccl/noimage, + clisp, clisp/noimage, + ecl, ecl/noimage, + cmucl, cmucl/noimage, + abcl, abcl/noimage]) + +m4_define([PREPARE_LISP_TEST], +[lisp=$1 +LISP=$m4_translit(m4_bregexp([$1], [/.*$], []), [a-z], [A-Z]) +AT_SKIP_IF([test "x$LISP" = x]) +case $lisp in + */*) opt=${lisp#*/} lisp=${lisp%%/*} ;; + *) opt="" ;; +esac +case /$opt/ in + */noimage/*) RUNLISP_IMAGEDIR=./notexist ;; + *) RUNLISP_IMAGEDIR=$abs_top_builddir ;; +esac +export RUNLISP_IMAGEDIR]) + +m4_define([WHICH_LISP], +[(or #+sbcl "sbcl" #+ccl "ccl" #+clisp "clisp" + #+ecl "ecl" #+cmu "cmucl" #+abcl "abcl" + "unknown")]) + +m4_define([NL], [ +]) + +m4_define([SETUP_RUNLISP_IMAGEDIR], +[RUNLISP_IMAGEDIR=$abs_top_builddir; export RUNLISP_IMAGEDIR]) + +m4_define([SETUP_RUNLISP_EVAL], +[RUNLISP_EVAL=$abs_top_srcdir/eval.lisp; export RUNLISP_EVAL]) + +###-------------------------------------------------------------------------- +### A basic smoke test. + +## Check that the system basically works, by running a trivial test program. +## Also try to verify that we're not running user or site startup code, +## though this is hard to do in general. +FOREACH([LISP_SYSTEMS], +[AT_SETUP([$1 smoke]) +AT_KEYWORDS([script smoke $1]) +PREPARE_LISP_TEST([$1]) + +## Prepare a user-init file which will break the test if it's run by printing +## something unexpected. +mkdir HOME +case $lisp in + sbcl) initfile=.sbclrc ;; + ccl) initfile=.ccl-init.lisp ;; + clisp) initfile=.clisprc.lisp ;; + ecl) initfile=.eclrc ;; + cmucl) initfile=.cmucl-init.lisp ;; + abcl) initfile=.abclrc ;; +esac +cat >HOME/$initfile <test-script <stdin + +## Prepare the reference stdout and stderr. +cat >stdout.ref <stderr.ref <test <out >err; rc=$? +AT_FAIL_IF([test $rc = 0]) + +AT_CLEANUP]) + +###-------------------------------------------------------------------------- +### Check eval mode. + +### Eval mode is implemented centrally through a script, so we don't need to +### test it separately for each Lisp implementation. + +AT_SETUP([eval mode]) +AT_KEYWORDS([eval common]) +SETUP_RUNLISP_IMAGEDIR +SETUP_RUNLISP_EVAL + +## A very basic smoke test. +AT_CHECK([RUNLISP_PATH -e '(format t "Just another Lisp hacker!~%")'],, +[Just another Lisp hacker! +]) + +## The `:runlisp-script' keyword should /not/ be in `*features*'. +traceon +AT_CHECK([RUNLISP_PATH -p '(find :runlisp-script *features*)'],, [NIL +]) + +## Check a mixture of all the kinds of evaluation. We'll need a stunt script +## to make this work. Also check that the individual forms are read and +## evaluated one at a time, so that each one can affect the way the reader +## interprets the next. +cat >script.lisp </dev/null; then good=t; else good=nil; fi + case ${lisp0+t},${badlisp+t},$good in + ,*,t) lisp0=$lisp ;; + t,*,t) lisp1=$lisp win=t; break ;; + *,,nil) badlisp=$lisp ;; + esac +done +AT_CHECK([case $win in nil) exit 77 ;; esac]) +case ${badlisp+t} in t) ;; *) badlisp=$1 ;; esac +BADLISP=$(echo $badlisp | tr a-z A-Z) +eval $BADLISP=/notexist/definitely-wrong +export $BADLISP +echo Primary Lisp = $lisp0 +echo Secondary Lisp = $lisp1 +echo Bad Lisp = $badlisp + +## Check that our selection worked. +AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp0 -p 'WHICH_LISP'],, ["$lisp0"NL]) +AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp1 -p 'WHICH_LISP'],, ["$lisp1"NL]) +AT_CHECK([RUNLISP_PATH -L$badlisp -p 'WHICH_LISP'], [127],, +[runlisp: no supported Lisp systems found[]NL]) + +## Unset all of the user preference mechanisms. +unset RUNLISP_OPTIONS +here=$(pwd) +mkdir HOME config +HOME=$here/HOME XDG_CONFIG_HOME=$here/config; export HOME XDG_CONFIG_HOME + +## We generally take the first one listed that exists. +AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp0,$lisp1 -p 'WHICH_LISP'],, ["$lisp0"NL]) +AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp1,$lisp0 -p 'WHICH_LISP'],, ["$lisp1"NL]) +AT_CHECK_UNQUOTED([RUNLISP_PATH -L$badlisp,$lisp0,$lisp1 -p 'WHICH_LISP'],, + ["$lisp0"NL]) + +## Check parsing of embedded options. +for i in 0 1; do + j=$(( 1 - $i )); eval lisp=\$lisp$i olisp=\$lisp$j + cat >script$i <$conf <&2 "usage: $0 LISP SCRIPT [ARGS ...]"; exit 127 ;; +esac +lisp=$1 script=$2; shift 2 + +__CL_ARGV0=$script; export __CL_ARGV0 # this is stupid + +lispscript=$(printf "%s" "$script" | sed 's/[\"]/\\&/g') + +load_asdf_rune="\ +(let ((*load-verbose* nil) + #+cmu (ext:*require-verbose* nil)) + (require \"asdf\"))" + +ignore_shebang_rune="\ +(set-dispatch-macro-character + #\# #\! + (lambda (stream char arg) + (declare (ignore char arg)) + (values (read-line stream))))" + +clisp_startup_rune="\ +(progn + $ignore_shebang_rune + $load_asdf_rune + (setf *standard-input* (ext:make-stream :input)) + (load \"$lispscript\" :verbose nil :print nil) + (ext:quit))" + +abcl_startup_rune="\ +(let ((script \"$lispscript\")) + $load_asdf_rune + $ignore_shebang_rune + (setf *error-output* + (java:jnew \"org.armedbear.lisp.Stream\" + 'sys::system-stream + (java:jfield \"java.lang.System\" \"err\") + 'character + java:+true+)) + (handler-case (load script :verbose nil :print nil) + (error (error) + (format *error-output* \"~A (unhandled error): ~A~%\" script error) + (ext:quit :status 255))))" + +#set -x +case $lisp in + + sbcl) + exec sbcl --noinform --eval "$load_asdf_rune" --script "$script" "$@" + ;; + + ecl) + exec ecl --norc --eval "$load_asdf_rune" --shell "$script" -- "$@" + ;; + + clisp) + exec clisp -norc -q -x "$clisp_startup_rune" -- "$@" + ;; + + cmucl) + exec cmucl -batch -noinit -nositeinit -quiet \ + -eval "$load_asdf_rune" \ + -eval "$ignore_shebang_rune" \ + -load "$script" -eval "(ext:quit)" -- "$@" + ;; + + ccl) + exec ccl -b -n -Q \ + -e "$load_asdf_rune" \ + -e "$ignore_shebang_rune" \ + -l "$script" -e "(ccl:quit)" -- "$@" + ;; + + abcl) + exec abcl --batch --noinform --noinit --nosystem \ + --eval "$abcl_startup_rune" -- "$@" + ;; + + *) + echo >&2 "$0: unsupported Lisp \`$lisp'" + exit 127 + ;; +esac diff --git a/vars.am b/vars.am new file mode 100644 index 0000000..4c9e164 --- /dev/null +++ b/vars.am @@ -0,0 +1,71 @@ +### -*-makefile-*- +### +### Common build-system definitions +### +### (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 . + +###-------------------------------------------------------------------------- +### Initial values for common variables. + +EXTRA_DIST = +CLEANFILES = + +bin_PROGRAMS = +bin_SCRIPTS = +nodist_bin_SCRIPTS = + +man_MANS = + +noinst_PROGRAMS = +noinst_DATA = + +###-------------------------------------------------------------------------- +### Standard configuration substitutions. + +## Substitute tags in files. +confsubst = $(top_srcdir)/config/confsubst + +SUBSTITUTIONS = \ + prefix=$(prefix) exec_prefix=$(exec_prefix) \ + libdir=$(libdir) includedir=$(includedir) \ + bindir=$(bindir) sbindir=$(sbindir) \ + imagedir=$(imagedir) \ + PACKAGE=$(PACKAGE) VERSION=$(VERSION) \ + ECLOPT=$(ECLOPT) + +v_subst = $(v_subst_@AM_V@) +v_subst_ = $(v_subst_@AM_DEFAULT_V@) +v_subst_0 = @echo " SUBST $@"; +SUBST = $(v_subst)$(confsubst) + +###-------------------------------------------------------------------------- +### List of Lisp systems. + +LISPS = + +LISPS += sbcl +LISPS += ccl +LISPS += clisp +LISPS += ecl +LISPS += cmucl +LISPS += abcl + +###----- That's all, folks --------------------------------------------------