What's the difference between Cake and Leiningen?
Leiningen is a build tool and dependency manager for Clojure which includes the ability to set up an interactive REPL (or swank server) with the appropriately configured classpath.
Cake is in many ways similar (indeed the project.clj file that describes your projects is in the same format as Leiningen's, and both Cake and Leiningen transparently utilize Maven for dependency management).
A big difference is that Cake sets up persistent JVMs that continue to run in the background. This means that you don't have to pay the price of starting up a fresh JVM every time you issue a cake command.
The convenience of persistent JVMs is achieved by Cake being quite a bit more complicated under the hood. The "cake" command is actually a Ruby script that communicates over sockets to the JVM(s) it starts up.
Both Cake and Leiningen are actually very easy to set up and get running.
If you are looking for a standalone Clojure development environment (with or without emacs as the editor) to get up and running with Clojure quickly, I would personally recommend Cake. If you will be integrating with other IDEs then the simpler Leiningen is probably a better bet.
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.