This repo contains a system which will be a collection of fairly miscellaneous Common Lisp tools, which I have written over the years in order to generally get stuff done. Currently only two are published here:
require-moduleprovides variants of
requirewhich will search for modules, as well as the mechanisms to control the search, and also a variant of
providewhich keeps records of the file which provided a module;
install-providersmakes use of the records of module providers kept by
require-modulein order to copy them to places they will be found.
I hope to add more tools as I disentangle them from the things they’re currently entangled with and modernise them where needed. All of the tools are intended to be portable CL except where documented.
The descriptions here are at best partial1.
These tools purport to be portable Common Lisp. If they’re not that’s either a bug in the tools or a bug in the CL implementation. In the former case I very definitely want to know, and I am also willing to add workarounds for the latter although I may be unable to test them on implementations I don’t use.
All of these tools have long and varied histories. However these histories are entangled with a lot of other code which is not public, so that history is not represented in the publication repo where you are probably reading this.
I planned to use semantic versioning, where the major version number only changes on incompatible changes. However the result of this would be that a complete new tool being added or a huge extension to an existing one would only be a minor version, which seems wrong. So what I’m doing is to modify this: a major version means either an incompatible change, a complete new tool, or a very major extension of an existing tool.
All of these tools make use of, and often work best with, domain-structured names: packages, modules, features and so on have names which start with a DNS domain name in reverse order and then may continue to divide further. In this case the prefix is
org.tfeb is the DNS component and
tools is the division within the DNS part.
This naming convention ensures uniqueness of things like package names. It does make typing package-qualified names somewhat irritating: the solution is not to do that but to construct packages which import or use the symbols you need. CL’s default package system is very well up to solving this problem, rumours to the contrary notwithstanding: conduit packages can make it a little simpler to express what you want to do, and UIOP’s
define-package can also do something similar, I think.
Nothing actually cares that names correspond to real DNS domains: some things do care that the namespace is hierarchically structured and big-endian.
Although CL now has competent tools for defining and distributing ‘systems’ of code, in the form of ASDF and particularly Quicklisp, not all bits of code are large enough to justify their use. In particular it’s in the nature of the incremental and exploratory programming style encouraged by CL that people2 tend to write a bunch of little tools and utilities, living in single source files, which help them get their work done and which they reuse over time. Some of these may be portable, some not, and many of them can be quite small: a tool I use very often is under 50 lines excluding noise, but including docstrings and comments, and I have had smaller ones.
Turning these small tools into ASDF systems3 either means combining them into some larger conglomeration of code such as this one, or doubling the number of files you have to deal with because of the system definition files for each small tool. Combining them into a larger conglomeration of code means everything takes longer and costs more; doubling the number of files means the same. Also, of course, the ancestor of
require-module predates the common availability of portable system definition tools by many years.
Well, CL has
*modules* and although these are deprecated, they’re not going away any time soon, or in fact ever. So a reasonable approach is to make all the small tools into modules by adding suitable
provide incantations, and then to use
require to load them. This still leaves the problem that
require needs to know where to find the module being required is in the filesystem, and I didn’t want to fill code with explicit pathnames, even though logical pathnames make things at least a little better.
require-module provides a souped-up variant of
require which has a portable mechanism for searching for and locating files corresponding to modules. In particular it understands how to combine domain-structured module names (
org.tfeb.hax.collecting say) with a search list of pathnames to locate a file corresponding to the name which, when loaded, should provide the module. There are mechanisms to define what directories should be searched and in which order, and to wrap code around the process of requiring a module in the manner of CLOS around methods. Finally it also provides a wrapper for
provide which keeps a record of the file which provided a given module, which can be used by
install-providers (below) to install modules where it expects them to be.
require-module can also take advantage of the fact that
require never actually checks that the file or files it loads provides the module being required. So
require-module will happily load any file it can find, and doesn’t care whether the module is provided or not.
require-module also keeps a cache of the truenames and write dates of the files it has loaded: unless you ask to bypass the cache, files will only ever be loaded once until they are modified, no matter how many times they are required.
require-module also keeps a structure which maps between modules it has loaded and all the descendant modules of them – modules which were loaded in the process of loading some other module – together with a note as to the file they came from. Using this structure you can ask it to reload a module if it has changed together with any descendant modules which have changed. This is currently experimental.
require-module doesn’t need you to use logical pathnames, but it does work well with them and I have usually used it that way until recently. Using logical pathnames gives you two advantages:
In summary you don’t have to use logical pathnames, but it can make things easier.
Module names are case sensitive:
string= to compare them. Depending on how you write module names this can make life a little complicated4. This is made more interesting because logical pathnames are really all upper-case.
require-module tries to be smart about this:
This typically works reasonably well: if you use module names which are symbols, are searching a physical pathname, are using a case-sensitive filesystem and prefer lowercase filenames, then it will find a lowercase filename on the second try.
The search process starts by splitting up a domain-structured module name into a list of strings (the set of characters which separate components can be controlled: see below), which it will then variously use as directory and name components for the search:
"org.tfeb.hax"is turned into
("org" "tfeb" "hax");
:org.tfeb.haxis turned into
("ORG" "TFEB" "HAX")assuming standard reader settings.
These lists, and parts of them, are then used as parts of pathname specifications after possible case fiddling.
There is a list of path specifications to search: see below for how the list is maintained and exactly what can be in it. A path specification should generally specify a wild or partly-wild lisp source file name. Each path specification is searched in turn, and finally a desperation search is done on all of them using only the last part of the module name. The search continues until either a hit is found or there’s nothing left to search.
Example: searching for
"CLEY:LIB;MODULES;*.LISP". This is a logical path specification, so only upper-case variants will be tried. Both the compiled file (via
compile-file-pathname) and the source file for the following pathnames will be tried5: if both exist then the compiled file will be taken if it is newer, otherwise the source file (with a warning):
"CLEY:LIB;MODULES;REQUIRE-MODULE;REQUIRE-MODULE.LISP"(desperation search, part 1);
"CLEY:LIB;MODULES;REQUIRE-MODULE.LISP"(desperation search, part 2).
Example: searching for
"/local/lisp/*-loader.lisp". This is a physical path specification so things are more complicated:
"/local/lisp/org/tfeb/hax/HAX-loader.lisp"(case as is);
"/local/lisp/org/tfeb/HAX-loader.lisp"(case as is);
In fact the search algorithm is cleverer than this in several ways:
"foo"say) then the desperation search using the last part of the name only is omitted as a whole, as it would all duplicate the previous search;
None of these tricks should alter which file is found for a module: they are all intended just to eliminate duplicate probes of the filesystem.
Everything below is exported from
:org.tfeb.tools.require-module will also be present in both
*features* once this module is loaded – the latter helps a lot with conditionals in init files.
require-module will search for and
require modules. It has one mandatory argument which is the module name, and a fairly large number of keyword arguments. Values for most of the keyword arguments are dynamically passed down6, so that recursive calls to
require-module will get the same values as the parent call: exceptions are noted below. The keywords and their default values are as follows.
verbosewill cause it to tell you what it’s doing (on
debugwill turn on some debugging output to
*debug-io*, and in particular will cause
locate-moduleto talk about evaded duplicate searches due to the search list. Default
quietwill cause some warnings not to happen (
verbosecan both be true). Default
testspecifies the comparison function to use for module names, by default
#'string=which is the right default.
pretendwill cause it not to actually require the module, default
forcewill cause it to forcibly require the module (by removing it from
*modules*as the first step), default
oncewill cause it to check the cache of loaded truenames and only load a file if it either has not been loaded previously or if its write date is newer than the cached date, default
cachewill update the cache of loaded truenames when a file is loaded, default
reloadwill cause it to reload any dependent modules if possible. Default is
nil& this is not inherited from the ambient value. Note that all dependent modules will be reloaded: not just direct children.
compilewill cause it to attempt to compile the module if it gets a source file name, default
usewill cause it to use a package with the same name as the module after it is loaded if it exists, default
nil. This is not inherited from the ambient value.
fallback, if non-
nil, should be a fallback function which will be be used to try to require a module if no location for it can be found, default
errormeans that failure to require a module is an error, default
module-path-descriptionsis the list of module path-descriptions, default
hintsis a list of pathname designators which are hints as to where the module lives: if given, these are searched first. Default is
(), & this is not inherited from the ambient value. This is used by reloading, but can also be used explicitly.
module-component-separatorsis a list of characters which separate module components. The default is the value of
*module-component-separators*(which, by default, is
module-component-rewriter, if non-
nil, is a designator for the function used to rewrite components (see below). The default is the value of
wrapper-argumentsis a list of keyword arguments passed to wrappers, default
Wrappers are not yet documented. Reloading is experimental.
tif the module was loaded;
nilif the module was already loaded (no search is done in this case);
tif it is given and no location was found;
nilif there is no fallback,
niland the module was not found.
require-module relies on
require to do the actual work of loading the file and most of the work of maintaining
*modules*. It will only search (and thus only call
require on the results of the search) if the module is not already present on
*modules*, either because it was not there before the call or because it’s just removed it due to the
It is not an error if a module doesn’t define a package with its own name when the
use option is given, but there will be a warning unless
quiet is also given.
locate-module locates a module: it’s what
require-module uses to find things. It has one mandatory argument which is the module name, and two keyword arguments.
module-path-descriptionsis the list of module path-descriptions, default
hintsis a list of possible pathname designators for the module; if given it is searched first. the default is
(). This is used by module reloading but can be used explicitly.
module-component-separatorsis a list of characters which separate module components. The default is the value of
*module-component-separators*(which, by default, is
module-component-rewriteris the module component rewriter, the default being the value of
verbosemakes it say what it’s doing, default
debugenables some debugging output.
locate-module will return 5 values:
nilif none was found;
nilif none was found.
If the selected pathname is
nil then all the other values will be
nil. Otherwise it will be the newest of the source and compiled files, or the only one of them found if both do not exist. In particular it will be
eq to exactly one of the other pathnames in this case. All pathnames returned will be truenames.
The reason for this behaviour is that the file located is often a tiny ‘loader’ shim which never gets compiled7, so in this case there’s nothing wrong if there is no compiled file. If there is a compiled file but it is out of date it’s best to return the source file: it’s current, and anything that calls
locate-module can choose to compile the file before loading, which is something
require-module can do.
locate-module does not either use or update any caches.
require-modules is a shim around
require-module which expects a list of module descriptions instead of a module name. It takes all the same keyword arguments as
require-module. A module description is either
In the second case
require-module is called with the two sets of arguments appended to each other, with those from the module specification first. Because CL uses the first of any repeated keyword arguments, this means that individual module specifications can override the keyword arguments provided to the function as a whole.
require-modules returns a list of lists of the two values returned by each
require-module it calls.
requires is a NOSPREAD version of
require-modules with a fallback to
(requires x y) is
(require-modules (list x y) :fallback #'require). So
requires lets you say that you want one or more modules, and if
require-module doesn’t know how to get them then the system should try
require in case it does.
needs lets you express a dependency on modules at compile time: it is
requires wrapped in a suitable
eval-when, but its arguments are quoted. Note that
needs quotes its arguments: an older version didn’t so this is an incompatible change. If you use strings or keywords as module names this doesn’t matter, but it makes things like
(needs (:org.tfeb.hax.collecting :use t)) more natural8.
clear-module-caches is a function of no arguments which will clear both the cache of loaded files & their write dates and the structure (not really a cache) which maintains the notion of the descendants of a module.
The list of path descriptions is
*module-path-descriptions*. Each entry in this list is a path description, which is one of:
nilwhich is ignored (this is useful so a function can return
nilas a way of declining to provide a useful value;
pathnameto turn into a pathname.
The initial value of
It is perfectly possible to maintain
*module-path-descriptions* manually, and that’s how it worked for a long time. There is now a macro which makes this a little easier, perhaps, and a utility which helps with entries which are functions.
define-module-path-descriptions defines module path descriptions. It does this for a particular host (this is one of the reasons logical pathnames are useful), and there are a bunch of options: the most simple ones control whether to add the descriptions before or after the existing ones, and whether to replace existing descriptions for the same host. Rather than describe it in detail here are a couple of examples (yes, this is copping out);
(define-module-path-descriptions ("QL" :after t) "QL:LOCAL-PROJECTS;*-LOADER.LISP" "QL:LOCAL-PROJECTS;LOADER.LISP" "QL:LOCAL-PROJECTS;*.LISP")
This will add a bunch of pathname descriptions for a logical host named
"QL and it will add them after any existing ones. It will replace any descriptions for the
"QL" logical host.
(define-module-path-descriptions "-" (lambda () (merge-pathnames "*-loader.lisp" (pathname (sb-posix:getcwd)))) (lambda () (merge-pathnames "loader.lisp" (pathname (sb-posix:getcwd)))) (lambda () (make-pathname "*.lisp" (pathname (sb-posix:getcwd)))))
This is what my SBCL init file looked like before
module-path-descriptions-for-function exists, where it was the first thing that adds to
"-" host doesn’t exist: it’s just there because the macro needs something to be there. Each form in the body is a function which will return a pathname based on the current directory, which means that subdirectories of the current directory get searched first.
There is a weirdness here which is worth noting:
define-module-path-descriptions doesn’t normally evaluate the forms in its body. But it does arrange for them to be evaluated if they are lists whose first element is
lambda, which is why the above works. This is a horrible hack.
define-module-path-descriptions uses an (exported) function called
add-module-path-descriptions to do most of its work. You’ll need to look at the source to what it does.
module-path-descriptions-for-function is a function which takes two arguments and returns a list of functions suitable for entries in
*module-path-descriptions*. Its arguments are:
For each pathname spec this will return a a functiony path description which calls the function, and if does not return
nil then merges either the result of a suitable
make-pathname call (for a listy pathname spec) or a call to
pathname (for any other pathname spec) with its result. If the function returns NIL just return NIL.
A good example of how to use this function is to provide searching depending on the current file being loaded or compiled:
(setf *module-path-descriptions* (append *module-path-descriptions* (module-path-descriptions-for-function (lambda () (or *compile-file-truename* *load-truename*)) '("*-loader.lisp" "loader.lisp" "*.lisp"))))
You can’t (easily) use this function with
define-module-path-descriptions: you need to use it as above. Note that the function will get called for each pathname spec.
As an interface to
install-providers, below, there is a function called
provide-module: it does exactly what
provide does (it relies on
provide to do the work) but also maintains an alist,
*module-providers* which maps from module names to the files that provided them (from
provides is a counterpart to
needs: it lets you state what module or modules a given file provides. Unlike
needs it’s not a macro because modules should not be provided until they are loaded.
after-require-module is a macro which can be used in a module, to arrange for the forms in its body to be run after the module has been provided. The forms are wrapped in a block also called
after-require-module. This can be used any number of times, and if the module is being loaded some other way it will simply do nothing. For example:
(defun interface (...) ...) ... (after-require-module (register-interface #'interface))
This is particularly useful for modules consisting of several files (with a ‘loader’ file to load them all): the forms given in
after-require-module are run after the whole module is loaded.
There is a mechanism for adding wrappers around the process of actually providing a module (after its file has been located). This is not yet documented here, but its main use has been to arrange to forget about system definitions for modules which involve some system definition tool, so the LispWorks development environment doesn’t get cluttered up with system definitions that are not interesting. It’s also used to implement
after-require-module, above. This mechanism is subject to change.
*module-component-separators* is the default list of characters which can separate module names. Its initial value is
(#\.), meaning that names are separated by
. characters. You could, for instance, set it to be
(#\. #\/) which would allow module named like
"org.tfeb.ts/test" to parse as
("org" "tfeb" "ts" "test").
*module-component-rewriter* is a variable which may either be
nil (the default) or a function designator, and which provides the default module component rewriter. If it is a function designator that function is called on each component of a dotted module name (for
"ORG.TFEB.FOSH" the components are
"FOSH") and the value it returns is used as the name of the component. This is useful for components which have names which are not valid pathname components: for instance it can be used to rewrite a name like
"series-conduit"(and this was its original purpose).
(defvar *my-mpds* '()) (define-module-path-descriptions ("TFB" :module-path-descriptions *my-mpds*) "TFB:LIB;MODULES;*.LISP" "TFB:LIB;MODULES;*-LOADER.LISP" "TFB:LIB;MODULES;LOADER.LISP")
> *my-mpds* ("TFB:LIB;MODULES;*.LISP" "TFB:LIB;MODULES;*-LOADER.LISP" "TFB:LIB;MODULES;LOADER.LISP") > (locate-module :org.tfeb.pretend :module-path-descriptions *my-mpds* :verbose t) Looking for module :org.tfeb.pretend Probing TFB:LIB;MODULES;ORG;TFEB;PRETEND;PRETEND.LISP as /Users/tfb/lib/lw/modules/org/tfeb/pretend/pretend.lisp from "TFB:LIB;MODULES;*.LISP" Probing TFB:LIB;MODULES;ORG;TFEB;PRETEND.LISP as /Users/tfb/lib/lw/modules/org/tfeb/pretend.lisp from "TFB:LIB;MODULES;*.LISP" Probing TFB:LIB;MODULES;ORG;TFEB;PRETEND;PRETEND-LOADER.LISP as /Users/tfb/lib/lw/modules/org/tfeb/pretend/pretend-loader.lisp from "TFB:LIB;MODULES;*-LOADER.LISP" Probing TFB:LIB;MODULES;ORG;TFEB;PRETEND-LOADER.LISP as /Users/tfb/lib/lw/modules/org/tfeb/pretend-loader.lisp from "TFB:LIB;MODULES;*-LOADER.LISP" Probing TFB:LIB;MODULES;ORG;TFEB;PRETEND;LOADER.LISP.NEWEST as /Users/tfb/lib/lw/modules/org/tfeb/pretend/loader.lisp from "TFB:LIB;MODULES;LOADER.LISP" Probing TFB:LIB;MODULES;PRETEND;PRETEND.LISP as /Users/tfb/lib/lw/modules/pretend/pretend.lisp from "TFB:LIB;MODULES;*.LISP" Probing TFB:LIB;MODULES;PRETEND.LISP as /Users/tfb/lib/lw/modules/pretend.lisp from "TFB:LIB;MODULES;*.LISP" Probing TFB:LIB;MODULES;PRETEND;PRETEND-LOADER.LISP as /Users/tfb/lib/lw/modules/pretend/pretend-loader.lisp from "TFB:LIB;MODULES;*-LOADER.LISP" Probing TFB:LIB;MODULES;PRETEND-LOADER.LISP as /Users/tfb/lib/lw/modules/pretend-loader.lisp from "TFB:LIB;MODULES;*-LOADER.LISP" Probing TFB:LIB;MODULES;PRETEND;LOADER.LISP.NEWEST as /Users/tfb/lib/lw/modules/pretend/loader.lisp from "TFB:LIB;MODULES;LOADER.LISP" nil
But instead if we had
(defvar *my-mpds* '()) (define-module-path-descriptions ("-" :module-path-descriptions *my-mpds* :after t) "/Local/packages/lispworks/lib/modules/*.lisp" "/Local/packages/lispworks/lib/modules/*-loader.lisp" "/Local/packages/lispworks/lib/modules/loader.lisp")
> *my-mpds* ("/Local/packages/lispworks/lib/modules/*.lisp" "/Local/packages/lispworks/lib/modules/*-loader.lisp" "/Local/packages/lispworks/lib/modules/loader.lisp") > (locate-module :org.tfeb.utilities.permutations :module-path-descriptions *my-mpds* :verbose t) Looking for module :org.tfeb.utilities.permutations Probing /Local/packages/lispworks/lib/modules/ORG/TFEB/UTILITIES/PERMUTATIONS/PERMUTATIONS.lisp from "/Local/packages/lispworks/lib/modules/*.lisp" Probing /Local/packages/lispworks/lib/modules/ORG/TFEB/UTILITIES/PERMUTATIONS.lisp from "/Local/packages/lispworks/lib/modules/*.lisp" Found /Local/packages/lispworks/lib/modules/ORG/TFEB/UTILITIES/PERMUTATIONS.64xfasl #P"/Local/packages/lispworks/lib/modules/ORG/TFEB/UTILITIES/PERMUTATIONS.64xfasl"
This second example is betraying the fact that I’m on a Mac: the mac’s filesystem is case-insensitive / case-preserving, so the file is being found with an uppercase filename, when its name is ‘really’ lowercase.
If you have Quicklisp, this works:
(needs (:org.tfeb.hax.collecting) (:cl-ppcre :fallback ql:quickload))
Which means you can write small programs which rely on Quicklisp to fetch and load things without either an ASDF system definition, or a bunch of explicit
eval-whens in the sources to load things at compile time. In fact it is pretty much possible to use
needs to informally define systems without a central system definition, even when those systems have dependencies on Quicklisp or other systems.
require-module does quite a lot of processing of pathnames. It is all intended to be portable but it also turns out to explore some of the boundaries of what implementations support. As an example, SBCL can’t currently deal with making partly-wild pathnames, so in SBCL you often need to provide stringy logical pathnames in configurations9.
When making module path descriptions based on the current file being compiled or loaded always use truenames and always prefer
*compile-file-truename* as otherwise you may get the name of some parent file which has asked to compile the file of interest.
All of the functions accept strings or symbols as module names: they’ll complain about anything else rather than blindly calling
needs has changed incompatibly so it now quotes its arguments.
locate-module used to return only what is now its first value: this is a compatible change, I think.
A consequence of the caching of truenames is that, so long as the graph of modules and their requirements is a DAG, each file will be loaded just once. That means it’s perfectly fine for any file in the DAG just to say
(needs ...) to express the files it relies on: if they’re already loaded, they won’t be loaded again unless they’ve changed. If the graph has cycles you’re in all sorts of trouble, of course.
I now use makefiles to install my personal CL modules and systems, and either ASDF or the LispWorks system definition tool to build them once installed10. Previously I used an ancestor of this code. It lives in the
org.tfeb.tools.install-providers package and will add
install-providers will install a set of modules from the files they were originally loaded from into a directory tree under a specified root. It has one argument which is the root under which to install things. The remaining keyword arguments are:
providersis an alist of
(module-name . providing-file)which tells it what to install, the default being
reallysays to really copy the files, the default is
nilin which case it will just tell you what it would do;
clearwill cause it to reset
()after installing, this is true if no explicit list is given and if it is actually installing, and it will refuse to clear the list in any case if an explicit list is given;
filteris a function which will be called with the module name and the source & destination paths, and which should return true if the module is to be installed.
The function returns an alist of
(source target) of the files copied.
> *module-providers* (("ORG.TFEB.TOOLS.INSTALL-PROVIDERS" . #P"/Users/tfb/src/lisp/systems/tools/install-providers.64xfasl") ("ORG.TFEB.LW.LW-COMMANDS" . #P"/Local/packages/lispworks/lib/modules/org/tfeb/lw/lw-commands.64xfasl") ("ORG.TFEB.TOOLS.REQUIRE-MODULE" . #P"/Users/tfb/src/lisp/systems/tools/require-module.64xfasl")) > (install-providers "/tmp/") Would ensure dirs for /tmp/org/tfeb/tools/install-providers.64xfasl copy /Users/tfb/src/lisp/systems/tools/install-providers.64xfasl to /tmp/org/tfeb/tools/install-providers.64xfasl Would ensure dirs for /tmp/org/tfeb/lw/lw-commands.64xfasl copy /Local/packages/lispworks/lib/modules/org/tfeb/lw/lw-commands.64xfasl to /tmp/org/tfeb/lw/lw-commands.64xfasl Would ensure dirs for /tmp/org/tfeb/tools/require-module.64xfasl copy /Users/tfb/src/lisp/systems/tools/require-module.64xfasl to /tmp/org/tfeb/tools/require-module.64xfasl nil
The TFEB.ORG tools are copyright 2002, 2012, 2020-2021 Tim Bradshaw. See
LICENSE for the license.
This README also has footnotes which GitHub does not support. You’ll just have to make sense of that. ↩
Well, I don’t know how other CL programmers work: I do know that I do this though. ↩
Or systems in one of the system definition facilities which existed before ASDF, of course. ↩
I write them as keyword symbols, so my modules always end up with upper-case names. ↩
Note that, here and below, I am writing pathnames as strings. This is just because I am being lazy: the system works in terms of pathnames, not strings. ↩
The combination of
&rest args &key ... and CL’s carefully-thought-out leftmost-first-duplicates-allowed keyword argument handling makes this delightfully simple. ↩
And, in fact, such loader shims often can’t be compiled as they rely on things changing during the load of their source. ↩
It’s hard to see a case where having
needs not quote its arguments is useful since whatever values it uses would need to be available at compile-time anyway, which means you’d already almost certainly have to use
eval-when. In any case, if you want what
needs does without the autoquoting, you should now use
(eval-when (...) (requires ...)). ↩
SBCL’s behaviour is reasonable: what should
(make-pathname :name "*-loader" ...) do? Unfortunately it also leaves no way of making pathnames which have partly-wild components other than parsing namestrings. ↩
A key to making this work with ASDF is to turn off output translations and keep the compiled files alongside their sources. ↩