For those of you with experience with Maven, you might be wondering why anyone who is using Leiningen to build a project would then want to run that build tool from Maven, which is itself another build tool. There is a reason why I even ventured down this path. I would like to share what I have found so far, in case it benefits anyone else, but I would also like to get feedback from people who know of a better way of accomplishing the same goals.
Original Goal
I was doing some work on a work-related side project for which I need to run code on the JVM. I could write the code in Java, but it would be painfully tedious to do so. There is no real Scala alternative, but the Clojure alternative shines brightly due to its simplicity. Anything worth doing must be repeatable as well as maintainable, and to put a long story short, I need (needed & still need) to be able to create an uberjar as a part of my build process.
All I wanted to do is to be able to compile my code via Maven into an uberjar and run the uberjar on the target computing system. But as it turned out, a Maven-compiled uberjar wouldn’t run, but a Leiningen-compiled uberjar ran without a hitch. And I need to be able to run the compilation process via Maven so that our build process is still done through Maven.
If Leiningen, Why Maven?
If I can create the ubejrar via Maven, I can keep my Clojure work largely to myself. If not, it might require having to install Leiningen on build servers, and that requires getting people at work to help in supporting Leiningen. Installing Leiningen, as well as pegging it to a specific version via lein upgrade [version]
, are simple self-contained operations, but try explaining that to people who heard the same things said about SBT back when it was called Simple Build Tool and are thus now stuck supporting a very entangled build setup. I’d rather avoid a conversation about Leiningen until the climate becomes more conducive to have it.
It’s one thing to slip Clojure into a Java project. Clojure is just a Java library that you can fetch from Maven Central. It’s another thing to slip a Leiningen build into a Maven build. But hey, Leiningen is just a Java library that you can fetch from Clojars…
Building with Maven is not a problem, because Maven supports plugins, and plugins exist to enable many things, including the compilation of JVM languages in a manner that is Maven-friendly. For Clojure, clojure-maven-plugin is indispensible. By enabling the compilation of Clojure via Maven, you can break up a Clojure project into sub-projects and sub-(sub-)*-projects while managing dependencies between the sub-projects as desired. Maven does a pretty good job in this regard, and I don’t see an easy way in Leiningen (via existing plugins) for a project to specify its dependent sub-projects, and for build properties to be inherited by those subprojects. When it comes to using clojure-maven-plugin, I have had success — I have been able to build a Compojure/Ring web app as described in the book Clojure Programming and have the .war file deploy without a hitch on a Java web server. I have also been able to build a Spark uberjar using the Flambo wrapper library and deploy the jar on Spark, even after breaking up the code into separate sub-projects based on areas of concern. And much of the source code clojure and clojure contrib libraries is built via Maven using clojure-maven-plugin, I believe. So far, so good.
The Original Problem?
Now, for the particular side project that I’m working on now, much like with Spark via Flambo, I need to create an uberjar and deploy it. The system on which I need to deploy the uberjar seems to be doing some ‘interesting’ behind-the-scenes introspection on the uberjar before it gets launched. The rub is that when I take my Maven-compiled uberjar and run it, I end up getting issues that are related to the classloader. There’s something related to Proxy and Classloader to do some checking of types of methods in a subclassed Interface.** In other incarnations of the code, certain classes representing functions inside a Clojure (proxy [] …) instance were not being found. And I don’t really understand the situation beyond that.
The Workaround
Since I didn’t really understand the nature of the problem, and needed to quickly find a way around this problem, I tried to find whatever seemed relevant. There are some other Clojure compiler plugins for Maven, but they’re incomplete and un-maintained for 4+ years. There seems to be a Leiningen plugin for Maven, but it ultimately seems to require lein be installed and in the path of the machine that Maven is running on, and it requires you to compile the plugin.
So the last workaround attempt centered around Leiningen being a Java library that can be called as a standalone executable jar from the command line. And Maven has a plugin to runnning an external process in the middle of a build — maven-exec-plugin. That plugin has 2 goals: running a Java process (mvn exec:java), and running any ol’ external process (mvn exec:exec). I could not get exec:java to run Leiningen successfully — again, I didn’t quite understand why calling Leiningen this way didn’t work. I even created a build.clj script that called Leiningen in turn. But what finally did work was resorting to using the exec:exec goal, where the program called at the command-line is java, and I used the plugin’s facilities to supply the project’s classpath to java’s command-line options.
Since I have Maven calling Leiningen, I need both a pom.xml file and a project.clj file. I reproduce them below.
project.clj:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defproject mvn-lein-test "0.1.0-SNAPSHOT" | |
:description "FIXME: write description" | |
:url "http://example.com/FIXME" | |
:license {:name "Eclipse Public License" | |
:url "http://www.eclipse.org/legal/epl-v10.html"} | |
:dependencies [[org.clojure/clojure "1.7.0"] | |
[medley "0.7.0"] | |
[leiningen "2.5.3"]] | |
:main mvnleintest.core | |
:aot :all | |
:source-paths ["src/main/clojure"] | |
:test-paths ["src/test/clojure"]) |
pom.xml:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<project> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>com.mycompany.app</groupId> | |
<artifactId>my-app</artifactId> | |
<version>1</version> | |
<repositories> | |
<repository> | |
<id>clojars</id> | |
<name>Clojars</name> | |
<url>http://clojars.org/repo</url> | |
</repository> | |
</repositories> | |
<dependencies> | |
<dependency> | |
<groupId>org.clojure</groupId> | |
<artifactId>clojure</artifactId> | |
<version>1.7.0</version> | |
</dependency> | |
<dependency> | |
<groupId>leiningen</groupId> | |
<artifactId>leiningen</artifactId> | |
<version>2.5.3</version> | |
</dependency> | |
</dependencies> | |
<pluginRepositories> | |
<pluginRepository> | |
<id>clojars</id> | |
<name>Clojars</name> | |
<url>http://clojars.org/repo</url> | |
</pluginRepository> | |
</pluginRepositories> | |
<build> | |
<plugins> | |
<plugin> | |
<groupId>org.codehaus.mojo</groupId> | |
<artifactId>exec-maven-plugin</artifactId> | |
<version>1.4.0</version> | |
<executions> | |
<execution> | |
<id>lein-clean</id> | |
<phase>clean</phase> | |
<goals> | |
<goal>exec</goal> | |
</goals> | |
<configuration> | |
<executable>java</executable> | |
<arguments> | |
<argument>-classpath</argument> | |
<classpath/> | |
<argument>clojure.main</argument> | |
<argument>-m</argument> | |
<argument>leiningen.core.main</argument> | |
<argument>clean</argument> | |
</arguments> | |
</configuration> | |
</execution> | |
<execution> | |
<id>lein-test</id> | |
<phase>test</phase> | |
<goals> | |
<goal>exec</goal> | |
</goals> | |
<configuration> | |
<executable>java</executable> | |
<arguments> | |
<argument>-classpath</argument> | |
<classpath/> | |
<argument>clojure.main</argument> | |
<argument>-m</argument> | |
<argument>leiningen.core.main</argument> | |
<argument>test</argument> | |
</arguments> | |
</configuration> | |
</execution> | |
<execution> | |
<id>lein-compile</id> | |
<phase>compile</phase> | |
<goals> | |
<goal>exec</goal> | |
</goals> | |
<configuration> | |
<executable>java</executable> | |
<arguments> | |
<argument>-classpath</argument> | |
<classpath/> | |
<argument>clojure.main</argument> | |
<argument>-m</argument> | |
<argument>leiningen.core.main</argument> | |
<argument>compile</argument> | |
</arguments> | |
</configuration> | |
</execution> | |
<execution> | |
<id>pkg-lein-clean</id> | |
<phase>package</phase> | |
<goals> | |
<goal>exec</goal> | |
</goals> | |
<configuration> | |
<executable>java</executable> | |
<arguments> | |
<argument>-classpath</argument> | |
<classpath/> | |
<argument>clojure.main</argument> | |
<argument>-m</argument> | |
<argument>leiningen.core.main</argument> | |
<argument>clean</argument> | |
</arguments> | |
</configuration> | |
</execution> | |
<execution> | |
<id>pkg-lein-uberjar</id> | |
<phase>package</phase> | |
<goals> | |
<goal>exec</goal> | |
</goals> | |
<configuration> | |
<executable>java</executable> | |
<arguments> | |
<argument>-classpath</argument> | |
<classpath/> | |
<argument>clojure.main</argument> | |
<argument>-m</argument> | |
<argument>leiningen.core.main</argument> | |
<argument>uberjar</argument> | |
</arguments> | |
</configuration> | |
</execution> | |
</executions> | |
</plugin> | |
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-jar-plugin</artifactId> | |
<version>2.4</version> | |
<executions> | |
<execution> | |
<id>default-jar</id> | |
<phase/> | |
</execution> | |
</executions> | |
</plugin> | |
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-install-plugin</artifactId> | |
<version>2.4</version> | |
<configuration> | |
<skip>true</skip> | |
</configuration> | |
</plugin> | |
</plugins> | |
</build> | |
</project> |
So there you have it. To explain the pom.xml file a little bit, there are executions configured, each of which uses the exec:exec goal insted of the exec:java goal. The executions are attached to different phases of the Maven build cycle. I chose to only consider clean, test, compile, and package since those are the ones that I actually use regularly and whose behavior should be handed off by Maven to Leiningen (since they are related to compilation). Both Maven and Leiningen execute phases in succession in an additive way — a test build target triggers a compile phase followed by a test phase. A package build target first triggers a compile phase and a test phase, in that order, before doing the package phase. So in this setup, I inserted an extra ‘lein clean’ in the Maven package phase to prevent errors that were happening calling ‘lein uberjar’ after effectively calling ‘lein compile’ 3 times right beforehand. (Again, I’m not sure why there would be errors, there.) Since I deferred to Leiningen to build jar artifacts, I overrode the mechanisms in Maven that build the jar and install it locally to not run. If I need to install a jar, then I can call out to Leiningen to do so just as before for clean, test, etc.
I know that it looks and feels dirty to hack up this Maven-Leiningen interaction. Hopefully, you never are never in the position to need it, but there it is, just in case.
** The original problem’s stacktrace looked something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2015-12-27 00:22:56,112 DEBUG com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory – Provided Arguments: {} | |
Exception in thread "main" java.lang.IllegalArgumentException: interface TestWordCountOptions is not visible from class loader | |
at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:581) | |
at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:557) | |
at java.lang.reflect.WeakCache$Factory.get(WeakCache.java:230) | |
at java.lang.reflect.WeakCache.get(WeakCache.java:127) | |
at java.lang.reflect.Proxy.getProxyClass0(Proxy.java:419) | |
at java.lang.reflect.Proxy.getProxyClass(Proxy.java:371) | |
at com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory.validateWellFormed(PipelineOptionsFactory.java:587) | |
at com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory.parseObjects(PipelineOptionsFactory.java:1242) | |
at com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory.access$400(PipelineOptionsFactory.java:99) | |
at com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory$Builder.as(PipelineOptionsFactory.java:284) | |
at datasplash.core$make_pipeline.invoke(core.clj:661) | |
at gcptest2.core$get_word_count_pipeline.invoke(core.clj:39) | |
at gcptest2.core$run_dataflow_job.invoke(core.clj:72) | |
at gcptest2.core$_main.doInvoke(core.clj:81) | |
at clojure.lang.RestFn.invoke(RestFn.java:397) | |
at clojure.lang.AFn.applyToHelper(AFn.java:152) | |
at clojure.lang.RestFn.applyTo(RestFn.java:132) | |
at gcptest2.core.main(Unknown Source) |
2 replies on “Compiling a Leiningen Project from Maven”
So now do you maintain the `pom.xml` and the `project.clj` in parallel (by hand)?
I’m looking for a way to write my `project.clj` so that `lein pom` will generate the correct `pom.xml` each time (so I don’t have to maintain the two sources in parallel.)
[…] 처음에는 executable jar 생성에 많이 사용되는 maven-share-plugin을 사용했는데 플러그인 세팅의 문제로 진입점을 찾지 못하는 것으로 생각하여 여러가지로 시도해보았지만 실패하고, maven-assembly-plugin도 사용하는 방법도 써봤지만 재미를 보지 못하다 결국 방법을 찾았다. (참고링크: http://www.elangocheran.com/blog/2015/12/compiling-a-leiningen-project-from-maven/) […]