tools-build

flowthing 2022-02-04T13:25:34.220009Z

I'm migrating a project from Leiningen to deps.edn and tools.build. In my project.clj, there's this:

:profiles {:dev {:aot [db.migration.migrations]}}
My (possibly incorrect) understanding is that this tells Leiningen to AOT-compile db.migration.migrations such that the generated classes are in the classpath for REPL use. In other words, if db.migration.migrations changes, I don't have to manually recompile it before running lein repl — Leiningen will take care of that for me. With tools.build, I can define a task like this:
(defn compile-migrations
  [_]
  (build/compile-clj
    {:src-dirs ["src"]
     :basis basis
     :ns-compile ['db.migration.migrations]
     :filter-nses ['db.migration.migrations]
     :class-dir "classes"}))
Then do clj -T:build compile-migrations before starting a REPL (with "classes" in the classpath) to make the classes available for dev. This does mean that I need to run clj -T:build compile-migrations manually whenever db.migration.migrations changes, though. That's not a big deal, but I just wanted to make sure my understanding's correct and that this is indeed the way to do this with tools.build.

borkdude 2022-02-04T13:46:55.795799Z

@flowthing why AOT-compile db-migrations at all for dev?

flowthing 2022-02-04T13:49:54.870709Z

These migrations generate a bunch of Flyway classes via gen-class. I don't think there's an alternative to AOT-compiling them?

borkdude 2022-02-04T13:50:14.554029Z

Ah I see!

borkdude 2022-02-04T13:50:24.347299Z

I guess you can always call compile manually from the REPL if those sources change

flowthing 2022-02-04T13:50:52.745759Z

Right, but I'll have to restart the REPL after that, though.

borkdude 2022-02-04T13:50:59.274029Z

yeah

borkdude 2022-02-04T13:51:18.138429Z

I guess you could put this in your user.clj

borkdude 2022-02-04T13:52:43.830499Z

(ns user)
(binding [*compile-path* "target/classes"]
  (compile 'db.migrations))

flowthing 2022-02-04T13:52:50.751249Z

That was my first thought, but I'm not sure that there's a benefit over making a tools.build task that you need to run beforehand, since you need to restart the REPL anyway.

flowthing 2022-02-04T13:53:23.116399Z

Also, :filter-nses is useful here, since I really don't want to AOT-compile anything except db.migration. migrations for dev.

borkdude 2022-02-04T13:53:26.576849Z

the benefit would be preventing starting up 2 JVMs, so it will likely save 5 seconds or so of startup time

borkdude 2022-02-04T13:53:52.826929Z

tools build runs in another JVM and then starts another JVM for compiling clj and then you start another JVM for your REPL

borkdude 2022-02-04T13:53:57.545009Z

which can be a bit slow

flowthing 2022-02-04T13:54:01.304879Z

But I only need to run the compile-migrations task (and spin up the second JVM) if the migrations change.

flowthing 2022-02-04T13:54:09.824119Z

No need to run it every time I start a REPL.

borkdude 2022-02-04T13:55:06.600569Z

Personally I would make a bb.edn with a bb dev task which checks if sources have changed using fs/modified-since which would then invoke tools build if necessary and then invokes your REPL

borkdude 2022-02-04T13:55:44.776079Z

but you can do something similar in your user.clj as well

borkdude 2022-02-04T13:57:15.385789Z

why use tools.build for this if you can just compile from the same dev REPL process, it's much cheaper to do so. (compile 'db.migration) is the same as :ns-compile '[db.migration]. Compilation is always a transitive process: all transitively required namespace will also be compiled, no matter what filter-nses is.

flowthing 2022-02-04T13:57:53.778649Z

That's an interesting thought, although I'm not sure I want to bring in Babashka just for that. Also, there are many ways to start a REPL, so it's not something I'd be willing to bake into something like bb dev.

borkdude 2022-02-04T13:58:29.901039Z

Sure, whatever tool you use to start the REPL (make, bash, manual invocation), it's conceptually the same idea: you need to do something, but only sometimes, before starting a REPL. Or you can do that work inside your REPL.

flowthing 2022-02-04T14:02:31.859649Z

> why use tools.build for this if you can just compile from the same dev REPL process, it's much cheaper to do so. I don't know that it's cheaper since I need to restart the REPL anyway?

borkdude 2022-02-04T14:02:56.858419Z

it's cheaper in terms of how much work is done. you're invoking 3 JVMs instead of just 1 REPL JVM.

borkdude 2022-02-04T14:03:52.048429Z

but if that's not a problem, go for it ;)

flowthing 2022-02-04T14:04:12.045629Z

Well, 3 vs. 2. 🙂 I'm not sure I'm convinced that the REPL approach will take up less time.

borkdude 2022-02-04T14:04:24.623349Z

3 vs 1. Why do you think there's 2?

flowthing 2022-02-04T14:04:32.082529Z

Need to restart the REPL.

borkdude 2022-02-04T14:04:56.338969Z

Oh, then it's 4 vs 2.

flowthing 2022-02-04T14:05:30.607909Z

1. Start REPL. 2. Eval (compile 'db.migration.migrations). Class files appear in classes/. 3. Shut down REPL. 4. Start new REPL with classes in classpath.

borkdude 2022-02-04T14:06:07.378409Z

No :) Restart REPL. user.clj: check if sources have changed, then call compile then load the rest of your app. Just 1.

flowthing 2022-02-04T14:20:22.218489Z

Oof. 🙂

flowthing 2022-02-04T14:20:43.340899Z

All right, I have no idea what's going on anymore...

borkdude 2022-02-04T14:22:20.083719Z

To keep it simple, I think just a (compile 'db.migrations) in your user.clj as the first thing you do (regardless if anything's changed), will be sufficient.

borkdude 2022-02-04T14:22:27.443119Z

When you change source, just restart your REPL and that's it.

borkdude 2022-02-04T14:22:58.417979Z

Do not require any other namespaces from your app prior to that

flowthing 2022-02-04T14:23:22.937629Z

Oh, right. That might be what's messing things up.

flowthing 2022-02-04T14:23:49.885209Z

Just to be clear: do you mean having (compile 'db.migration.migrations) at the top level of user.clj?

borkdude 2022-02-04T14:23:59.176259Z

yes.

flowthing 2022-02-04T14:24:31.582969Z

Caused by: java.lang.RuntimeException: *compile-path* not set

flowthing 2022-02-04T14:24:42.637599Z

If I do that.

borkdude 2022-02-04T14:24:48.280879Z

(binding [*compile-path* "target/classes"] (compile '...))

flowthing 2022-02-04T14:25:09.745389Z

Oh, I see. 👍 Let me try that.

borkdude 2022-02-04T14:25:20.977579Z

or whatever your classes dir is.

flowthing 2022-02-04T14:25:42.203939Z

Yep.

flowthing 2022-02-04T14:35:10.209049Z

Cool, that seems to work! So now I have this:

(binding [*compile-path* "classes"]
  (compile 'db.migration.migrations))

(ns user (:require ,,,))

;; etc
I just need to ensure that classes/ exists. I guess the reason I'm confused is that I feel quite certain I tried a variant of this and couldn't get it to work, but maybe the key is having the (compile ,,,) call at the top level before the ns form? Another (entirely plausible) option is that I made a stupid mistake previously that left me under the impression that I would need to restart the REPL after compiling for the changes to take effect.

borkdude 2022-02-04T14:36:09.140489Z

You can also write:

(ns user)

(binding [*compile-path* "classes"]
  (compile 'db.migration.migrations))

;; rest of your user.clj
(require ')

borkdude 2022-02-04T14:36:48.445559Z

as an optimization you could do a check with fs/modified-since or similar to check if compilation is necessary, but I assume compilation itself doesn't take that long that it makes a huge difference

flowthing 2022-02-04T14:37:02.668959Z

Yep, could use require instead. 👍

flowthing 2022-02-04T14:37:16.304059Z

Yeah, compilation doesn't seem to take that long, but I'll keep that in mind.

borkdude 2022-02-04T14:38:52.495779Z

To ensure classes exists: (fs/create-dirs "classes")

borkdude 2022-02-04T14:39:22.846899Z

(about modified-since: https://blog.michielborkent.nl/speeding-up-builds-fs-modified-since.html)

flowthing 2022-02-04T14:46:16.095649Z

Well, I tried putting (binding [*compile-path* "classes"] (compile 'db.migration.migrations)) into (user/start) instead of at the top level and it still works. Better to have it at the top level so that the migrations are compiled even when I don't run (user/start), though. I have zero idea why it works now and didn't before, though, but I think Stupid User Error is the only possibility. 🙂

borkdude 2022-02-04T14:46:53.780079Z

Congrats on getting it working

flowthing 2022-02-04T14:47:09.783399Z

Nonetheless, many thanks for the help! This is much better than having the compile-migrations task. 🙂

flowthing 2022-02-04T15:06:28.963789Z

Oh, not quite there yet, actually.

λ rm -rf classes/*
λ clj -T:build compile-migrations
λ ls classes
db
λ rm -rf classes/*
λ clj -M:dev -r # with (compile ,,,) in user.clj
Clojure 1.10.3
user=>

λ ls classes
clojure cprop   db      hugsql  myapp

flowthing 2022-02-04T15:07:22.951749Z

Having other AOT-compiled classes than db.migration.migrations messes things up in dev. Well, need to revisit this tomorrow.

borkdude 2022-02-04T15:10:35.249519Z

You cannot have (compile 'db.migration) only have the db space compiled. If it depends on other namespaces, those are also going to be compiled. This will be the same with tools.build. But what filter-nses does is that it only copies some of the compiled files from a temporary dir to the class dir.

borkdude 2022-02-04T15:11:12.081979Z

You could have the same if you set compile-path to some tmp-dir and then only copy over the db dir to the class dir.

flowthing 2022-02-04T15:11:59.629369Z

Yes, I figured it must be something like that. Well, need to think about this a bit.

borkdude 2022-02-04T15:12:01.143139Z

but since these are dependencies, they shouldn't be changing, so maybe it doesn't hurt to have their compiled namespaces in here too.

borkdude 2022-02-04T15:12:33.113119Z

if you remove them, clojure will compile them into memory bytecode anyway, again 🤷

borkdude 2022-02-04T15:13:19.708579Z

some people use compile to make an AOT cache for their deps to have their REPL start up faster, which is basically what you're doing here, but only for the transitive deps of db.migrations

flowthing 2022-02-04T15:13:27.601659Z

It does seem to hurt, but I don't know why yet. I evaluated (tools.namespace.repl/refresh ,,,) and got an exception related to one of the AOT-compiled namespaces, but I didn't have the time to look further into it yet.

borkdude 2022-02-04T15:13:47.141869Z

omg, tools namespace refresh... let's not go there ;)

flowthing 2022-02-04T15:14:00.238099Z

I don't want to use t.n.r, but that's the way the project is set up, unfortunately. 🙂

flowthing 2022-02-04T15:14:20.862559Z

Guess I could try to rip it out, maybe...

borkdude 2022-02-04T15:14:24.944949Z

ok, delete everything from the classes dir except db and then you should have a similar thing as with tools.builder filter-nses

flowthing 2022-02-04T15:14:51.535519Z

Yes, I'll consider that, thanks. 👍

Jungin Kwon 2022-02-04T03:07:33.410979Z

I am getting this error, when I build with clojure -T:build uber. I am using v0.6.7 tools.build. Sometimes the build succeeds and sometimes it fails with this error. Anyone know why?

Syntax error compiling at (clojure/tools/namespace/parse.cljc:55:19).
No such var: reader/read

2022-02-04T03:30:01.130159Z

What version of tools.deps? There was a code loading race condition that could manifest oddly like that

2022-02-04T03:31:19.551179Z

0.12.1098 and later should have the race I am thinking of fixed

Alex Miller (Clojure team) 2022-02-04T03:34:00.139689Z

Yeah, I would bump to latest tools.build, that should be fixed

seancorfield 2022-02-04T03:36:15.218359Z

FWIW, tools.build 0.6.7 brings in t.d.a 0.12.1071 so, yeah, use a later version to see if it helps @jungin.kwon1

Jungin Kwon 2022-02-04T03:59:10.004019Z

Thank you! 👍