What's the difference between Cake and Leiningen?
4 Answers
This answer continues to get interest, presumably as a reference for Leiningen in StackOverflow so it is now significantly edited to update it for 2014.
Leiningen and Cake merged back in 2011. Leiningen (version 2) is now the de facto Clojure automation tool.
Leiningenis a build tool and dependency manager for Clojure which includes the ability to set up an interactive REPL with the appropriately configured classpath and with all java and clojure dependencies acquired in an automated manner from maven repositories and/or the community based Clojars.
Cake was very similar to Leiningen (down to using the same project.clj file format at the time) but attempted to avoid a lot of startup overhead by keeping persistent JVMs around in the background. This was more responsive but traded convenience for bugs due to accumulated state in the persistent processes (old function definitions hanging around etc.) over the typical course of iterative REPL based development. This turned out to be a bad bargain.
Experience with Leiningen and a continued desire for faster startup times have lead to an number of recommendations and approaches for speeding things up: https://github.com/technomancy/leiningen/wiki/Faster
The main difference is in the way tasks are implemented.
Cake's approach is "it's hard to extend functions after they've been defined, so let's invent a new mechanism for tasks rather than use functions", which resulted in the deftask macro.
Leiningen's approach is "it's hard to extend functions after they've been defined, so we should make a way to do this easily; that way we can use functions for tasks and also be able to extend things that aren't tasks," which lets you apply all the composability advantages of functions with tasks.
As Alex mentioned, the most striking difference is speed from the command line. Cake uses a persistent JVM, so you only encounter the jvm startup overhead when you run a task within your project for the first time. If you are not using emacs + slime + clojure-test-mode, this can be a huge timesaver. For example, a reasonably large set of tests on one of my projects runs in 0.3 seconds in cake, vs 11.2s in lein.
Aside from performance, the core idea behind cake is the dependency task model. Each task is only run once in a given build, taking into account all transitive prerequisites in the dependency graph. Here's an example from Martin Fowler's article on rake in cake syntax, which goes directly in your project.clj.
(deftask code-gen
"This task generates code. It has no dependencies."
(println "generating code...")
...)
(deftask compile #{code-gen}
"This task does the compilation. It depends on code-gen."
(println "compiling...")
...)
(deftask data-load #{code-gen}
"This task loads the test data. It depends on code-gen."
(println "loading test data...")
...)
(deftask test #{compile data-load}
"This task runs the tests. It depends on compile and data-load."
(println "running tests...")
...)
To do the same in Leiningen, you would first have to create a leiningen directory in your project with 4 files: code_gen.clj, compile.clj, data_load.clj, and my_test.clj.
src/leiningen/code_gen.clj
(ns leiningen.code-gen
"This task generates code. It has no dependencies.")
(defn code-gen []
(println "generating code..."))
src/leiningen/my_compile.clj
(ns leiningen.my-compile
"This task does the compilation. It depends on code-gen."
(:use [leiningen.code-gen]))
(defn my-compile []
(code-gen)
(println "compiling..."))
src/leiningen/data_load.clj
(ns leiningen.data-load
"This task loads the test data. It depends on code-gen."
(:use [leiningen.code-gen]))
(defn data-load []
(code-gen)
(println "loading test data..."))
src/leiningen/my_test.clj
(ns leiningen.my-test
"This task runs the tests. It depends on compile and data-load."
(:use [leiningen.my-compile]
[leiningen.data-load]))
(defn my-test []
(my-compile)
(data-load)
(println "running tests..."))
One would expect...
generating code...
compiling...
loading test data...
running tests...
But both data-load and my-compile depend on code-gen, so your actual ouput is...
generating code...
compiling...
generating code...
loading test data...
running tests...
You would have to memoize code-gen to prevent it from being run multiple times:
(ns leiningen.code-gen
"This task generates code. It has no dependencies.")
(def code-gen (memoize (fn []
(println "generating code..."))))
output:
generating code...
compiling...
loading test data...
running tests...
Which is what we want.
Builds are simpler and more efficient if a task is only ran once per build, so we made it the default behavior in cake builds. The philosophy is decades old and shared by a lineage of build tools. You can still use functions, you can still call them repeatedly, and you always have the full power of clojure at your disposal.
Lein just gives you a plain function as a task, but with the added constraint that it must have it's own namespace in src. If a task depends on it, it will be in a separate namespace, and must use/require the other in it's ns
macro. Cake builds look much neater and concise in comparison.
Another key difference is how tasks are appended to. Let's say we wanted to add my-test
as prerequisite to cake/lein's built in jar
task. In cake, you can use the deftask
macro to append to a task's forms and dependencies.
(deftask jar #{my-test})
Lein uses Robert Hooke to append to tasks. It's a really cool library, named after everyone's favorite natural philospher, but it would require a macro for the conciseness of deftask
.
(add-hook #'leiningen.jar/jar (fn [f & args]
(my-test)
(apply f args)))
Cake also has the notion of a global project. You can add user specific dev-dependencies, like swank, to ~/.cake/project.clj
and have it across all of your projects. The global project is also used to start a repl outside of a project for experimentation. Lein implements similar features by supporting per-user configuration in ~/.lein/init.clj
, and global plugins in ~/.lein/plugins
. In general, Lein currently has a much richer plugin ecosystem than cake, but cake includes more tasks out of the box (war, deploy, java compilation, native dependencies, clojars, and swank). Cljr may also be worth checking out, it's essentially just a global project with a package manager, but without build capabilities (i have no experience with it however).
The real irreconcible difference is task defintions, as technomancy pointed out. In my (biased) opinion, cake handles tasks much better. The need for a task dependency model became evident when we started using protocol buffers in our project with lein. Protobufs were pre-requisites for all of our tasks, yet compiling them is really slow. We also have alot of inter-dependent tasks, so any build was painful. I also don't like the requirement of a seperate namespace, and therefore an additional src file, for every task I create. Developers should create a lot tasks, lein's approach discourages this by creating too much friction. With cake, you can just use the deftask macro within project.clj.
Cake is still young, and a work in progress, but it's a very active project.