ASDF 3, or Why Lisp Is Now An Acceptable Scripting Language
ASDF 3, or Why Lisp Is Now An Acceptable Scripting Language
ASDF 3, or Why Lisp Is Now An Acceptable Scripting Language
Abstract
libraries, or network services; one can scale them into large, maintainable and modular systems; and one can make those new services available to other programs via the command-line as well as
via network protocols, etc.
The last barrier to making that possible was the lack of a
portable way to build and deploy code so a same script can run
unmodified for many users on one or many machines using one or
many different compilers. This was solved by ASDF 3.
ASDF has been the de facto standard build system for portable
CL software since shortly after its release by Dan Barlow in 2002
(Barlow 2004). The purpose of a build system is to enable division of labor in software development: source code is organized
in separately-developed components that depend on other components, and the build system transforms the transitive closure of
these components into a working program.
ASDF 3 is the latest rewrite of the system. Aside from fixing
numerous bugs, it sports a new portability layer UIOP. One can
now use ASDF to write Lisp programs that may be invoked from the
command line or may spawn external programs and capture their
output ASDF can deliver these programs as standalone executable
files; moreover the companion script cl-launch (see section 2.9)
can create light-weight scripts that can be run unmodified on many
different kinds of machines, each differently configured. These
features make portable scripting possible. Previously, key parts
of a program had to be configured to match ones specific CL
implementation, OS, and software installation paths. Now, all of
ones usual scripting needs can be entirely fulfilled using CL,
benefitting from its efficient implementations, hundreds of software
libraries, etc.
In this article, we discuss how the innovations in ASDF 3 enable
new kinds of software development in CL. In section 1, we explain
what ASDF is about; we compare it to common practice in the C
world; this section does not require previous knowledge of CL. In
section 2, we describe the improvements introduced in ASDF 3 and
ASDF 3.1 to solve the problem of software delivery; this section
requires some familiarity with CL though some of its findings are
independent from CL; for a historical perspective you may want to
start with appendices A to F below before reading this section. In
section 3, we discuss the challenges of evolving a piece of community software, concluding with lessons learned from our experience; these lessons are of general interest to software programmers
though the specifics are related to CL.
This is the extended version of this article. In addition to extra
footnotes and examples, it includes several appendices with historical information about the evolution of ASDF before ASDF 3. There
again, the specifics will only interest CL programmers, but general lessons can be found that are of general interest to all software
practitioners. Roughly in chronological order, we have the initial
ASDF, the de facto standard build system for Common Lisp, has
been vastly improved between 2009 and 2014. These and other improvements finally bring Common Lisp up to par with "scripting
languages" in terms of ease of writing and deploying portable code
that can access and "glue" together functionality from the underlying system or external programs. "Scripts" can thus be written in
Common Lisp, and take advantage of its expressive power, welldefined semantics, and efficient implementations. We describe the
most salient improvements in ASDF and how they enable previously difficult and portably impossible uses of the programming
language. We discuss past and future challenges in improving this
key piece of software infrastructure, and what approaches did or
didnt work in bringing change to the Common Lisp community.
Introduction
As of 2013, one can use Common Lisp (CL)1 to portably write
the programs for which one traditionally uses so-called "scripting"
languages: one can write small scripts that glue together functionality provided by the operating system (OS), external programs, C
1 Common
2015/5/9
:depends-on ("packages"))
(:file "pp-quasiquote"
:depends-on ("quasiquote"))))
successful experiment in section 4; how it became robust and usable in section 5; the abyss of madness it had to bridge in section 6;
improvements in expressiveness in section 7; various failures in
section 8; and the bug that required rewriting it all over again in
section 9.
All versions of this article are available at http://fare.
tunes.org/files/asdf3/: extended HTML, extended PDF,
short HTML, short PDF (the latter was submitted to ELS 2014).
1.
What ASDF is
Components
ASDF is a build system for CL: it helps developers divide software into a hierarchy of components and automatically generates a
working program from all the source code.
Top components are called systems in an age-old Lisp tradition,
while the bottom ones are source files, typically written in CL.
In between, there may be a recursive hierarchy of modules that
may contain files or other modules and may or may not map to
subdirectories.
Users may then operate on these components with various
build operations, most prominently compiling the source code (operation compile-op) and loading the output into the current Lisp
image (operation load-op).
Several related systems may be developed together in the same
source code project. Each system may depend on code from other
systems, either from the same project or from a different project.
ASDF itself has no notion of projects, but other tools on top of
ASDF do: Quicklisp (Beane 2011) packages together systems
from a project into a release, and provides hundreds of releases
as a distribution, automatically downloading on demand required
systems and all their transitive dependencies.
Further, each component may explicitly declare a dependency
on other components: whenever compiling or loading a component(as contrasted with running it) relies on declarations or definitions of packages, macros, variables, classes, functions, etc.,
present in another component, the programmer must declare that
the former component depends-on the latter.
1.1.2
Action Graph
2015/5/9
alike, as well as a protocol to declare features and conditionally include or omit code or data based on them. Therefore you
dont need dark magic at compile-time to detect available features. In C, people resort to horribly unmaintainable configuration scripts in a hodge podge of shell script, m4 macros, C
preprocessing and C code, plus often bits of python, perl,
sed, etc.
ASDF possesses a standard and standardly extensible way to
In-image
and compile-time, so there is only one configuration mechanism to learn and to use, and minimal discrepancy.6 In C, completely different, incompatible mechanisms are used at runtime
(ld.so) and compile-time (unspecified), which makes it hard
to match source code, compilation headers, static and dynamic
libraries, requiring complex "software distribution" infrastructures (that admittedly also manage versioning, downloading and
precompiling); this at times causes subtle bugs when discrepancies creep in.
Nevertheless, there are also many ways in which ASDF pales
in comparison to other build systems for CL, C, Java, or other
systems:
ASDF isnt a general-purpose build system. Its relative sim-
Most programmers are familiar with C, but not with CL. Its therefore worth contrasting ASDF to the tools commonly used by C programmers to provide similar services. Note though how these services are factored in very different ways in CL and in C.
To build and load software, C programmers commonly use
make to build the software and ld.so to load it. Additionally,
they use a tool like autoconf to locate available libraries and
identify their features.5 In many ways these C solutions are better
engineered than ASDF. But in other important ways ASDF demonstrates how these C systems have much accidental complexity that
CL does away with thanks to better architecture.
ern adversarial multi-user, multi-processor, distributed environments where source code comes in many divergent versions
and in many configurations. It is rooted in an age-old model of
building software in-image, whats more in a traditional singleprocessor, single-machine environment with a friendly single
user, a single coherent view of source code and a single target
6 There
3 Of
2015/5/9
configuration. The new ASDF 3 design is consistent and general enough that it could conceivably be made to scale, but that
would require a lot of work.
2.3
Understandable Internals
After bundle support was merged into ASDF (see section 2.2
above), it became trivial to implement a new concatenatesource-op operation. Thus ASDF could be developed as multiple files, which would improve maintainability. For delivery purpose, the source files would be concatenated in correct dependency
order, into the single file asdf.lisp required for bootstrapping.
The division of ASDF into smaller, more intelligible pieces had
been proposed shortly after we took over ASDF; but we had rejected the proposal then on the basis that ASDF must not depend
on external tools to upgrade itself from source, another strong requirement (see section 5.1). With concatenate-source-op,
an external tool wasnt needed for delivery and regular upgrade,
only for bootstrap. Meanwhile this division had also become more
important, since ASDF had grown so much, having almost tripled
in size since those days, and was promising to grow some more. It
was hard to navigate that one big file, even for the maintainer, and
probably impossible for newcomers to wrap their head around it.
To bring some principle to this division (2.26.62), we followed the principle of one file, one package, as demonstrated
by faslpath (Etter 2009) and quick-build (Bridgewater
2012), though not yet actively supported by ASDF itself (see section 2.10). This programming style ensures that files are indeed
providing related functionality, only have explicit dependencies on
other files, and dont have any forward dependencies without special declarations. Indeed, this was a great success in making ASDF
understandable, if not by newcomers, at least by the maintainer
himself;9 this in turn triggered a series of enhancements that would
not otherwise have been obvious or obviously correct, illustrating
the principle that good code is code you can understand, organized in chunks you can each fit in your brain.
Bundle Operations
2.4
Package Upgrade
Preserving the hot upgradability of ASDF was always a strong requirement (see section 5.1). In the presence of this package refactoring, this meant the development of a variant of CLs defpackage that plays nice with hot upgrade: define-package.
Whereas the former isnt guaranteed to work and may signal an
error when a package is redefined in incompatible ways, the latter will update an old package to match the new desired definition
while recycling existing symbols from that and other packages.
8 Most CL implementations maintain their own heap with their own garbage
collector, and then are able to dump an image of the heap on disk, that can
be loaded back in a new process with all the state of the former process.
To build an application, you thus start a small initial image, load plenty of
code, dump an image, and there you are. ECL, instead, is designed to be
easily embeddable in a C program; it uses the popular C garbage collector
by Hans Boehm & al., and relies on linking and initializer functions rather
than on dumping. To build an application with ECL (or its variant MKCL),
you thus link all the libraries and object files together, and call the proper
initialization functions in the correct order. Bundle operations are important
to deliver software using ECL as a library to be embedded in some C
program. Also, because of the overhead of dynamic linking, loading a single
object file is preferable to a lot of smaller object files.
9 On
the other hand, a special setup is now required for the debugger to
locate the actual source code in ASDF; but this price is only paid by ASDF
maintainers.
2015/5/9
2.6 run-program
With ASDF 3, you can run external commands as follows:
(run-program `("cp" "-lax" "--parents"
"src/foo" ,destination))
On Unix, this recursively hardlinks files in directory src/foo into
a directory named by the string destination, preserving the
prefix src/foo. You may have to add :output t :erroroutput t to get error messages on your *standard-output*
and *error-output* streams, since the default value, nil,
designates /dev/null. If the invoked program returns an error
code, run-program signals a structured CL error, unless you
specified :ignore-error-status t.
This utility is essential for ASDF extensions and CL code in
general to portably execute arbitrary external programs. It was a
challenge to write: Each implementation provided a different underlying mechanism with wildly different feature sets and countless corner cases. The better ones could fork and exec a process and
control its standard-input, standard-output and error-output; lesser
ones could only call the system(3) C library function. Moreover, Windows support differed significantly from Unix. ASDF 1
itself actually had a run-shell-command, initially copied over
from mk-defsystem, but it was more of an attractive nuisance
than a solution, despite our many bug fixes: it was implicitly calling format; capturing output was particularly contrived; and what
shell would be used varied between implementations, even more so
on Windows.12
ASDF 3s run-program is full-featured, based on code originally from XCVBs xcvb-driver (Brody 2009). It abstracts
away all these discrepancies to provide control over the programs
standard-output, using temporary files underneath if needed. Since
ASDF 3.0.3, it can also control the standard-input and error-output.
It accepts either a list of a program and arguments, or a shell command string. Thus your previous program could have been:
Portability Layer
(run-program
(format nil "cp -lax --parents src/foo S"
(native-namestring destination))
:output t :error-output t)
where (UIOP)s native-namestring converts the pathname object destination into a name suitable for use by the
operating system, as opposed to a CL namestring that might be
escaped somehow.
You can also inject input and capture output:
(run-program '("tr" "a-z" "n-za-m")
:input '("uryyb, jbeyq") :output :string)
returns the string "hello, world". It also returns secondary
and tertiary values nil and 0 respectively, for the (non-captured)
error-output and the (successful) exit code.
run-program only provides a basic abstraction; a separate
system inferior-shell was written on top of UIOP, and
provides a richer interface, handling pipelines, zsh style redirections, splicing of strings and/or lists into the arguments, and implicit conversion of pathnames into native-namestrings, of symbols
our first reflex was to declare the broken run-shellcommand deprecated, and move run-program to its own separate system. However, after our then co-maintainer (and now maintainer) Robert
Goldman insisted that run-shell-command was required for backward
compatibility and some similar functionality expected by various ASDF extensions, we decided to provide the real thing rather than this nuisance, and
moved from xcvb-driver the nearest code there was to this real thing,
that we then extended to make it more portable, robust, etc., according to the
principle: Whatever is worth doing at all is worth doing well (Chesterfield).
12 Actually,
10 U,
I, O and P are also the four letters that follow QWERTY on an anglosaxon keyboard.
11 ASDF 3.1 notably introduces a nest macro that nests arbitrarily many
forms without indentation drifting ever to the right. It makes for more
readable code without sacrificing good scoping discipline.
2015/5/9
Configuration Management
2.8
Standalone Executables
13 In CL, most variables are lexically visible and statically bound, but special variables are globally visible and dynamically bound. To avoid subtle
mistakes, the latter are conventionally named with enclosing asterisks, also
known in recent years as earmuffs.
2015/5/9
2.9 cl-launch
Running Lisp code to portably create executable commands from
Lisp is great, but there is a bootstrapping problem: when all you
can assume is the Unix shell, how are you going to portably invoke
the Lisp code that creates the initial executable to begin with?
We solved this problem some years ago with cl-launch. This
bilingual program, both a portable shell script and a portable CL
program, provides a nice colloquial shell command interface to
building shell commands from Lisp code, and supports delivery
as either portable shell scripts or self-contained precompiled executable files.14
Its latest incarnation, cl-launch 4 (March 2014), was updated to take full advantage of ASDF 3. Its build specification interface was made more general, and its Unix integration was improved. You may thus invoke Lisp code from a Unix shell:
cl -sp lisp-stripper \
-i "(print-loc-count \"asdf.lisp\")"
You can also use cl-launch as a script "interpreter", except
that it invokes a Lisp compiler underneath:15
#!/usr/bin/cl -sp lisp-stripper -E main
(defun main (argv)
(if argv
(map () 'print-loc-count argv)
(print-loc-count *standard-input*)))
In the examples above, option -sp, shorthand for --systempackage, simultaneously loads a system using ASDF during the
build phase, and appropriately selects the current package; -i,
shorthand for --init evaluates a form at the start of the execution
phase; -E, shorthand for --entry configures a function that is
called after init forms are evaluated, with the list of commandline arguments as its argument.16 As for lisp-stripper, its a
simple library that counts lines of code after removing comments,
blank lines, docstrings, and multiple lines in strings.
cl-launch automatically detects a CL implementation installed on your machine, with sensible defaults. You can easily override all defaults with a proper command-line option, a
configuration file, or some installation-time configuration. See
cl-launch --more-help for complete information. Note
that cl-launch is on a bid to homestead the executable path
/usr/bin/cl on Linux distributions; it may slightly more
portably be invoked as cl-launch.
A nice use of cl-launch is to compare how various implementations evaluate some form, to see how portable it is in practice,
whether the standard mandates a specific result or not:
for l in sbcl ccl clisp cmucl ecl abcl \
scl allegro lispworks gcl xcl ; do
cl -l $l -i \
'(format t "'$l': S%" `#5(1 ,@`(2 3)))' \
2>&1 | grep "^$l:" # LW, GCL are verbose
done
cl-launch compiles all the files and systems that are specified, and keeps the compilation results in the same output-file cache
2.10 package-inferred-system
ASDF 3.1 introduces a new extension package-inferredsystem that supports a one-file, one-package, one-system style
of programming. This style was pioneered by faslpath (Etter
2009) and more recently quick-build (Bridgewater 2012).
This extension is actually compatible with the latter but not the
former, for ASDF 3.1 and quick-build use a slash "/" as a
hierarchy separator where faslpath used a dot ".".
This style consists in every file starting with a defpackage
or define-package form; from its :use and :importfrom and similar clauses, the build system can identify a list
of packages it depends on, then map the package names to the
names of systems and/or other files, that need to be loaded first.
Thus package name lil/interface/all refers to the file
interface/all.lisp18 under the hierarchy registered by
system lil, defined as follows in lil.asd as using class
package-inferred-system:
(defsystem "lil" ...
:description "LIL: Lisp Interface Library"
:class :package-inferred-system
:defsystem-depends-on ("asdf-package-system")
:depends-on ("lil/interface/all"
"lil/pure/all" ...)
...)
The :defsystem-depends-on ("asdf-package-system")
is an external extension that provides backward compatibility with
ASDF 3.0, and is part of Quicklisp. Because not all package names
can be directly mapped back to a system name, you can register new
mappings for package-inferred-system. The lil.asd
file may thus contain forms such as:
(register-system-packages :closer-mop
'(:c2mop :closer-common-lisp :c2cl ...))
Then, a file interface/order.lisp under the lil hierarchy,
that defines abstract interfaces for order comparisons, starts with
the following form, dependencies being trivially computed from
the :use and :mix clauses:
(uiop:define-package :lil/interface/order
(:use :closer-common-lisp
:lil/interface/definition
:lil/interface/base
:lil/interface/eq :lil/interface/group)
(:mix :fare-utils :uiop :alexandria)
(:export ...))
14 cl-launch
2015/5/9
the style encourages good factoring of the code into coherent units;
by contrast, the traditional style of "everything in one package" has
low overhead but doesnt scale very well. ASDF itself was rewritten
in this style as part of ASDF 2.27, the initial ASDF 3 pre-release,
with very positive results.
Since it depends on ASDF 3, package-inferred-system
isnt as lightweight as quick-build, which is almost two orders of magnitude smaller than ASDF 3. But it does interoperate
perfectly with the rest of ASDF, from which it inherits the many
features, the portability, and the robustness.
2015/5/9
3.4
style break this assumption, and will require non-trivial work for
Quicklisp to support.
What then, is backward compatibility? It isnt a technical constraint. Backward compatibility is a social constraint. The new
version is backward compatible if the users are happy. This doesnt
mean matching the previous version on all the mathematically conceivable inputs; it means improving the results for users on all the
actual inputs they use; or providing them with alternate inputs they
may use for improved results.
3.3
The CL standard leaves many things underspecified about pathnames in an effort to define a useful subset common to many thenexisting implementations and filesystems. However, the result is
that portable programs can forever only access but a small subset
of the complete required functionality. This result arguably makes
the standard far less useful than expected (see section 6). The lesson is dont standardize partially specified features. Its better to
standardize that some situations cause an error, and reserve any resolution to a later version of the standard (and then follow up on it),
or to delegate specification to other standards, existing or future.
There could have been one pathname protocol per operating
system, delegated to the underlying OS via a standard FFI. Libraries could then have sorted out portability over N operating systems. Instead, by standardizing only a common fragment and letting each of M implementations do whatever it can on each operating system, libraries now have to take into account N*M combinations of operating systems and implementations. In case of disagreement, its much better to let each implementations variant
exist in its own, distinct namespace, which avoids any confusion,
than have incompatible variants in the same namespace, causing
clashes.
Interestingly, the aborted proposal for including defsystem
in the CL standard was also of the kind that would have specified
a minimal subset insufficient for large scale use while letting the
rest underspecified. The CL community probably dodged a bullet
thanks to the failure of this proposal.
3.5
2015/5/9
readtable at the REPL does not pollute the build. A patch to this
effect is pending acceptance by the new maintainer. Note that for
full support of readtable modification, other tools beside ASDF will
have to be updated too: SLIME, the Emacs mode for CL, as well as
its competitors for VIM, climacs, hemlock, CCL-IDE, etc.
Until such issues are resolved, even though the Lisp ideal is
one of ubiquitous syntax extension, and indeed extension through
macros is ubiquitous, extension though reader changes are rare in
the CL community. This is in contrast with other Lisp dialects, such
as Racket, that have succeeded at making syntax customization
both safe and ubiquitous, by having it be strictly scoped to the
current file or REPL. Any language feature has to be safe before
it may be ubiquitous; if the semantics of a feature depend on
circumstances beyond the control of system authors, such as the
bindings of syntax variables by the user at his REPL, then these
authors cannot depend on this feature.
3.6
4.2
Appendices
4.
4.1
Ever since the late 1970s, Lisp implementations have each been
providing their variant of the original Lisp Machine DEFSYSTEM
(Moon 1981). These build systems allowed users to define systems,
units of software development made of many files, themselves often grouped into modules; many operations were available to transform those systems and files, mainly to compile the files and to
load them, but also to extract and print documentation, to create
an archive, issue hot patches, etc.; DEFSYSTEM users could further declare dependency rules between operations on those files,
modules and systems, such that files providing definitions should
be compiled and loaded before files using those definitions.
Since 1990, the state of the art in free software CL build systems was mk-defsystem (Kantrowitz 1990).21 Like late 1980s
variants of DEFSYSTEM on all Lisp systems, it featured a declarative model to define a system in terms of a hierarchical tree of
components, with each component being able to declare dependencies on other components. The many subtle rules constraining build
operations could be automatically deduced from these declarations,
instead of having to be manually specified by users.
However, mk-defsystem suffered from several flaws, in addition to a slightly annoying software license. These flaws were
probably justified at the time it was written, several years before
the CL standard was adopted, but were making it less than ideal in
the world of universal personal computing. First, installing a system
required editing the system definition files to configure pathnames,
and/or editing some machine configuration file to define "logical
pathname translations" that map to actual physical pathnames the
"logical pathnames" used by those system definition files. Back
when there were a few data centers each with a full time administrator, each of whom configured the system once for tens of users,
this was a small issue; but when hundreds of amateur programmers
were each installing software on their home computer, this situation was less than ideal. Second, extending the code was very hard:
Mark Kantrowitz had to restrict his code to functionality universally available in 1990 (which didnt include CLOS), and to add a
lot of boilerplate and magic to support many implementations. To
add the desired features in 2001, a programmer would have had to
modify the carefully crafted file, which would be a lot of work, yet
eventually would probably still break the support for now obsolete
implementations that couldnt be tested anymore.
In the early history of Lisp, back when core memory was expensive,
all programs fit in a deck of punched cards. As computer systems
grew, they became files on a tape, or, if you had serious money, on
a disk. As they kept growing, programs would start to use libraries,
and not be made of a single file; then youd just write a quick
script that loaded the few files your code depended on. As software
kept growing, manual scripts proved unwieldy, and people started
developing build systems. A popular one, since the late 1970s, was
make.
Ever since the late 1970s, Lisp Machines had a build system
called DEFSYSTEM. In the 1990s, a portable free software reimplementation, mk-defsystem, became somewhat popular. By 2001,
it had grown crufty and proved hard to extend, so Daniel Barlow
created his own variant, ASDF, that he published in 2002, and that
became an immediate success. Dans ASDF was an experiment in
many ways, and was notably innovative in its extensible objectoriented API and its clever way of locating software. See section 4.
By 2009, Dans ASDF 1 was used by hundreds of software
systems on many CL implementations; however, its development
cycle was dysfunctional, its bugs were not getting fixed, those bug
fixes that existed were not getting distributed, and configuration
was noticeably harder that it should have been. Dan abandoned
CL development and ASDF around May 2004; ASDF was loosely
maintained until Gary King stepped forward in May 2006. After
Gary King resigned in November 2009, we took over his position,
and produced a new version ASDF 2, released in May 2010, that
turned ASDF from a successful experiment to a product, making it
upgradable, portable, configurable, robust, performant and usable.
See section 5.
The biggest hurdle in productizing ASDF was related to dealing
with CL pathnames. We explain a few salient issues in section 6.
While maintaining ASDF 2, we implemented several new features that enabled a more declarative style in using ASDF and CL
in general: declarative use of build extensions, selective control of
10
2015/5/9
Thus he could abandon the strictures of supporting long obsolete implementations, and instead target modern CL implementations. In 2002, he published ASDF, made it part of SBCL, and
used it for his popular CL software. It was many times smaller
than mk-defsystem (under a thousand line of code, instead of
five thousand), much more usable, easy to extend, trivial to port
to other modern CL implementations, and had an uncontroversial
MIT-style software license. It was an immediate success.
ASDF featured many brilliant innovations in its own right.
Perhaps most importantly as far as usability goes, ASDF cleverly used the *load-truename* feature of modern Lisps,
whereby programs (in this case, the defsystem form) can identify from which file they are loaded. Thus, system definition files
didnt need to be edited anymore, as was previously required with
mk-defsystem, since pathnames of all components could now
be deduced from the pathname of the system definition file itself.
Furthermore, because the truename resolved Unix symlinks, you
could have symlinks to all your Lisp systems in one or a handful
directories that ASDF knew about, and it could trivially find all
of them. Configuration was thus a matter of configuring ASDFs
*central-registry* with a list of directories in which to
look for system definition files, and maintaining "link farms" in
those directories and both aspects could be automated. (See
section 5.3 for how ASDF 2 improved on that.)
Also, following earlier suggestions by Kent Pitman (Pitman
1984), Dan Barlow used object-oriented style to make his defsystem extensible without the need to modify the main source
file.23 Using the now standardized Common Lisp Object System
(CLOS), Dan Barlow defined his defsystem in terms of generic
functions specialized on two arguments, operation and component, using multiple dispatch, an essential OO feature unhappily not available in lesser programming languages, i.e. sadly almost all of them they make do by using the "visitor pattern".
Extending ASDF is a matter of simply defining new subclasses of
operation and/or component and a handful of new methods
for the existing generic functions, specialized on these new subclasses. Dan Barlow then demonstrated such simple extension with
his sb-grovel, a system to automatically extract low-level details of C function and data structure definitions, so they may be
used by SBCLs foreign function interface.
4.4
because users were not relying on new features, but instead wrote
kluges and workarounds that institutionalized old bugs, there was
no pressure for providers to update; indeed the pressure was to not
update and risk be responsible for breakage, unless and until the
users would demand it. Thus, one had to assume that no bug would
ever be fixed everywhere; and for reliability one had to maintain
ones own copy of ASDF, and closely manage the entire build
chain: start from a naked Lisp, then get ones fixed copy of ASDF
compiled and loaded before any system could be loaded.24 In the
end, there was little demand for bug fixes, and supply followed by
not being active fixing bugs. And so ASDF development stagnated
for many years.
5.
In November 2009, we took over ASDF maintainership and development. A first set of major changes led to ASDF 2, released in
May 2010. The versions released by Dan Barlow and the maintainers who succeeded him, and numbered 1.x are thereafter referred to
as ASDF 1. These changes are explained in more detail in our ILC
2010 article (Goldman 2010).
5.1
Upgradability
The first bug fix was to break the vicious circle preventing bug
fixes from being relevant. We enabled hot upgrade of ASDF, so
that users could always load a fixed version on top of whatever
the implementation or distribution did or didnt provide. 25 Soon
enough, users felt confident relying on bug fixes and new features,
and all implementations started providing ASDF 2.
These days, you can (require "asdf") on pretty much
any CL implementation, and start building systems using ASDF.
Most implementations already provide ASDF 3. A few still lag with
ASDF 2, or fail to provide ASDF; the former can be upgraded with
(asdf:upgrade-asdf); all but the most obsolete ones can be
fixed by an installation script we provide with ASDF 3.1.
Upgradability crucially decoupled what ASDF users could rely
on from what implementations provided, enabling a virtuous circle
of universal upgrades, where previously everyone was waiting for
others to upgrade, in a deadlock. Supporting divergence creates
an incentive towards convergence.
Limitations of ASDF 1
ASDF was a great success at the time, but after many years, it was
also found to have its downsides: Dan Barlow was experimenting
with new concepts, and his programming style was to write the
simplest code that would work in the common case, giving him
most leeway to experiment. His code had a lot of rough edges:
while ASDF worked great on the implementation he was using
for the things he was doing with it, it often failed in ugly ways
when using other implementations, or exercising corner cases he
had never tested. The nave use of lists as a data structure didnt
scale to large systems with thousands of files. The extensibility API
while basically sane was missing many hooks, so that power users
had to redefine or override ASDF internals with modified variants,
which made maintenance costly.
Moreover, there was a vicious circle preventing ASDF bugs
from being fixed or features from being added (Rideau 2009):
Every implementation or software distribution (e.g. Debian) had its
own version, with its own bug fixes and its own bugs; so developers
of portable systems could not assume anything but the lowest
common denominator, which was very buggy. On the other hand,
24 It was also impossible to provide a well configured ASDF without preloading it in the image; and it was impossible to upgrade ASDF once it
was loaded. Thus Debian couldnt reliably provide "ready to go" images
that would work for everyone who may or may not need an updated ASDF,
especially not with stability several years forward.
25 In ASDF 3, some of the upgrade complexity described in our 2010 paper was done away with: even though CL makes dynamic data upgrade
extraordinarily easy as compared to other languages, we found that its
not easy enough to maintain; therefore instead of trying hard to maintain that code, we "punt" and drop in-memory data if the schema has
changed in incompatible ways; thus we do not try hard to provide methods for update-instance-for-redefined-class. The only potential impact of this reduction in upgrade capability would be users who
upgrade code in a long-running live server; but considering how daunting
that task is, properly upgrading ASDF despite reduced support might be the
least of their problems. To partly compensate this issue, ASDF 3 preemptively attempts to upgrade itself at the beginning of every build (if an upgrade is available as configured) that was recommended but not enforced
by ASDF 2. This reduces the risk of either having data to drop from a previous ASDF, or much worse, being caught upgrading ASDF in mid-flight.
In turn, such special upgrading of ASDF itself makes code upgrade easier.
Indeed, we had found that CL support for hot upgrade of code may exist
but is anything but seamless. These simpler upgrades allow us to simply
use fmakunbound everywhere, instead of having to unintern some
functions before redefinition.
23 Dan
Barlow may also have gotten from Kent Pitman the idea of reifying
a plan then executing it in two separate phases rather than walking the
dependencies on the go.
11
2015/5/9
5.2
Portability
common-lisp-controller (CLC) popularized the technique as far back as 2002, and so did the more widely portable asdfbinary-locations (ABL) after it: by defining an :around method
for the output-files function, it was possible for the user to divert
where ASDF would store its output. The fact that this technique could be
developed as an obvious extension to ASDF without the author explicitly
designing the idea into it, and without having to modify the source code, is
an illustration of how expressive and modular CLOS can be.
But apart from its suffering from the same lack of modularity as the
*central-registry*, CLC and ABL also had a chicken-and-egg
problem: you couldnt use ASDF to load it, or it would itself be compiled
and loaded without the output being properly diverted, negating any advantage in avoiding clashes for files of other systems.
ABL thus required special purpose loading and configuration in whichever
file did load ASDF, making it not modular at all. CLC tried to solve the issue
by managing installation or all CL software; it failed eventually, because
these efforts were only available to CL programmers using Debian or a few
select other Linux software distributions, and only for the small number of
slowly updated libraries, making the maintainers a bottleneck in a narrow
distribution process. CLC also attempted to institute a system-wide cache
of compiled objects, but this was ultimately abandoned for security issues; a
complete solution would have required a robust and portable build service,
which was much more work than justified by said narrow distribution
process.
The solution in ASDF 2 was to merge this functionality in ASDF itself,
according to the principle to make it as simple as possible, but no simpler.
But whereas ASDF 1 followed this principle under the constraint that the
simple case should be handled correctly, ASDF 2 updated the constraint
to include handling all cases correctly. Dan Barlows weaker constraint
may have been great for experimenting, it was not a good one for a robust
product.
Another, more successful take on the idea of CLC, is Zach Beanes Quicklisp (2011): it manages the loading and configuration of ASDF, and can
then download and install libraries. Because it does everything in the users
directory, without attempts to share between users and without relying on
support from the system or software distribution, it can be actually ubiquitous. Thanks to ASDF 2s modular configuration, the Quicklisp-managed
libraries can complement the users otherwise configured software rather
than one completely overriding the other.
26 Debians
Configurability
ASDF 1 was much improved over what preceded it, but its configuration mechanism was still lacking: there was no modular way for
whoever installed software systems to register them in a way that
users could see them; and there was no way for program writers
to deliver executable scripts that could run without knowing where
libraries were installed.
One key feature introduced with ASDF 2 (Goldman 2010)
was a new configuration mechanism for programs to find libraries,
the source-registry, that followed this guiding principle: Each can
specify what they know, none need specify what they dont.
Configuration information is taken from multiple sources, with
the former partially or completely overriding the latter: argument
explicitly passed to initialize-source-registry, environment variable, central user configuration file, modular user
configuration directory, central system configuration files, modular system configuration directories, implementation configuration,
with sensible defaults. Also, the source-registry is optionally capable of recursing through subdirectories (excluding source control
directories), where *central-registry* itself couldnt. Soft12
2015/5/9
5.4
Robustness
ASDF 1 used to pay little attention to robustness. A glaring issue, for instance, was causing much aggravation in large projects:
killing the build process while a file was being compiled would
result in a corrupt output file that would poison further builds until it was manually removed: ASDF would fail the first time, then
when restarted a second time, would silently load the partially compiled file, leading the developer to believe the build had succeeded
when it hadnt, and then to debug an incomplete system. The problem could be even more aggravating, since a bug in the program
itself could be causing a fatal error during compilation (especially
since in CL, developers can run arbitrary code during compilation).
The developer, after restarting compilation, might not see the issue; he would then commit a change that others had to track down
and painfully debug. This was fixed by having ASDF compile into
a temporary file, and move the outputs to their destination only in
case of success, atomically where supported by implementation and
OS. A lot of corner cases similarly had to be handled to make the
build system robust.
We eventually acquired the discipline to systematically write
tests for new features and fixed bugs. The test system itself was
vastly improved to make it easier to reproduce failures and debug
them, and to handle a wider variety of test cases. Furthermore, we
adopted the policy that the code was not to be released unless every
regression test passed on every supported implementation (the list
of which steadily grew), or was marked as a known failure due to
some implementation bugs. Unlike ASDF 1, that focused on getting
the common case working, and letting users sort out non-portable
uncommon cases with their implementation, ASDF 2 followed the
principle that code should either work of fail everywhere the same
way, and in the latter case, fail early for everyone rather than
pass as working for some and fail mysteriously for others. These
two policies led to very robust code, at least compared to previous
CL build systems including ASDF 1.
Robustness decoupled the testing of systems that use ASDF
from testing of ASDF itself: assuming the ASDF test suite is complete enough, (sadly, all too often a preposterous assumption), systems defined using ASDF 2 idioms run the same way in a great variety of contexts: on different implementations and operating systems, using various combinations of features, after some kind of
hot software upgrade, etc. As for the code in the system itself it
might still require testing on all supported implementations in case
it doesnt strictly adhere to a portable subset of CL (which isnt automatically enforceable so far), since the semantics of CL are not
fully specified but leave a lot of leeway to implementers, unlike e.g.
ML or Java.
5.5
5.6
Usability
6.
Appendix C: Pathnames
6.1
Performance
ASDF 1 performance didnt scale well to large systems: Dan Barlow was using the list data structure everywhere, leading to
worst case planning time no less than O(n4 ) where n is the total size
of systems built. We assume he did it for the sake of coding simplicity while experimenting, and that his minimalism was skewed
by the presence of many builtin CL functions supporting this old
school programming style. ASDF did scale reasonably well to a
large number of small systems, because it was using a hash-table to
find systems; on the other hand, there was a dependency propagation bug in this case (see section 9). In any case, ASDF 2 followed
the principle that good data structures and algorithms matter,
and should be tailored to the target problem; it supplemented or replaced the lists used by ASDF 1 with hash-tables for name lookup
and append-trees to recursively accumulate actions, and achieved
linear time complexity. ASDF 2 therefore performed well whether
or not the code was split in a large number of systems.
27 load-system
was actually implemented by Gary King, the last maintainer of ASDF 1, in June 2009; but users couldnt casually rely on it being
there until ASDF 2 in 2010 made it possible to rely on it being ubiquitous.
Starting with ASDF 3.1, (asdf:make :foo) is also available, meaning "do whatever makes sense for this component"; it defaults to loadsystem, but authors of systems not meant to be loaded can customize it to
mean different things.
13
2015/5/9
kind of files used while developing software, that makes UIOP not
suitable for dealing with all the corner cases that may arise when
processing files for end-users, especially not in adversarial situations. For a complete and well-designed reimplementation of pathnames, that accesses the operating system primitives and libraries
via CFFI, exposes them and abstracts over, in a way portable across
implementations (and, to the extent that its meaningful, across operating systems), see IOLib instead. Because of its constraint of
being self-contained and minimal, however, ASDF cannot afford to
use IOLib (or any library).
All the functions in (UIOP) have documentation strings explaining their intended contract.
6.2
6.3
Namestrings
Pathname Structure
28 Still,
14
2015/5/9
#p"foo-V1.2.lisp". This contrasts with the idioms previously used by ASDF 1 in similar situations, which for the
longest time would return #p"foo-V1.lisp". The type may
also be specified as :directory, in which case it treats the
last "word" in the slash-separated path as a directory component
rather than a name and type components; thus, (parse-unixnamestring "foo-V1.2" :type :directory) returns
#p"foo-V1.2/", at least on a Unix filesystem where the slash
is the directory separator.
Each function in UIOP tries to do the Right Thing, in its
limited context, when passed a string argument where a pathname
is expected. Often, this Right Thing consists in calling the standard CL function parse-namestring; sometimes, its calling parse-unix-namestring; rarely, its calling parsenative-namestring. And sometimes, that depends on a
:namestring argument, as interpreted by UIOPs general pathname coercing function ensure-pathname. To be sure, you
should read the documentation and/or the source code carefully.
6.4
Trailing Slash
Merging Pathnames
Logical Pathnames
A logical pathname is a way to specify a pathname under a virtual "logical host" that can be configured independently from the
physical pathname where the file is actually stored on a machine.
2015/5/9
Before it may be used, a logical pathname host must be registered, with code such as follows:
(setf (logical-pathname-translations
"SOME-HOST")
'(("SOURCE;**;*.LISP.*"
"/home/john/src/**/*.lisp.*")
("SOURCE;**;*.ASD.*"
"/home/john/src/**/*.asd.*")
("**;*.FASL.*"
"/home/john/.fasl-cache/**/*.fasl.*")
("**;*.TEMP.*"
"/tmp/**/*.tmp.*")
("**;*.*.*"
"/home/john/data/**/*.*.*")))
The first two lines map Lisp source files and system definitions
under the absolute directory source to a subdirectory in Johns
home; The third line maps fasl files to a cache; the fourth maps files
with a temporary suffix to /tmp; and the fifth one maps all the rest
to a data directory. Thus, the case-insensitive pathname #p"somehost:source;foo;bar.lisp" (internally stored in uppercase) would be an absolute logical pathname that is mapped to the
absolute physical pathname /home/john/src/foo/bar.lisp
on that machine; on different machines, it might be be configured differently, and for instance on a Windows machine might be
mapped to C:\Users\jane\Source\foo\bar.lsp.
Problem is, this interface is only suitable for power users: it
requires special setup before anything is compiled that uses them,
typically either by the programmer or by a system administrator, in an implementation-dependent initialization file (and the
#p"..." syntax is implementation-dependent, too). Moreover,
once a string is registered as a logical pathname host, it may shadow
any other potential use that string might have in representing an actual host according to some implementation-dependent scheme.
Such a setup is therefore not modular, and not robust: as an author,
to be sure youre not interfering with any other piece of software,
youd need to avoid all the useful hostnames on all Lisp installations on the planet; more likely, as a system administrator, youd
need to audit and edit each and every piece of Lisp software to
rename any logical pathname host that would clash with a useful
machine name. All of this made sense in the 1970s, but already
no more in the mid 1990s, and not at all in the 2010s. "Logical
pathnames" are totally inappropriate for distributing programs as
source code "scripts" to end users. Even programmers who are not
beginners will have trouble with "logical pathnames".
Importantly, the standard specifies that only a small subset of
characters is portably accepted: uppercase letters, digits, and hyphens. When parsed, letters of a logical pathname are converted
to uppercase; once mapped to physical pathnames, the uppercase
letters are typically converted to whatever is conventional on the
destination pathname host, which these days is typically lowercase,
unlike in the old days. Logical pathnames also use the semi-colon
";" as directory separator, and, in a convention opposite to that of
Unix, a leading separator indicates a :relative pathname directory whereas absence thereof indicates an :absolute pathname
directory. This makes the printing of standard logical pathnames
look quite unusual and the distraction generated is a minor nuisance.
Most implementations actually accept a preserved mix of lowercase and uppercase letters without mapping them all to uppercase. On the one hand, that makes these logical pathnames more
useful to users; on the other hand, this doesnt conform to the standard. One implementation, SBCL, strictly implements the standard,
in the hope of helping programmers not accidentally write nonconformant programs, but, actually makes it harder to portably use
6.8
always believe its a small bug that will be fixed in the next half-hour.
After hours of analysis and false tracks, we finally understand the issue for
good, and just do it... until we find the next issue, and so on.
16
2015/5/9
7.
30 It
17
2015/5/9
isnt finished until its tested and used in production. Until then,
there are likely issues that need to be addressed.
As an example use, the proper way to use the CFFI library is to
use :defsystem-depends-on ("cffi-grovel") as below, which defines the class asdf::cffi-grovel, that can be
designated by the keyword :cffi-grovel amongst the components of the system:
7.3
(defsystem "some-system-using-ffi"
:defsystem-depends-on ("cffi-grovel")
:depends-on ("cffi")
:components
((:cffi-grovel "foreign-functions")
...))
7.2
Encoding Support
Since the beginning, ASDF has had a mechanism to force recompilation of everything:
(asdf:oos 'asdf:load-op 'my-system :force t)
In ASDF 2 that would be more colloquially:
(asdf:load-system 'my-system :force :all)
In 2003, Dan Barlow introduced a mechanism to selectively
:force recompilation of some systems, but not others: :force
:all would force recompilation of all systems; :force t
would only force recompilation of the requested system; and
:force (some list of systems) would only force recompilation of the specified systems. However, his implementation
had two bugs: :force t would continue to force everything,
like :force :all; and :force (some list of systems) would cause a runtime error (that could have been found at
compile-time with static strong typing).
The bugs were found in 2010 while working on ASDF 2;
they were partially fixed, but support for the selective syntax was
guarded by a continuable error message inviting users to contact
the maintainer.31 Despite the feature demonstrably not ever having
had any single user, it had been partially documented, and so was
finally fixed and enabled in ASDF 2.015 (May 2011) rather than
removed.
The feature was then extended in ASDF 2.21 (April 2012) to
cover a negative force-not feature, allowing the fulfillment of
a user feature request: a variant require-system of loadsystem that makes no attempt to upgrade already loaded systems.
This is useful in some situations: e.g. where large systems already
loaded and compiled in a previously dumped image are known to
work, and need to promptly load user-specified extensions, yet do
not want to expensively scan every time the (configured subset of
the) filesystem for updated (or worse, outdated) variants of their
source code. The hook into the require mechanism was then
amended to use it.32
This illustrates both Dan Barlows foresight and his relative lack
of interest in developing ASDF beyond the point where it got the
rest of his software off the ground; and by contrast the obsession to
detail of his successor.
31 CL possesses a mechanism for continuable errors, cerror, whereby
users can interactively or programmatically tell the system to continue
despite the error.
32 The two mechanisms were further enhanced in ASDF 3, then in ASDF 3.1.
One conceptual bug was having the :force mechanism take precedence
over :force-not; this didnt fit the common use cases of users having
a set of immutable systems that shouldnt be refreshed at all, and needing
to stop a :force :all from recursing into them. This was only fixed in
ASDF 3.1.
33 And
indeed, though all other implementations that support Unicode accept the keyword :utf-8 as an external format, GNU CLISP, always
the outlier, wants the symbol charset:utf-8 in a special package
charset.
18
2015/5/9
19
2015/5/9
8.3
In the last days of ASDF 1, there was an attempt to export its small
set of general purpose utilities as package asdf-extensions,
quickly renamed asdf-utilities before the release of ASDF
2, to avoid a misnomer. Still, because ASDF had been changing so
much in the past, and it was hard to rely on a recent version, no
one wanted to depend on ASDF for utilities, especially not when
the gain was so small in the number of functions used. A brief
attempt was make these (now more numerous) utilities available
as a completely separate system asdf-utils with its own copy
of them in its own package. But the duplication felt like both
a waste of both runtime resources and maintainer time. Instead,
asdf-driver, once renamed UIOP, was relatively successful,
because it was also available as a system that could be updated
independently from the rest of ASDF, yet shared the same source
code and same package as the version used by ASDF itself. No
duplication involved. Thats a case where two namespaces for
the same thing was defeating the purpose, and one namespace
necessitated the two things to actually be the same, which could
not be the case until this transclusion made it possible.
In a different failure mode, a brief attempt to give asdfdriver the nickname d was quickly met with reprobation, as
many programmers feel that that short a name should be available
for a programmers own local nicknames while developing. Trying
to homestead the :DBG keyword for a debugging macro met the
same opposition. Some (parts of) namespaces are in the commons
and not up for grabs.
Partial Solutions
The asdf-binary-locations extension ultimately failed because it didnt fully solve its configuration problem, only concentrated it in a single point of failure. The *system-cache*
feature to share build outputs between users and associated getuid function, introduced by common-lisp-controller and
used by ASDF 2s output-translation layer, were removed because of security issues. See section 5.3. A :currentdirectory keyword in the configuration DSL was removed,
because not only did its meaning vary wildly with implementation
and operating system, this meaning varied with what the value of
that global state at the time the configuration file was read, yet
because of lazy loading and implicit or explicit reloading of configuration, no one was really in control of that value. On the other
hand, the :here keyword was a successful replacement: it refers
to the directory of the configuration file being read, the contents of
which are clearly controlled by whoever writes that file.
In an attempt to solve namespace clashes between .asd files,
Dan Barlow had each of them loaded in its own automatically created private package asdf0, asdf1, etc., automatically deleted
afterward. But this didnt help. If the file contained no new definition, this hassle wasnt needed; and if there were new definitions,
8.4
Some features were not actively rejected, but havent found their
users yet.
ASDF 3 introduced build-op as a putative default build operation that isnt specialized for compiling CL software. But it
hasnt found its users yet. The associated function asdf:buildsystem was renamed asdf:make in ASDF 3.1 in an effort to
make it more usable. Maybe we should add an alias asdf:aload
for asdf:load-system, too.
During the ASDF 2 days, the *load-system-operation*
was designed so that ECL may use load-bundle-op instead of
load-op by default; but thats still not the case, and wont be
until ECL users more actively test it, which they might not do until
its the default, since they havent otherwise heard of it. Indeed, it
seems there are still bugs in corner cases.
8.5
Ubiquity or Bust!
CL possesses a standard but underspecified mechanism for extending the language: (require "module") loads given "mod20
2015/5/9
Interface Rigidity
Back in the bad old days of ASDF 1, the official recipe, described
in the manual, to override the default pathname type .lisp for a
Lisp source file to e.g. .cl, used to be to define a method on the
generic function source-file-type, specialized on the class
cl-source-file and on your system (in this example, called
my-sys):
(defmethod source-file-type
((c cl-source-file)
(s (eql (find-system 'my-sys))))
"cl")
Some people advertised this alternative, that also used to work,
to define your own sub-class foo-file of cl-source-file,
and use: (defmethod source-file-type ((c foo-file)
(s module)) "foo"). This caused much grief when we tried
to make system not a subclass of module anymore, but both be
subclasses of new abstract class parent-component instead.
In ASDF 2.015, two new subclasses of cl-source-file
were introduced, cl-source-file.cl and cl-sourcefile.lsp, that provide the respective types .cl and .lsp,
which covers the majority of systems that dont use .lisp. Users
need simply add to their defsystem the option :defaultcomponent-class :cl-source-file.cl and files will
have the specified type. Individual modules or files can be overridden, too, either by changing their class from :file to :clsource-file.cl, or more directly by specifying a :pathname
parameter.
If needed, users can define their own subclass of cl-sourcefile and override its default type, as in:
(defclass my-source-file (cl-source-file)
((type :initform "l")))
Or they can directly override the type while defining a component,
as in:
(:file "foo" :type "l")
In any case, the protocol was roundabout both for users and
implementers, and a new protocol was invented that is both simpler
21
2015/5/9
bug remained in the bug tracker, with maybe two other minor
annoyances; all of them were bugs as old as ASDF itself, related
to the traverse algorithm that walks the dependency DAG.
The minor annoyances were that a change in the .asd system
definition file ought to trigger recompilation in case dependencies
changed in a significant way, and that the traverse algorithm
inherited from ASDF 1 was messy and could use refactoring to allow finer and more modular programmatic control of what to build
or not to build. The real but small bug was that dependencies were
not propagated across systems. Considering that my co-maintainer
Robert Goldman had fixed the same bug earlier in the case of dependencies across modules within a system, and that one reason he
had disabled the fix across systems was that some people claimed
they enjoyed the behavior, it looked like the trivial issue of just enabling the obvious fix despite the conservative protests of some old
users. It was a wafer thin mint of an issue.
And so, of course, since this was the "last" bug standing, and
longstanding, I opened it... except it was a Pandoras Box of bigger
issues, where the fixing of one quickly led to another, etc., which
resulted in the explosion of ASDF 2.
Underspecified Features
9.2
9.
9.1
In the last release by Dan Barlow, ASDF 1.85 in May 2004, the
traverse algorithm was a 77-line function with few comments,
a terse piece of magic at the heart of the original 1101-line build
system.38 Shortly before I inherited the code, in ASDF 1.369 in October 2009, it had grown to 120 lines, with no new comment but
with some commented out debugging statements. By the time of
ASDF 2.26 in October 2012, many changes had been made, for
correctness (fixing the incorrect handling of many corner cases),
for robustness (adding graceful error handling), for performance
(enhancing asymptotic behavior from O(n4 ) to O(n) by using better data structures than nave lists), for extensibility (moving away
support for extra features such as :version and :feature),
for portability (a trivial tweak to support old Symbolics Lisp Machines!), for maintainability (splitting it into multiple smaller functions and commenting everything). There were now 8 functions
spanning 215 lines. Yet the heart of the algorithm remained essentially unchanged, in what was now a heavily commented 86-line
function do-traverse. Actually, it was one of a very few parts
of the ASDF 1 code base that we hadnt completely rewritten.
Indeed, no one really understood the underlying design, why
the code worked when it did (usually) and why it sometimes didnt.
The original author was long gone and not available to answer questions, and it wasnt clear that he fully understood the answers himself Dan Barlow had been experimenting, and how successfully!
His ASDF illustrates the truth that code is discovery at least as
much as design; he had tried many things, and while many failed,
he struck gold once or twice, and thats achievement enough for
anyone.
Nevertheless, the way traverse recursed into children components was particularly ugly; it involved an unexplained special
kind of dependency, do-first, and propagation of a force flag. But
of course, any obvious attempt to simplify these things caused the
algorithm to break somehow.
Here is a description of ASDF 1s traverse algorithm,
reusing the vocabulary introduced in section 1.1.3.
traverse recursively visits all the nodes in the DAG of actions, marking those that are visited, and detecting circularities.
Each action consists of an operation on a component; for a simple
CL system with regular Lisp files, these actions are compile-
git checkout of the code has a make target extract that will extract
notable versions of the code, so you can easily look at them and compare
them.
38 A
37 An
22
2015/5/9
23
2015/5/9
For instance, the good old downward propagation was implemented by this mixin:
(defclass downward-operation (operation) ())
(defmethod component-depends-on
((o downward-operation)
(c parent-component))
`((,o ,@(component-children c))
,@(call-next-method)))
Prepare Operation
And so we introduced a new operation, initially called parentload-op (2.26.14), but eventually renamed prepare-op (2.26.21),
corresponding to the steps required to be taken in preparation for a
load-op or compile-op, namely to have completed a loadop on all the sideway dependencies of all the transitive parents.
Now, unlike load-op and compile-op that both were propagated downward along the dependency graph, from parents to
children, prepare-op had to be propagated upward, from children to parents. And so, the operation class had a new special
subclass upward-operation, to be specially treated by traverse...
Or better, the propagation could be moved entirely out of traverse and delegated to methods on component-dependson! A mixin class downward-operation would handle the
downward propagation along the component hierarchy for loadop, compile-op and the likes, whereas upward-operation
would handle prepare-op; sideway-operation would
handle the dependency from prepare-op to the load-op
of a components declared depends-on, whereas selfwardoperation would handle the dependency of load-op and
compile-op to prepare-op. Thanks to CLOS multiple inheritance and double dispatch, it all fell into place (2.26.21).
40 Interestingly, this compute-action-stamp could be very easily updated to use cryptographic digests of the various files instead of timestamps,
or any other kind of stamp. Because its the only function for which the
contents of stamps isnt opaque, and is a generic function that takes a plan
class as parameter, it might be possible to override this function either for
a new plan class and make that the *default-plan-class*), without destructively modifying any code. However, this hasnt been tested, so
theres probably a bug lurking somewhere. Of course, such a modification
cannot be part of the standard ASDF core, because it has to be minimal and
ubiquitous and cant afford to pull a cryptographic library (for now), but
an extension to ASDF, particularly one that tries to bring determinism and
scalability, could use this very simple change to upgrade from timestamps
to using a persistent object cache addressed by digest of inputs.
Needed In Image
24
2015/5/9
Why Oh Why?
Some may ask: how did ASDF survive for over 11 years with such
an essential birth defect? Actually, the situation is much worse:
the very same bug was present in mk-defsystem, since 1990.
Worse, it looks like the bug might have been as old as the original
DEFSYSTEM from the 1970s.
The various proprietary variants of defsystem from Symbolics, Franz, and LispWorks all include fixes to this issue. However,
the variants from Symbolics and Franz, require using a non-default
kind of dependency, :definitions, as opposed to the regularly
advertised :serial; also, the variant from Franz still has bugs in
corner cases. Meanwhile the variant from LispWorks also requires
the programmer to follow a non-trivial and under-documented discipline in defining build :rules, so you need to declare your dependencies in two related rules :caused-by and :requires
that are akin to the original depends-on vs do-first in ASDF 1 (but
probably predate it). What is worse, the live knowledge about this
bug and its fix never seems to have made it out to the general Lisp
programming public, and so most of those who are using those
tools are probably doing it wrong, even when the tools allow them
to do things right. The problem isnt solved unless the bug is
fixed by default.
This is all very embarrassing indeed: in the world of C programming, make solved the issue of timestamp propagation, correctly,
since 1976. Though historical information is missing at this point, it
seems that the original DEFSYSTEM was inspired by this success.
Even in the Lisp world the recent faslpath and quick-build,
though they were much simpler than any defsystem variant, or
quite possibly because they were much simpler, got it right on the
first attempt. How come the bug was not found earlier? Why didnt
most people notice? Why didnt the few who noticed something
care enough to bother fixing it, and fixing it good?
We can offer multiple explanations to this fact. As a first explanation, to put the bug back in perspective, an analogy in the C
world would be that sometimes when a .h file is modified in a
different library (and in some more elaborate cases, in the same
library, if its divided in multiple modules), the .c files that use
it are not getting recompiled. Put that way, you find that most C
builds actually have the same problem: many simple projects fail
to properly maintain dependency information between .c and .h
files, and even those that do dont usually account for header files
in other libraries, unless they bother to use some automated dependency analysis tools. Still, the situation is somewhat worse in
the CL world: first because every file serves the purpose of both
.c and .h so these dependencies are ubiquitous; second because
because CL software is much more amenable to modification, indeed, dynamic interactive modification, so these changes happen
more often; third because CL software libraries are indeed often
lacking in finish, since tinkering with the software is so easy that
users are often expected to do so rather than have all the corner
cases painfully taken care of by the original author. In C, the development loop is so much longer, jumping from one library to the
next is so expensive, that building from clean is the normal thing to
do after having messed with dependencies, which often requires reconfiguring the software to use a special writable user copy instead
of the read-only system-provided copy. The price usually paid in
awkwardness of the development process in C is vastly larger than
the price paid to cope with this bug in CL. Users of languages like
Python or Java, where installation and modification of libraries is
more streamlined by various tools, do not have this problem. But
25
2015/5/9
then their programs dont have any kind of macros, so they lose, a
lot, in expressiveness, as compared to CL, if admittedly not to C.
As a second explanation, most CL programmers write software
interactively in the small, where the build system isnt a big factor.
This is both related to the expressive power of the language, that
can do more with less, and to the size of the community, which is
smaller. In the small, there are fewer files considered for build at
a time; only one file changes at a time, in one system, on one machine, by one person, and so the bug isnt seen often; when a dependency changes incompatibly, clients are modified before the system
is expected to work anyway. Those who have written large software
in the past tended to use proprietary implementations, that provided
a defsystem where this bug was fixed. ITA Software was one of
the few companies using ASDF to write really large software, and
indeed, its by managing the build there that we eventually cared
enough to fix ASDF. In the mean time, and because of all the issues
discussed above, the policy had long been to build from clean before running the tests that would qualify a change for checkin into
the code repository.
As third, and related, explanation, Lisp has historically encouraged an interactive style of development, where programs compile
very fast, while the programmer is available at the console. In the
event of a build failure, the programmer is there to diagnose the
issue, fix it, and interactively abort or continue the build, which
eliminates most cases of the bug due to an externally interrupted
build. Utter build failures and interruptions are obvious, and programmers quickly learn that a clean rebuild is the solution in case
of trouble. They dont necessarily suspect that the bug is the build
system, rather than in their code or in the environment, especially
since the bug usually shows only in conjunction with such other
bug in their code or in the environment.
As a fourth explanation, indeed for the defsystem bug to
show without the conjunction of an obvious other bug, it takes
quite the non-colloquial use of "stateful" or "impure" macros, that
take input from the environment (such as the state of packages or
some special variables) into account in computing their output expansions. Then, a change in a dependency can lead in expecting a
change in the macro expansion, without the client site being modified, and that change will fail to take place due to the defsystem bug. But most macros are "stateless", "pure", and have no such
side-effect. Then, a meaningful change in a macro defined in a dependency usually requires a change in the client file that depends
on it, in which case the client will be recompiled after that change
and no bug will be seen. The one case that the programmer may notice, then, is when the macro interface didnt change, but a bug in
its implementation was fixed, and the clients were not recompiled.
But the programmer is usually too obsessed with his bug and fixing
it to pay close attention to a bug in the build system.
9.9
Bibliography
Henry Baker. Critique of DIN Kernel Lisp Definition Version 1.2. 1992.
http://www.pipeline.com/~hbaker1/CritLisp.html
Daniel Barlow. ASDF Manual. 2004. http://common-lisp.net/
project/asdf/
Zach Beane. Quicklisp. 2011. http://quicklisp.org/
Alastair Bridgewater. Quick-build (private communication). 2012.
Franois-Ren Rideau and Spencer Brody. XCVB: an eXtensible Component Verifier and Builder for Common Lisp. 2009. http://
common-lisp.net/projects/xcvb/
Peter von Etter. faslpath. 2009. https://code.google.com/p/
faslpath/
Franois-Ren Rideau and Robert Goldman. Evolving ASDF: More Cooperation, Less Coordination. 2010. http://common-lisp.net/
project/asdf/doc/ilc2010draft.pdf
Mark Kantrowitz. Defsystem: A Portable Make Facility for Common Lisp. 1990. ftp://ftp.cs.rochester.edu/pub/
archives/lisp-standards/defsystem/pd-code/
mkant/defsystem.ps.gz
Dan Weinreb and David Moon. Lisp Machine Manual. 1981.
https://bitsavers.trailing-edge.com/pdf/mit/
cadr/chinual_4thEd_Jul81.pdf
Kent Pitman. The Description of Large Systems. 1984. http://www.
nhplace.com/kent/Papers/Large-Systems.html
Franois-Ren Rideau. Software Irresponsibility. 2009. http://fare.
livejournal.com/149264.html
Richard Elliot Robbins. BUILD: A Tool for Maintaining Consistency in
Modular Systems. 1985. ftp://publications.ai.mit.edu/
ai-publications/pdf/AITR-874.pdf
The Aftermath
At the end of this epic battle against a tiny old bug, ASDF was
found completely transformed: much more sophisticated, yet much
simpler. For instance, the commented traverse-action function is 43 lines long, which is still significantly less than the original traverse function. Reading the ASDF 3 source code requires
much less figuring out what is going on, but much more understanding the abstract concepts at the same time, the abstract concepts
are also well documented, when they were previously implicit.
Interestingly, this new ASDF 3 can still meaningfully be said
to be "but" a debugged version of Dan Barlows original ASDF
1. Dan probably had no idea of all the sophistication required to
make his defsystem work correctly; if he had, he might have
been scared and not tried. Instead, he was daringly experimenting
many ideas; many of them didnt pan out in the end, but most were
26
2015/5/9