Fork me on GitHub

Is there a good way to produce both a library and an uberjar from the same build.clj file? And will it require me to split up my code a little bit to achieve it? Right now I just have src/project/[files].clj and all my code lives in there. Would I need to do something like lib/project/lib.clj and src/project/main.clj?

Alex Miller (Clojure team)02:06:19

it's your program, so it can do whatever you want

Alex Miller (Clojure team)02:06:26

you don't need to split up your code - the library jar would just contain the source and the uberjar would contain your code + your deps (possibly all compiled)


Okay, I’m just trying to figure out how this would work in my deps.edn and build.clj files @U064X3EF3. I’ve got

But I only want the functions in lib.clj to be accessible by importing the library. But they need to be public so I can actually write tests for them, right? So how do I have them public, but inaccessible by a consumer? And for the uberjar, I’ve got that plus
But I don’t want this to be included at all in the library jar.

Alex Miller (Clojure team)03:06:44

if you want to vary which source files get included, then you can choose to copy only a subset of your source into the directory where you each thing. or you could separate those sources. either will work.

Alex Miller (Clojure team)03:06:39

> So how do I have them public, but inaccessible by a consumer In general with Clojure, this is not a thing. All vars are accessible. You can mark functions as private (with defn- or ^:private) to indicate to consumers that they are not part of the public api. It is still possible to test those vars (because they are still accessible via the var).


Ahh, I wasn’t aware that using defn- or the private metadata still allowed them to be accessed via the var. Okay, well that solves that problem.

Alex Miller (Clojure team)03:06:45

you can invoke vars as if they were functions (if they refer to a function, invoking the var invokes the function)


Just one more question… I’m trying to build my uberjar right now using the default build.clj provided by deps-new. It uses rather than the normal . I’m getting this error building though:

Execution error (ClassCastException) at (build.clj:108).
class build$lib cannot be cast to class clojure.lang.Named (build$lib is in unnamed module of loader clojure.lang.DynamicClassLoader @4fa86cb8; clojure.lang.Named is in unnamed module of loader 'app')
And this is the build.clj atm:
(ns build
  (:refer-clojure :exclude [test])
  (:require [ :as bb]))

(def lib 'proj/proj)
(def version "0.1.0-SNAPSHOT")
(def main 'proj.main)

(defn test "Run the tests." [opts]
  (bb/run-tests opts))

(defn uber [opts]
  (println "Building executable uberjar")
  (-> opts
      (assoc :lib lib :main main)

(defn lib [opts]
  (println "Building library jar")
  (-> opts
      (assoc :lib lib :version version)
Do you know what might be the issue here?

Alex Miller (Clojure team)03:06:57

I don't know, but I'd say just use directly. as soon as you're doing anything other than the default thing, I think is going to be in your way, not helping you


Lol okay, I’ll give that a shot then

Alex Miller (Clojure team)03:06:49

I'm not a fan of because it assumes a ton of stuff to make it "easy" but that makes it harder to transition into a custom build (and having that transition available is the whole point of it does help with the test stuff so I'd keep that part.


Okay, so I’ve got it working now using I’ve just got one issue - I have a file in my resources that I’m accessing through io/resource but it’s not being found. I’m getting

java.lang.IllegalArgumentException: Cannot open <nil> as a Reader.
	at $fn__11544.invokeStatic(io.clj:288)
	at $fn__11544.invoke(io.clj:288)
	at $fn__11446$G__11422__11453.invoke(io.clj:69)
	at $reader.invokeStatic(io.clj:102)
	at $reader.doInvoke(io.clj:86)
	at clojure.lang.RestFn.invoke(
	at clojure.lang.AFn.applyToHelper(
	at clojure.lang.RestFn.applyTo(
	at clojure.core$apply.invokeStatic(core.clj:669)
	at clojure.core$slurp.invokeStatic(core.clj:6944)
	at clojure.core$slurp.doInvoke(core.clj:6944)
	at clojure.lang.RestFn.invoke(


I have my resources directory included in my :src-dirs when calling compile-clj

Alex Miller (Clojure team)04:06:05

You also need to copy them into the classes dir, just like your src dir

Alex Miller (Clojure team)04:06:41

compile-clj just makes the classes


Ahah, there we go. Thank you so much! Everything appears to be working now then. Just going to add the stuff back in to run the tests as well and then it’s all good.

Alex Miller (Clojure team)04:06:00

You can also use cognitect-labs.test-runner directly too - it has a function style api


I’ll check that out, thanks!


Man, 2 years and I thought it was cognitech… Then I get an error that the namespace can’t be found and I had to look reeeeeal close


@U01BH40EA0Z Your initial error was here:

(defn lib [opts]
  (println "Building library jar")
  (-> opts
      (assoc :lib lib :version version)
You're shadowing the lib top-level def so you're passing your lib function in as :lib.


Hence "class build$lib cannot be cast to class clojure.lang.Named" -- your function value cannot be cast to a symbol or keyword.


A possibly important difference between using the Cognitect test runner directly in your script vs using my wrapper is that my wrapper runs tests in a subprocess so your code is isolated from any dependencies that, tools.deps.alpha, etc bring into the process running the build. A lot of times that won't matter but it could matter and I prefer process isolation when running tests (Polylith takes an interesting approach with test running because it creates isolated classloaders for running tests, rather process isolation, but that's a lot of work).


:face_palm: yup, that makes a lot of sense, can't believe I missed that! Thanks @U04V70XH6!