It is common in enterprise environments to have medium-sized codebases written in both Java and Clojure. Both Maven and Leiningen produce JAR packages with POM files and provide internal mechanisms for managing project-specific dependencies.

Challenges arise when you try to integrate Maven and Leiningen into a reliable, reproducible build workflow. There is no native way to perform incremental builds that leverage both tools. Naturally, teams end up with shell scripts that parse POM files to discover dependencies and call Leiningen or Maven to build and publish JAR files to the local repository.

Mature projects such as Bazel are designed specifically to solve builds for polyglot codebases, and they may be a better fit for large projects. We chose GNU Make for its simplicity and lack of extra dependencies. GNU Make is preinstalled on most GNU/Linux and macOS environments.

People unfamiliar with GNU Make may feel overwhelmed when they see sophisticated Makefiles used to incrementally build large codebases in C or C++, because of the macros and compact idioms used in advanced Makefiles. But the basic structure of a Makefile is simple. The main building block of a Makefile is a rule. A rule consists of a target, prerequisites, and a recipe.

VER := 1.2.3
ORG_PATH := com/org
M2_REPO := $(HOME)/.m2/repository

$(M2_REPO)/$(ORG_PATH)/foo-clj/$(VER)/foo-clj-$(VER).jar: \
    $(M2_REPO)/$(ORG_PATH)/bar-clj/$(VER)/bar-clj-$(VER).jar \
    $(M2_REPO)/$(ORG_PATH)/jfoo/$(VER)/jfoo-$(VER).jar
	cd foo-clj && lein install

$(M2_REPO)/$(ORG_PATH)/bar-clj/$(VER)/bar-clj-$(VER).jar:
	cd bar-clj && lein install

$(M2_REPO)/$(ORG_PATH)/jfoo/$(VER)/jfoo-$(VER).jar:
	mvn install -f jfoo

In this abstract Makefile, we declare the target foo-clj-1.2.3.jar in the local Maven repository, which depends on bar-clj-1.2.3.jar and jfoo-1.2.3.jar being present in the local repository. GNU Make will check that the prerequisites exist and are not stale before running cd foo-clj && lein install. If the prerequisites are not met, GNU Make will try to create them based on the rules it has.

This can be simplified with a function to reduce repetition:

VER := 1.2.3
ORG_PATH := com/org
M2_REPO := $(HOME)/.m2/repository

jar = $(M2_REPO)/$(ORG_PATH)/$(1)/$(VER)/$(1)-$(VER).jar

$(call jar,foo-clj): $(call jar,bar-clj) $(call jar,jfoo)
	cd foo-clj && lein install

$(call jar,bar-clj):
	cd bar-clj && lein install

$(call jar,jfoo):
	mvn install -f jfoo

Conclusion

GNU Make turned out to be a good, simpler alternative to more advanced build systems such as Bazel. It is worth investing a little time to become familiar with GNU Make for Java and Clojure developers.

It can take some time initially to write basic target rules for your system, but after that, maintenance and extension are straightforward. Some useful qualities of GNU Make are:

  • Incremental builds: Only outdated targets are built, which enables efficient builds.
  • Multiple jobs: You can specify -j [jobs] or --jobs[=jobs] to speed up builds on multicore machines.

Documentation