MrAnderson 0.6
MrAnderson 0.6 is out. If you’ve never heard of it, that’s perfectly fine - it’s the kind of tool that’s supposed to do its job quietly and stay out of sight. cider-nrepl has been using MrAnderson to inline its dependencies (bundle private copies of them that can’t clash with yours) for about as long as cider-nrepl has existed, and most of the time you’d never know it was there.
The trouble is the “most of the time”. MrAnderson has always had a few rough edges, and every so often one of them would draw blood during a cider-nrepl release and I’d find myself wondering whether we should just rip the whole thing out. I wasn’t even joking about it - at one point I went and opened a PR to remove MrAnderson from cider-nrepl entirely. That’s roughly how fond of it I was feeling.
What actually pulled me back was a different project. I’d started
migrating cider-nrepl’s build from Leiningen to tools.deps,
and MrAnderson, being a Leiningen plugin, was squarely in the way - which forced
the question of whether to drop dependency inlining altogether or make it work
outside of Leiningen. Poking at that turned into poking at MrAnderson’s
long-standing issues more broadly, and the grown-up answer won out: fix the
thing instead of tearing it out. This release is the result of that effort. It
even ships a Leiningen-free entry point (mranderson.core/inline-deps,
#42) so it can be
driven straight from tools.build, which is what got the ball rolling in the
first place.
Before I get into the gory details, huge thanks to Benedek Fazekas, who wrote MrAnderson and has carried it for years. I showed up with a pile of questions and a longer pile of patches, and he was nothing but supportive throughout - he even made me a co-maintainer along the way. None of what follows would have happened without him. Thank you, Benedek!
What problem is this even solving?
cider-nrepl is nREPL middleware, which is a fancy way of saying it gets loaded
into your project’s JVM, right next to whatever dependencies you happen to be
using. So picture this: cider-nrepl needs one version of rewrite-clj, your
project pins a different one. One of them wins, the other breaks, and you get a
baffling error that has nothing to do with anything you were working on.
A normal library can shrug and tell you to bump a version. Tooling that injects
itself into essentially every Clojure project on the planet doesn’t have that
luxury - it has to coexist with everyone’s choices. The trick MrAnderson pulls
is to make cider-nrepl stop depending on rewrite-clj in the usual sense at
all. Instead, it copies rewrite-clj’s source straight into cider-nrepl under a
private prefix:
;; rewrite-clj.zip becomes
cider.nrepl.inlined-deps.rewrite-clj.v1.rewrite-clj.zip
and then rewrites every reference to point at the private copy. Now cider-nrepl
carries its own sealed-off rewrite-clj that can’t possibly collide with yours.
In the JVM world this is usually called “shading”; in Clojure-land we call it
dependency inlining.
Why this is much harder than it looks
“Rename a namespace everywhere” sounds like a job for find-and-replace. It is absolutely not. Clojure refers to a namespace in a startling number of shapes, and they don’t all want the same treatment:
nsforms, with:require/:use,:asaliases,:refer, and prefix lists- fully-qualified vars in code (
some.ns/a-fn) - Java interop:
:import, fully-qualified class names, type hints, constructors defrecord/deftype, which compile down to Java classes- reader conditionals in
.cljc, data readers,(load "..."), resource paths - metadata and docstrings (which can, of course, contain example code)
And here’s the part that turns it from tedious into genuinely tricky: a dotted,
fully-qualified name can be either a namespace/var reference or a Java
class/record reference, and the two get munged differently. A namespace keeps
its dashes; a class has to follow the JVM’s package rules, where dashes become
underscores. Frequently the only way to tell which is which is to look at the
surrounding context. On top of all that, a dependency can ship compiled Java
.class files that need their own, separate repackaging - and the two
mechanisms have to agree with each other.
MrAnderson does a lot of this transformation textually rather than with a full program analyzer, mostly for speed. That’s a reasonable trade-off, but it’s also exactly where the gremlins live.
A taste of what got fixed
Here are a few of those gremlins, now squashed:
- Records and dashes (#73) -
cider-nrepl uses the prefix
cider.nrepl.inlined-deps, dash and all. A reference like(instance? instaparse.gll.Failure x)points at a record, which compiles down to a Java class - so the prefix has to follow Java’s package rules, and the dash becomes an underscore (inlined-depsbecomesinlined_deps). An ordinary namespace reference, on the other hand, keeps the dash. MrAnderson used to get this wrong and emit a broken reference; it now tells the two apart from the shape of the symbol, the way Clojure’s own reader does. - Over-eager imports (#52) -
if a dependency shipped its own class in the
clojure.langpackage, MrAnderson would happily prefix everyclojure.langimport it saw, including core classes likeclojure.lang.Var. Imports are now rewritten by exact class, not by package. - Mixed imports (#33) -
(:import [com.acme.impl Deftyped JavaClass])can mix adeftype-generated class with a real Java class, and the two need different prefixes. The import now gets split so each class lands in the right place. - Imports nobody was rewriting (#54, #92) - imports of inlined classes in a dependency’s top-level namespace, and in your own project’s sources, were simply being skipped.
loadstatements (#61) - libraries that split a namespace across(load "...")ed files (hello,tools.deps.alpha) embed a resource path, not a symbol. Those paths weren’t being repointed, so the inlined library couldn’t find its own pieces.- The genuinely scary one (#88) -
if an inlined library shared a root namespace with your project and shipped AOT
.classfiles, MrAnderson could delete your project’s own sources while cleaning up. That one I was happy to see the back of.
There’s also a good deal of less glamorous work underneath: the Java-class
repackaging used to be hardwired to a target/ directory and had no test
coverage at all, which is a big part of why these bugs lived as long as they
did. It’s now decoupled and tested. My favourite detail: MrAnderson builds its
own releases by inlining its own dependencies, so cutting 0.6 was itself a
full-scale integration test of the whole thing.
What’s next
There’s still plenty of room for improvement here. The body of each namespace is still rewritten textually rather than properly parsed, and I’d love to see that grow into something more principled one day. Some of the trickier corner cases (the dash-vs-underscore guessing in particular) really want a proper analyzer rather than clever pattern matching. But the first few steps have been encouraging, and for the first time in years I’m not eyeing the eject lever.
If you rely on MrAnderson, give 0.6 a spin - and if you hit a rough edge I missed, the issue tracker is right there. Bug reports with a minimal repro are worth their weight in gold.
That’s all from me for now. Keep hacking!