views:

916

answers:

3

What's the difference between Cake and Leiningen?

+10  A: 

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.

Alex Stoddard
+13  A: 

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.

technomancy
Would you care to provide an example? I am not very familiar with either Leiningen or Cake, and would be interested to see an illustration of the differentiation you are describing between the two.
A. Levy
When tasks are just functions, you can extend them in much more interesting ways. Witness this Leiningen plugin: http://github.com/technomancy/rodney-leonard-stubbs Cake's task extension only allows for appending/prepending side-effects, while Leiningen's allows rebinding, altering arguments/return value, and conditional execution as well.
technomancy
cake tasks can contain function calls as well, supporting all of these features
lancepantz
+8  A: 

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.

lancepantz
Well, there is also "lein interactive" which lets you run persistent lein without overhead of java startup time for each lein command.
Dev er dev
The four-task example is quite contrived. It's done that way in make because make doesn't have functions, so everything has to be a task. But you'd never actually break up your tasks that way with Leiningen; it just doesn't make sense. You never see tasks that are that short; there are always at least 2 or 3 support functions in that task's namespace in practice.
technomancy
The conciseness objection to Robert Hooke is also very silly; I implemented a prepend macro in four lines to do what Cake dependencies do. The hardest part about it was choosing a name for the macro.
technomancy
Dev er dev: In the long term, the persistence doesn't matter. There have been discussions of a adding a persistent command server and repl to clojure itself, and we are working on factoring that piece out into a separate library. Chas Emerick's nREPL project is also a promising solution. It's a very useful feature, and hopefully cake, lein, and any clojure project can all use the same solution for it one day.
lancepantz
technomancy: ofcourse it's a contrived example! no tasks would ever just print lines :) Actually, the example actually comes from the MF rake article I cited, and rake does have functions. The example did not come from make. The point is that if you want a function to be an invokable task and a dependency of another task in lein, you *have* to break up your tasks that way. Indeed it does not make sense, that's why we created cake. If i'm doing something wrong, please post a better example using lein with invokable tasks as dependencies. It's a very useful requirement.
lancepantz
re the silly hooke objection: I mentioned a macro would be required as, you suggested, before posting. It would require your macro, no matter how trivial it may be to be as concise. I don't think that's a silly point.
lancepantz