Roswell and Walled Gardens
Tagged as blog, common-lisp
Written on 2022-01-04 19:00:00 UTC
Recently, Eitaro Fukamachi has been sharing blog posts about Roswell, "a launcher for a major lisp environment that just works." Like many in the CL community, I've heard of Roswell and even dabbled with it a bit. I'm not sure how many people actually use Roswell, but I do know it's non-negligible.
Roswell certainly solves some real problems for folks, but I could never get into it myself. There are two primary reasons for that. First, I use a Linux distro with that a) stays relatively up to date with upstreams and b) makes it trivial to carry my own patches to CL implementations (which I frequently do). Second, Roswell feels like a walled garden to me (I doubt this was an intentional decision by its authors, however).
The purpose of this post is to dig more into the second reason. This is mostly for my own benefit. I have not really progressed beyond broad "feelings" on this subject and I'd be doing myself and the Roswell authors a disservice if I keep not using it based on mere feelings without some concrete issues backing it up. Perhaps it will benefit others as well by finding others with concerns similar to mine and getting a concrete set of issues laid out that we could work on contributing fixes for.
Roswell authors: If you read this, please know this isn't meant to be a dig at you. I'm writing this as a sincere effort at exploring why I don't like Roswell with an eye toward coming up with solutions that would make it more palatable to me and (hopefully) others with a similar mindset.
User/Environment Intercession
My core complaint is that Roswell interposes itself between the user and the CL
environment in a highly visible and intrusive way: through the ros
executable. Let's look at what this means in terms of both being an
implementation manager and scripting.
Implementation Manager
Roswell bills itself largely as an implementation manager. It makes it trivial to install just about any version of any major CL implementation on any computer. That's a huge win for folks running Debian or Ubuntu LTSs (as they tend to have packages that are extremely out of date) or on odd arch/OS combinations (if binary packages are not provided, Roswell can build the implementation for you).
But what does it mean to be an implementation manager? To me, that means after installing the implementation, I should be able to use it freely, as if it were installed by my native package manager. So let's give that a try:
user@rocinante:~$ ros install sbcl-bin
No SBCL version specified. Downloading sbcl-bin_uri.tsv to see the available versions...
[##########################################################################]100%
Installing sbcl-bin/2.2.0...
Downloading https://github.com/roswell/sbcl_bin/releases/download/2.2.0/sbcl-2.2.0-x86-64-linux-binary.tar.bz2
[##########################################################################]100%
Extracting sbcl-bin-2.2.0-x86-64-linux.tar.bz2 to /home/user/.roswell/src/sbcl-2.2.0-x86-64-linux/
Building sbcl-bin/2.2.0... Done.
Install Script for sbcl-bin...
Making core for Roswell...
Installing Quicklisp... Done 7169
user@rocinante:~$ export PATH="/home/user/.roswell/bin:$PATH"
user@rocinante:~$ sbcl
zsh: command not found: sbcl
Huh. Well that's disappointing. It seems that the only (out of the box) way to
run an implementation is via ros run
.
user@rocinante:~$ ros run
* (find-package :ql)
#<PACKAGE "QUICKLISP-CLIENT">
What does this mean? Virtually everything CL related needs to know you use
Roswell. Switching from an OS-managed SBCL install to Roswell-managed? Better
update your SLIME/Sly config to use ros run
instead of sbcl
. Writing
documentation for a cool hack? Better include directions for Roswell as well as
stock implementations (or hope that your users are confident enough in CL to
figure it out on their own). You know those bad jokes that go something like
"How do you know if someone is X? Don't worry, they'll tell you!"? This
kind of feels like a real-world instantiation of that.
Not only that, but Roswell is imposing its opinions on its users. See that
#<PACKAGE "QUICKLISP-CLIENT">
in the REPL? That certainly doesn't come from
my .sbclrc
, so where does it come from? Let's look at what ros run
invokes
under the hood:
user@rocinante:~$ ps aux | grep sbcl
user 43354 0.4 0.5 1238788 93548 pts/1 Sl+ 10:27 0:00 /home/user/.roswell/impls/x86-64/linux/sbcl-bin/2.2.0/bin/sbcl --core /home/user/.roswell/impls/x86-64/linux/sbcl-bin/2.2.0/lib/sbcl/sbcl.core --noinform --no-sysinit --no-userinit --eval (progn #-ros.init(cl:load "/etc/roswell/init.lisp")) --eval (ros:run '((:eval"(ros:quicklisp)")))
Yikes. It looks like ros run
modifies your CL image a decent amount by
default. Not only does it load its own init file at /etc/roswell/init.lisp
(while ignoring your own!), it also loads the Quicklisp client for you. And
it's not obvious here, but the QL client it loads is located at
~/.roswell/quicklisp/
, not the standard ~/quicklisp/
folder.
I dislike this for several reasons. First, I'm definitely biased here, but Quicklisp isn't the only dependency management solution out there. Second, this can make providing support to Roswell users a nightmare. If something goes wrong with one of my programs on a Roswell user's computer, I need to become an expert in Roswell to help them! Third, it really rubs me the wrong way that it just blithely ignores a user's standard customization file by default. Fourth, I think almost every feature added on top of the vanilla implementation should default to off. That reduces cognitive burden when worrying about if Roswell will ever change a default on me when they add a new feature.
So, how do we get a bog standard REPL from Roswell? Based on the help messages,
there is an option to disable loading QL and most of Roswell's own init
files. But there's no option to load the default RC files or skip
/etc/roswell/init.lisp
. So the best we can do seems to be:
user@rocinante:~$ ros run +Q +R --load /etc/sbclrc --load ~/.sbclrc
Which ends up invoking SBCL as:
/home/user/.roswell/impls/x86-64/linux/sbcl-bin/2.2.0/bin/sbcl --core /home/user/.roswell/impls/x86-64/linux/sbcl-bin/2.2.0/lib/sbcl/sbcl.core --noinform --no-sysinit --no-userinit --eval (progn #-ros.init(cl:load "/etc/roswell/init.lisp")) --eval (ros:run '((:load "/etc/sbclrc")(:load "/home/user/.sbclrc")))
Not terrible, but not great either.
Solution?
We already have a standard way for a user to specify to shadow programs of the
same name: the $PATH
variable. This is already used by other programming
language environment managers out there. Let's take a look
at RVM, which is probably the closest analog to
Roswell that I know of.
user@rocinante:~$ rvm install 2.6.9
[OUTPUT CUT]
user@rocinante:~$ which ruby
/home/user/.rvm/rubies/ruby-2.6.9/bin/ruby
That's nice! Every tutorial or piece of documentation out there that uses Ruby should Just Work. No need to modify anything because you're using RVM-managed Ruby instead of an OS-managed one.
So, Roswell can keep its opinionated setup if it really wants to, no matter how much I disagree with it (hey, that's how opinions go). But I think it would do its users a great service if installing an implementation also placed that implementation on the PATH, with the standard name. The easiest way of doing it is probably a shell script that looks something like:
#!/bin/sh
# A hypothetical Roswell command that resolves which version of SBCL we should
# use. This could look at config files, envvars, whatever.
_SBCL_PATH="$(ros which sbcl)"
exec "$_SBCL_PATH" "$@"
UPDATE: Opened an issue to discuss this more.
Scripting
Let's turn to scripting now: the other big place where it feels like Roswell is making a land grab and then building a wall around it.
First, let's get this out of the way: each CL implementation has different CLI options, some of them are persnickety about order, and it really sucks. This does make writing portable scripts difficult and is something that really needs improvement.
But, again, with Roswell's solution we see it forcing itself between the user and the CL implementation.
First, let's consider a script that works only on SBCL. Starting that script
with the following is a great way of doing that (assuming you don't care that
Busybox's env
doesn't support the -S
option).
#!/usr/bin/env -S sbcl --script
If Roswell added its managed implementations to the PATH, this would even work with Roswell! As it currently stands though, you need to start with something like:
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -- $0 "$@"
|#
You additionally need to define a main
function which Roswell calls for
you. Explicitly calling a function in the file, whether it be main
or
another, might work (so you could use sbcl --script
or similar which does
not automatically call main
for you), but I suspect it'd break ros build
and might result in some weird error messages if you ever return from that
function.
With this approach we run into many of the same issues as above. Support is difficult for projects maintained by non-Roswell users, it requires a Roswell and non-Roswell version of any given script, and you're subject to Roswell's opinionated defaults.
The defaults issue is particularly thorny here, as I have seen Roswell scripts
in the wild that depend on the existence of the ros
or ql
packages and
functions they export. This means that folks using SBCL can't count on being
able to do sbcl --load some-script.ros --eval '(main)'
and have it work. This
does not smell like portability to me.
Solution?
The CL community really needs a portable way of running scripts across multiple implementations and OSes. Unfortunately, I don't have a concrete solution in mind. If I did I would have started trying to implement it already!
I think cl-launch is pretty nice, but have
issues with its insistence on loading ASDF and upgrading it. ASDF is not
needed for every script under the sun and I'd sometimes prefer to use a
specific version of ASDF I ship with the script instead of whatever the
end-user has lying around in their ~/common-lisp/asdf/
folder. I also dislike
that it's written as a shell script, which makes it a non starter on Windows.
If Roswell aimed to be less of a monolith, perhaps its scripting facilities
could be broken out into a separate project and adapted to call implementations
directly instead of via ros
. This might be a tough sell though, given the
current defaults load a decent chunk of Roswell code into the image.
Honestly, I worry that there's never going to be a single implementation of CL scripting that satisfies everyone. Which leads to the next point...
Importance of interfaces
I don't know if you've noticed or not, but directing CL'ers (myself included) is a lot like herding cats. If you tell them to do one thing you'll have a couple follow along, some start then get lost on the way, some start to explore different options and then do what you suggested (or get lost), and some that do the exact opposite of what you want/invent a new way of doing it purely out of spite (I jest about the spite part... mostly).
Given Roswell's current state, if someone told me that I had to install Roswell
to run their fancy program which is run through a .ros
script, then, by God,
I will find a different way to run it.
Roswell is a black box to me. I don't know what utilities are loaded with ros run
or when running a script. And even if I did, Roswell could choose to
change them at any time. Similarly, I expect a certain contract from the
implementation's CLI when I run it, which ros run
breaks.
If we instead had some community specifications that described things like "CL scripts" (written with an honest attempt at considering competing needs and desires) and some project told me "you can run this script with any CL script runner that conforms to v1 of the CL script spec. Oh, by the way, Roswell contains one such implementation," I'd be much more likely to just say "OK" and install Roswell to get it to work. Knowing that there's the possibility I could make my own implementation of a conforming CL script runner would make me more likely to follow the crowd in the short term and then split off later if I really needed to.
I speak only for myself, of course, but my gut tells me a lot of CL'ers would feel the same way. Maybe it's because it's the way our favorite language is designed :).
Anyways, this post has grown too long. But it has achieved its primary purpose of helping me organize my thoughts on this topic. Now I can idly day-dream about "CL script" specs and how to get Roswell to install unsullied CL implementations on the user's PATH.