Static Executables with SBCL v2
timmons.dev
timmons.dev
Most people who actually used CL for building stuff were somewhat amused, and tried to find out what was so important about encapsulating everything into a single executable and why other languages didn't have to do it, but CL absolutely did.
I thought this was a red herring, but indeed for some reason it did affect acceptability. Glad to see a solution that looks fairly good.
$ find ~/bin /usr/bin/ /bin/ -executable | xargs -I XXX file XXX | rg "statically linked" | awk '{ print $1 }'
/home/t/bin/floki:
/home/t/bin/yj:
/home/t/bin/helm:
/home/t/bin/fzf:
/home/t/bin/kubectl:
/usr/bin/containerd-shim-runc-v1:
/usr/bin/hyperfine:
/usr/bin/docker-init:
/usr/bin/containerd-shim:
/usr/bin/containerd-shim-runc-v2:
/bin/busybox:Pretty much everyone who's ever used an AppImage on Linux, for one.
This is also still the norm for macOS, from the user's point of view (yes, the application is technically a folder, but as far as most end users are concerned it's a single "thing" that gets dropped into their Applications folder). Still the norm for Windows, too, at least for installers (and "portable" apps, while not as common as ones installed to Program Files, certainly ain't obscure, even in this day and age).
Having someone download Anaconda Python (massive install) to run a script is a huge fail in my eyes.
Go and Alpine Linux especially have caused a resurgence of interest in distributing static binaries. The vast majority of tools in the Go ecosystem encourage this, for instance.
Really the whole conflation of and "executable application" as a "single file" is a historical mistake that we made but haven't fully let go of yet, and it's caused huge issues for users. For example, the distinction between "static linking" and "dynamic linking" largely isn't exposed to users on macOS, because they use .app bundles -- for all intents and purposes every macOS .app is "statically linked" because it can never resolve against other (possibly incompatible) dynamic libraries and it's a single file the user can manage. Then static/dynamic linking is just a technical aspect of how some amounts of code get loaded into the process, it's not a factor which impacts distribution or anything.
At the end of the day, an "application" is NOT a file. It's an abstract object from the users perspective. Maybe there is a single file hiding behind the curtain, or it's a collection of files, or maybe something else entirely, but they aren't the same.
Even Windows is also now moving in this direction (you can now have 'Runtimes' in Windows so that, say, a .py file and its data files can appear collectively like an .exe and be managed like a single file, with a single python runtime in the background), and Linux is too with things like AppImage, etc.
Static linking is dead, long live static linking, etc etc.
A common detour you will hit, is that the ease you had in distribution of the program can be erased by the complication in tracking what statically linked lib is active in that program.
Such that static versus dynamic linking is just a show swinging of a trend back and forth.
Possibly a feedback loop on lack of popularity :). Other languages do have to do this in some way or another, with respect to their runtimes. The dependencies can be dynamically loaded, but the entry point - from the user's POV - still has to be invocable by double-clicking or by ./, or you'll lose audience. Also size considerations; people are now more willing to download a 100+ MB trivial application than they were just a few years ago.
On the runtime end, C/C++ solve this by having the runtime being part of the system, Java by somehow managing to make itself required everywhere, so everybody has a JRE already. Rust also compiles down to regular executables. Python got popular enough that at least Linux users are likely to have several runtimes already installed - but then Windows users don't, and any serious app will ship the runtime with it, etc.
In my experience, it always boiled down to this: how to ship a portable Lisp application in a single package (by "portable", I mean no installation step), where the user just has to double-click on something and have it run, and at no point they have to know or care it's written in Lisp? This, IIRC, was meaningfully non-trivial, especially if you wanted to interop with external dependencies by FFI. I remember that at one point, I shipped my code to a non-Lisper in form of a Dockerfile which handled installing SBCL for them. Not only they were grateful, they've managed to pass on that program to another friend who (back then) was not much of a coder at all, and they both used it. That's the qualitative difference easy deployment makes.
The third best thing is shipping the binary alongside the app. The worst thing I can think of is just not shipping a runtime.
(Of course when it comes to package maintainers, it may still be desirable to ship a separate runtime package that everything links to, so as long as the language provides ABI stability for the runtime... but I like to think of package maintainer use cases as being separate from normal usage.)
And of course, for developers who primarily write apps that don't "ship" to external users, it would be normal to feel puzzled about this.
PSA: PSA stands for Public Service Announcement. Just in case.
I've been in this field for 15 years now. At some point the puns are a bit overbearing. We have enough cognitive overhead anyway, it's inherent in our field.
They could have named it Carnegie Mellon Common Lisp, but hey, someone wanted to feel clever.
Though this is super minor compared to Ruby (gems & co.) and especially to Chef (where you work with cookbooks, recipes, etc.) And of course, the granddaddy of them all, Unix. Because of course, less is more (all of my non-techie friends roll their eyes when I tell them about that one).
As I understand it, this work is attempting to remove even those last few dynamic dependencies so you can distribute the dumped executable and be done.
So, better if you want simpler distribution at the cost of larger executables.
1. compile & load all lisp code
2. dump list of external symbols
3. save-lisp-and-die into a .core file
4. build new SBCL w/ symbols from step #2 linked in (using sbcl.o, which is the SBCL runtime)
5. run new SBCL, using the core from step #3
6. save-lisp-and-die into an executable
edit. fixed formatting