The JVM has one classpath and one version of every class on it. Clojure inherits
that. If your project pulls in library A, which needs some.lib 1.0, and library
B, which needs some.lib 2.0, and those two versions aren't compatible, you have
a problem that no amount of :exclusions will fully solve. Maven's conflict
resolution will pick one version and hope for the best. Sometimes that's fine.
Sometimes you get a NoSuchMethodError at three in the morning.
For applications this is usually annoying but manageable: you control the whole
dependency set, so you can pin versions and exclude things until it works. For
libraries it's worse, because your dependencies leak onto your users. If you
write a library that depends on some.lib 1.0, every project that uses your
library now has an opinion about some.lib forced on it, and if the user needs
2.0, you've just started a fight.
This is especially painful for developer tooling. nREPL middleware (cider-nrepl,
refactor-nrepl) gets loaded into someone else's running process, alongside
their dependencies. A REPL tool that drags in its own tools.reader or
rewrite-clj and clobbers the user's version is a tool that breaks the user's
project. That's the itch MrAnderson was built to scratch, and cider-nrepl has
been its main client for years.
The way out is inlining (also called shading or vendoring): take the
dependency, copy it into your own project under a private name, rewrite every
reference, and ship that. Now your copy of some.lib is called something like
yourproject.inlined-deps.some.lib, it can't collide with anyone else's
some.lib, and your users never even know it's there.
The catch in Clojure specifically is that namespaces are resolved by name at
runtime, not by file path. You can't just move files and rename packages the way
a Java bytecode shader does. You have to rewrite the names inside the source: the
ns forms, the requires, the imports, the type hints, the fully-qualified
symbols, the (load ...) paths. And if the dependency also bundles Java
.class files, you have to relocate those too, with a separate tool, and make
the rewritten source agree with where they landed. MrAnderson does all of that.
The design doc walks through how.
MrAnderson occupies a specific niche, source-level inlining for libraries, and most of the obvious alternatives are solving a different problem.
:exclusions and pin versionsThis is the right answer most of the time, and you should try it first. It breaks down only when you have a genuine diamond conflict: two dependencies that need incompatible versions of a third, where no single version satisfies both. Then no exclusion helps, because there's no good version to keep. That's the point where inlining earns its keep.
uber, Maven Shade Plugin)These relocate packages when you build an application uberjar. They run at assembly time, operate on bytecode/jar contents, and produce a single fat jar with the conflicts shaded away. Great for apps. They don't help a library that wants to publish a normal jar with a few dependencies quietly inlined, because there's no uberjar in the picture: you're deploying a library artifact that other people will pull in as a dependency. MrAnderson produces inlined source that goes into your ordinary library jar.
There's also the Clojure-runtime wrinkle: bytecode relocation can rename compiled
class names, but Clojure resolves namespaces from strings at runtime, and those
strings aren't reliably where a bytecode shader looks. So a pure jarjar/shade
pass tends to leave the namespace references behind, which is brittle at best.
MrAnderson uses jarjar for the Java .class files but does the Clojure side as a
source rewrite.
Unrelated, despite the name. shadow-cljs is a ClojureScript build tool; the "shadow" isn't about shading dependencies.
A friendly Leiningen wrapper around MrAnderson itself. If you're on Leiningen and want a gentler interface, lein-isolate wraps it nicely.
Honest version: inlining is a power tool, and it has sharp edges.
:exclusions work, use those. Inlining is more machinery and more
ways to go wrong.yourproj.inlined-deps.foo.v1v2v3.foo.core)
and they show up in stack traces. That's the price of isolation.(require (symbol ...)), resolve/find-ns on
a built-up name, or a name read out of a resource file, is invisible to the
rewriter. The reference stays pointed at the original, un-inlined name and blows
up at runtime.ClassNotFoundException no test saw coming. :gen-class with a fixed :name
has the same flavour: the generated class name is a config string, not a
namespace reference, so it won't be prefixed.None of this is exotic, but it's the stuff that bites. Most code inlines cleanly. If you've read all that and you still have two libraries fighting over a third, or you're shipping tooling that has to coexist with whatever the user already has on their classpath, MrAnderson is for you.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |