Cambada
Cambada is a packager for Clojure based on deps.edn
(AKA tools.deps
). It is
heavily inspired by Leiningen's jar and uberjar tasks and also supports
GraalVM's new native-image making it a one-stop shop for any packaging needed
for your Clojure project.
Motivation
Leiningen has laid the foundations of what many of us have come to accept as the
standard for Clojure projects. Clojure's tools.deps
potentially brings new
ideas to the Clojure workflow. Cambada brings some of the great features of
Leiningen to the tools.deps
workflow.
Cambada's sole focus is packaging. It doesn't have plugins, templates or Clojars
integration. It packages your deps.edn
progject as one - or all - of:
- jar
- uberjar
- GraalVM native image
On top of Phil Hagelberg's (and so many others') great Leiningen, many thanks to
Dominic Monroe and his work on pack
as well as Taylor Wood and his
clj.native-image
. These projects offered a lot of inspiration (and, in some
cases, donor code too).
Table of Contents
- Getting Started
- Easy Aliases
- Packaging as a Jar
- Packaging as an Uberjar
- Packaging as a Native Image
- Caveats
- Performance Comparison
- Bugs
- Help!
Getting Started
Cambada is a simple set of main functions that can be called from a deps.edn
alias. The simplest way to have it available in your project is to add an alias
with extra-deps
to your deps.edn
file:
{:aliases {:cambada
{:extra-deps
{luchiniatwork/cambada
{:mvn/version "1.0.5"}}}}}
Cambada has three main entry points, cambada.jar
, cambada.uberjar
and
cambada.native-image
. Let's say you simply want to create an uberjar:
$ clj -R:cambada -m cambada.uberjar
Cleaning target
Creating target/classes
Compiling ...
Creating target/project-name-1.0.0-SNAPSHOT.jar
Updating pom.xml
Creating target/project-name-1.0.0-SNAPSHOT-standalone.jar
Including ...
Done!
Your files will be located at target/
by default.
All entry points have a few extra configuration options you might be interested in. For instance:
$ clj -R:cambada -m cambada.uberjar --help
Package up the project files and all dependencies into a jar file.
Usage: clj -m cambada.uberjar [options]
Options:
-m, --main NS_NAME The namespace with the -main function
--app-group-id STRING project-name Application Maven group ID
--app-artifact-id STRING project-name Application Maven artifact ID
--app-version STRING 1.0.0-SNAPSHOT Application version
--[no-]copy-source Copy source files by default
-a, --aot NS_NAMES all Namespaces to be AOT-compiled or `all` (default)
-d, --deps FILE_PATH deps.edn Location of deps.edn file
-o, --out PATH target Output directory
-h, --help Shows this help
Do try --help
for cambada.jar
and cambada.native-image
if you are
interested or refer to the sections below.
Easy Aliases
One of the powers-in-simplicity of tools.deps
is the ability to define aliases
on deps.edn
. When we used the alias cambada
on the section above, we simply
specified it as an dependency to be resolved (therefore the -R
when calling
clj
).
You can also be a lot more prescriptive in your aliases by making them do more work for you. For instance, the alias below will create a versioned uberjar:
{:aliases {:uberjar
{:extra-deps
{luchiniatwork/cambada {:mvn/version "1.0.0"}}
:main-opts ["-m" "cambada.uberjar"
"--app-version" "0.5.3"]}}}
By having an alias like this uberjar
one in your deps.edn
you can simply run
it by using $ clj -A:uberjar
making it very familiar to those used with $ lein uberjar
:
$ clj -A:uberjar
Cleaning target
Creating target/classes
Compiling ...
Creating target/project-name-0.5.3.jar
Updating pom.xml
Creating target/project-name-0.5.3-standalone.jar
Including ...
Done!
Packaging as a Jar
Let's start with an empty project folder:
$ mkdir -p myproj/src/myproj/
$ cd myproj
Create a deps.edn
at the root of your project with cambada.jar
as an alias:
{:aliases {:jar
{:extra-deps
{luchiniatwork/cambada {:mvn/version "1.0.2"}}
:main-opts ["-m" "cambada.jar"
"-m" "myproj.core"]}}}
Create a simple hello world on a -main
function at src/myproj/core.clj
:
(ns myproj.core
(:gen-class))
(defn -main [& args]
(println "Hello World!"))
Of course, just for safe measure, let's run this hello world via clj
:
$ clj -m myproj.core
Hello World!
Then just call the alias from the project's root:
$ clj -A:jar
Cleaning target
Creating target/classes
Compiling myproj.core
Creating target/myproj-1.0.0-SNAPSHOT.jar
Updating pom.xml
Done!
Once Cambada is done, you'll have a jar package at target/
. In order to run
it, you'll need to add Clojure and spec to your class path. The paths will vary
on your system:
$ java -cp target/myproj-1.0.0-SNAPSHOT.jar myproj.core
Hello World!
For a standalone jar file see the uberjar option on the next section.
You can specify the following options for cambada.jar
:
-m, --main NS_NAME The namespace with the -main function
--app-group-id STRING project-name Application Maven group ID
--app-artifact-id STRING project-name Application Maven artifact ID
--app-version STRING 1.0.0-SNAPSHOT Application version
--[no-]copy-source Copy source files by default
-a, --aot NS_NAMES all Namespaces to be AOT-compiled or `all` (default)
-d, --deps FILE_PATH deps.edn Location of deps.edn file
-o, --out PATH target Output directory
-h, --help Shows this help
These options should be quite self-explanatory and the defaults are hopefully sensible enough for most of the basic cases. By default everything gets AOT-compiled and sources are copied to the resulting jar.
For those used to Leiningen, the application's group ID, artifact ID
and version are not extracted from project.clj
(since it's assumed
you don't have a project.clj
in a deps.edn
workflow). Therefore,
you must specify these expressively as options.
Packaging as an Uberjar
Let's start with an empty project folder:
$ mkdir -p myproj/src/myproj/
$ cd myproj
Create a deps.edn
at the root of your project with cambada.jar
as an alias:
{:aliases {:uberjar
{:extra-deps
{luchiniatwork/cambada {:mvn/version "1.0.0"}}
:main-opts ["-m" "cambada.uberjar"
"-m" "myproj.core"]}}}
Create a simple hello world on a -main
function at src/myproj/core.clj
:
(ns myproj.core
(:gen-class))
(defn -main [& args]
(println "Hello World!"))
Of course, just for safe measure, let's run this hello world via clj
:
$ clj -m myproj.core
Hello World!
Then just call the alias from the project's root:
$ clj -A:uberjar
Cleaning target
Creating target/classes
Compiling myproj.core
Creating target/myproj-1.0.0-SNAPSHOT.jar
Updating pom.xml
Creating target/myproj-1.0.0-SNAPSHOT-standalone.jar
Including myproj-1.0.0-SNAPSHOT.jar
Including clojure-1.9.0.jar
Including spec.alpha-0.1.143.jar
Including core.specs.alpha-0.1.24.jar
Done!
Once Cambada is done, you'll have two jar packages at target/
. One for a basic
jar and one standalone with all dependencies in it. In order to run it, simply
call it:
$ java -jar target/myproj-1.0.0-SNAPSHOT-standalone.jar
Hello World!
cambada.uberjar
has exactly the same options and defaults as
cambada.jar
(see above for more details).
Caveats
If any of your transitive dependencies has a Maven Central dependency,
cambada
may fail on you (investigations under way). Therefore, it is
recommended that you explicitly add your repos (Central included) to
your deps.edn
file i.e.:
{:deps {...}
:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
"clojars" {:url "https://repo.clojars.org/"}}}
Packaging as a Native Image
By using GraalVM we now have the option of packaging everything AOT compiled as a native image.
If you want to use this feature, make sure to download and install GraalVM.
If you are a MacOS user, GraalVM CE is available as a brew cask:
$ brew cask install graalvm/tap/graalvm-ce
GraalVM's native-image
is a package that needs to be installed
manually with the following command (attention that gu
is at
$GRAALVM_HOME/bin/
if it is not on your PATH
):
$ gu install native-image
You will need to set your GRAALVM_HOME
environment variable to point
to where GraalVM is installed. Alternatevely you can call
cambada.native-image
with the argument --graalvm-home
pointing to it.
The entry point for native image packaging is
cambada.native-image
. Let's assume your GRAALVM_HOME
variable is
set (if you don't, use --graalvm-home
).
Let's start with an empty project folder:
$ mkdir -p myproj/src/myproj/
$ cd myproj
Create a deps.edn
at the root of your project with cambada.jar
as an alias:
{:aliases {:native-image
{:extra-deps
{luchiniatwork/cambada {:mvn/version "1.0.0"}}
:main-opts ["-m" "cambada.native-image"
"-m" "myproj.core"]}}}
Create a simple hello world on a -main
function at src/myproj/core.clj
:
(ns myproj.core
(:gen-class))
(defn -main [& args]
(println "Hello World!"))
Of course, just for safe measure, let's run this hello world via clj
:
$ clj -m myproj.native-image
Hello World!
Then just call the alias from the project's root:
$ clj -A:native-image
Cleaning target
Creating target/classes
Compiling myproj.core
Creating target/myproj
classlist: 2,810.07 ms
(cap): 1,469.31 ms
setup: 2,561.28 ms
(typeflow): 5,802.45 ms
(objects): 2,644.17 ms
(features): 40.54 ms
analysis: 8,609.18 ms
universe: 314.28 ms
(parse): 1,834.84 ms
(inline): 2,338.45 ms
(compile): 16,824.24 ms
compile: 21,435.77 ms
image: 1,862.44 ms
write: 1,276.55 ms
[total]: 38,942.48 ms
Done!
Once Cambada is done, you'll have an executable package at target/
:
$ ./target/myproj
Hello World!
Extra options can be sent to GraalVM's packager by using Cambada's
--graalvm-opt
option i.e., to include FILE
as a resource, simply
use --graalvm-opt H:IncludeResources=FILE
.
Performance Comparison
A quick comparison of the myproj
hello world as described previously and ran
across different packaging options:
Straight with clj
:
$ time clj -m myproj.core
Hello World!
1.160 secs
As a standalone uberjar:
$ time java -jar target/myproj-1.0.0-SNAPSHOT-standalone.jar
Hello World!
0.850 secs
As a native image:
$ time ./target/myproj
Hello World!
0.054 secs
Comparing with clj
as a baseline:
Method | Speed in secs | Speed relative to clj |
---|---|---|
clj |
1.160 secs |
1x |
uberjar |
0.850 secs |
1.36x |
native-image |
0.054 secs |
21.48x |
Bugs
If you find a bug, submit a Github issue.
Help
This project is looking for team members who can help this project succeed! If you are interested in becoming a team member please open an issue.
License
Copyright © 2018 Tiago Luchini
Distributed under the MIT License. See LICENSE