The Missing Class Phenomenon (or Why the Build Tools are broken)

So you have your cool project, which you now want to move to the clojuresque plugin for Gradle. And suddenly your build is broken, because it cannot find a required class, you generate with gen-class. Building with ant works. So what is going on?

The Old Setup

You have some project with namespace a. It uses some generated class B.

; In B.clj...
(ns B
  (:gen-class))

; In a.clj...
(ns a
  (:import B))

Everything is stitched together with an ant task like this:

<target name="aot">
    <java classname="clojure.lang.Compile" failonerror="true">
        <classpath>
            <path location="${classes.dir}"/>
            <path location="${src.dir}"/>
            <fileset dir="${lib.dir}" includes="**/*.jar"/>
        </classpath>
        <sysproperty key="clojure.compile.path" value="${classes.dir}"/>
        <arg value="B"/>
        <arg value="a"/>
    </java>
</target>

And all was well.

The Move and the Problem

Then you decided for various other reasons to switch to clojuresque. And the build suddenly blows up. „Huh? It can't find class B anymore?“ Yes. And the reason is the incomplete dependency tree.

I'm not sure what happened in the last 10 years in the Java community, but they obviously didn't improve over make. Leting the compiler figure out the dependencies is obviously broken, because it knows only its own language. As soon as you mix languages you are in trouble or you need some super-compiler knowing (the backends for) different languages. The above example is a clear failure even considering only one language: the compiler does not recognise the :import as a dependency[1].

There are tools – like Ant and Gradle – which all claim to be an improvement over make and they all fail miserably. The only build tool superior to make I saw so far is Cook. It is the only one, which goes out of its way to get the dependencies right. It allows easy hooking into the build logic to extract and specify dependencies. The gap are the languages, which do not allow easy extraction of dependencies (as well as the mapping of dependencies to compiler input).

The Solution

To „fix“ our code, we have to complete the dependency graph and tell the compiler, that we actually need B for a. The easiest way to do this is to add B also as a :required lib.

(ns a
  (:require B)
  (:import B))

Well, but maybe I'm just a dinosaur still living in the C world.

Footnotes

  1. In fact it cannot do that at all. One might define arbitrary classes in arbitrary namespaces. How can the compiler know which namespace?

Published by Meikel Brandmeyer on .