Fork me on GitHub
#shadow-cljs
<
2022-12-29
>
mbertheau21:12:07

@thheller Why would a :target :bootstrap build error out with ClassNotFoundException: com.rpl.specter.Util? Isn't all compilation done by the JS/CLJS implementation of the CLJS compiler?

mbertheau21:12:26

This is the full backtrace:

markus@ubuntu2204:~/src/specter-demo$ npx shadow-cljs compile :bootstrap
shadow-cljs - config: /home/markus/src/specter-demo/shadow-cljs.edn
shadow-cljs - updating dependencies
shadow-cljs - dependencies updated
[:bootstrap] Compiling ...
------ ERROR -------------------------------------------------------------------
 File: /home/markus/src/specter-demo/specter/src/clj/com/rpl/specter/navs.cljc
failed to require macro-ns "com.rpl.specter", it was required by "com.rpl.specter.navs"
Error in phase :execution
ClassNotFoundException: com.rpl.specter.Util
	java.net.URLClassLoader.findClass (URLClassLoader.java:476)
	clojure.lang.DynamicClassLoader.findClass (DynamicClassLoader.java:69)
	java.lang.ClassLoader.loadClass (ClassLoader.java:589)
	clojure.lang.DynamicClassLoader.loadClass (DynamicClassLoader.java:77)
	java.lang.ClassLoader.loadClass (ClassLoader.java:522)
	java.lang.Class.forName0 (Class.java:-2)
	java.lang.Class.forName (Class.java:398)
	clojure.lang.RT.classForName (RT.java:2209)
	clojure.lang.RT.classForNameNonLoading (RT.java:2222)
	com.rpl.specter.impl/eval15625/loading--6789--auto----15626 (impl.cljc:1)
	com.rpl.specter.impl/eval15625 (impl.cljc:1)
	com.rpl.specter.impl/eval15625 (impl.cljc:1)
	clojure.lang.Compiler.eval (Compiler.java:7194)
	clojure.lang.Compiler.eval (Compiler.java:7183)
	clojure.lang.Compiler.load (Compiler.java:7653)
	clojure.lang.RT.loadResourceScript (RT.java:381)
	clojure.lang.RT.loadResourceScript (RT.java:372)
	clojure.lang.RT.load (RT.java:459)
	clojure.lang.RT.load (RT.java:424)
	clojure.core/load/fn--6908 (core.clj:6161)
	clojure.core/load (core.clj:6160)
	clojure.core/load (core.clj:6144)
	clojure.core/load-one (core.clj:5933)
	clojure.core/load-one (core.clj:5928)
	clojure.core/load-lib/fn--6850 (core.clj:5975)
	clojure.core/load-lib (core.clj:5974)
	clojure.core/load-lib (core.clj:5953)
	clojure.core/apply (core.clj:669)
	clojure.core/load-libs (core.clj:6016)
	clojure.core/load-libs (core.clj:6000)
	clojure.core/apply (core.clj:669)
	clojure.core/require (core.clj:6038)
	clojure.core/require (core.clj:6038)
	com.rpl.specter/eval15483/loading--6789--auto----15484 (specter.cljc:1)
	com.rpl.specter/eval15483 (specter.cljc:1)
	com.rpl.specter/eval15483 (specter.cljc:1)
	clojure.lang.Compiler.eval (Compiler.java:7194)
	clojure.lang.Compiler.eval (Compiler.java:7183)
	clojure.lang.Compiler.load (Compiler.java:7653)
	clojure.lang.RT.loadResourceScript (RT.java:381)
	clojure.lang.RT.loadResourceScript (RT.java:372)
	clojure.lang.RT.load (RT.java:459)
	clojure.lang.RT.load (RT.java:424)
	clojure.core/load/fn--6908 (core.clj:6161)
	clojure.core/load (core.clj:6160)
	clojure.core/load (core.clj:6144)
	clojure.core/load-one (core.clj:5933)
	clojure.core/load-one (core.clj:5928)
	clojure.core/load-lib/fn--6850 (core.clj:5975)
	clojure.core/load-lib (core.clj:5974)
	clojure.core/load-lib (core.clj:5953)
	clojure.core/apply (core.clj:669)
	clojure.core/load-libs (core.clj:6016)
	clojure.core/load-libs (core.clj:6000)
	clojure.core/apply (core.clj:669)
	clojure.core/require (core.clj:6038)
	clojure.core/require (core.clj:6038)
	shadow.build.macros/load-macros/fn--13100/fn--13111 (macros.clj:101)
	shadow.build.macros/load-macros/fn--13100 (macros.clj:100)
	shadow.build.macros/load-macros (macros.clj:94)
	shadow.build.macros/load-macros (macros.clj:85)
	shadow.build.compiler/post-analyze-ns (compiler.clj:49)
	shadow.build.compiler/post-analyze-ns (compiler.clj:46)
	shadow.build.compiler/post-analyze (compiler.clj:92)
	shadow.build.compiler/post-analyze (compiler.clj:89)
	shadow.build.compiler/analyze/fn--13241 (compiler.clj:265)
	shadow.build.compiler/analyze (compiler.clj:252)
	shadow.build.compiler/analyze (compiler.clj:211)
	shadow.build.compiler/analyze (compiler.clj:213)
	shadow.build.compiler/analyze (compiler.clj:211)
	shadow.build.compiler/default-analyze-cljs (compiler.clj:415)
	shadow.build.compiler/default-analyze-cljs (compiler.clj:404)
	clojure.core/partial/fn--5908 (core.clj:2642)
	shadow.build.compiler/do-analyze-cljs-string (compiler.clj:321)
	shadow.build.compiler/do-analyze-cljs-string (compiler.clj:278)
	shadow.build.compiler/analyze-cljs-string/fn--13326 (compiler.clj:518)
	shadow.build.compiler/analyze-cljs-string (compiler.clj:517)
	shadow.build.compiler/analyze-cljs-string (compiler.clj:515)
	shadow.build.compiler/do-compile-cljs-resource/fn--13354 (compiler.clj:637)
	shadow.build.compiler/do-compile-cljs-resource (compiler.clj:618)

mbertheau21:12:12

This is the configuration for the :bootstrap target:

:bootstrap
  {:target     :bootstrap
   :output-dir "public/bootstrap"
   :exclude    #{cljs.js}
   :entries    [cljs.js com.rpl.specter]}

mbertheau21:12:39

I was under the impression that for the bootstrap build only #?(:cljs branches are taken.

thheller05:12:15

bootstrap compiles cljc files twice. one for cljs, once for macros

thheller05:12:37

this appears to be a java helper classes, so you need to write an equiv for JS

mbertheau21:01:58

@thheller Ok, but both are compiled to JS by CLJS, and they take #?(:cljs branches, correct? All mentions of the helper class are in #?(:clj branches.

thheller07:01:53

yes, it takes the :cljs branch when compiling for macros. when compiling normally it is taking for :clj branch for the macros, since that runs in clj

thheller08:01:05

so I looked into self-hosted macros compilation and why deftime doesn't work

thheller08:01:43

which explains it. but it has been like this for 5 years. so either nobody ever used deftime with shadow-cljs

thheller08:01:51

or nobody complained

thheller08:01:15

I'm sort of hesitant to change it, since I'm not sure who'd be affected

mbertheau08:01:55

I don't yet follow. What is going on that the :bootstrap target can complain about Java things if it should take only #?(:cljs branches? Is this an issue that's separate from the deftime question? About changing or not changing *ns*: "`macros/deftime` and macros/usetime to clearly demarcate regions of code that should be run in the macro-definition stage or in the macro-usage stage. (In Clojure there's no distinction; in pure Clojurescript it's easy: just wrap the first stage in #?(:clj ...) and the latter one in #?(:cljs ...); in self-hosted Clojurescript it's messy or everything gets evaluated twice; supporting the three at the same time is Macrovich's raison d'être.)" (from https://github.com/cgrand/macrovich#usage) I take this to mean that in other self-hosted environments deftime works, so maybe it's a good idea for shadow-cljs to be compatible in that regard to these other self-hosted environments.

thheller08:01:43

:bootstrap must compile the namespace twice. the first time it is compiled like regular CLJS, just the normal namespace using CLJ for macro expansion. this will end up loading all the related Java code. on the second pass it is compiled again in macro mode, which is purely CLJS.

thheller08:01:34

the entirely separate issue of deftime is related to the *ns* binding, which I guess macrovich expects to be bound to the $macros variant, which is not the case in shadow-cljs and never has been

thheller08:01:09

it is not the case because otherwise namespaced keywords such as ::foo would have unexpected results

thheller08:01:38

I have never verified what regular CLJS does in these cases since I frankly don't know how to do macro compilation for regular CLJS

thheller08:01:12

but as I said many times before. just remove deftime. it is not needed and doesn't do anything useful here.

mbertheau08:01:38

Part of my quest is to solve the problem, the other is to gain understanding 🙂

thheller08:01:43

if you want to setup a reproduction with regular CLJS that proves the differences I'm happy to adjust

thheller08:01:18

but without that I'm not changing something nobody had an issue with for 5 years

thheller08:01:48

you seem to have a good understanding of stuff. just the wrong expectation of what deftime does. at its current stage it is broken with regards to how shadow-cljs handles it. so its either broken in shadow-cljs or macrovich

mbertheau08:01:09

Just one more question now, of what use is a ClojureScript JVM compilation of a cljs namespace, that is, one using CLJ for macro expansion, in the context of the :bootstrap build, which is producing artifacts that should be consumed by self-hosted ClojureScript?

thheller08:01:42

the entire point of :bootstrap is to *pre-compile* namespaces. this is not something regular CLJS self-hosted does

mbertheau08:01:01

I'll see if I can set up a show case how stuff behaves differently in shadow-cljs self-hosted vs other environments.

mbertheau08:01:28

Yeah ok, but isn't that slightly incorrect, given that without shadow-cljs's caching/pre-compiling, the self-hosted CLJS compiler would request the CLJS source for the namespaces, both :macro false and :macro true , and read both without taking any CLJ branches? That must result in different analysis results than doing it on the server side with CLJ-hosted CLJS.

mbertheau08:01:56

Is it a trade-off, where the difference is just in uncommon edge-cases that haven't come up in five years, or is there something else that I'm missing that resolves these differences?

mbertheau09:01:06

To do everything "correctly" (in my current understanding of what that is), you'd have to use the self-hosted CLJS compiler for the pre-compilation as well. Not sure how practical that is. Maybe a way forward would be to have the pre-compilation be optional and just have shadow-cljs provide or prepare the file system / class path for loading CLJS sources to be used by the self-hosted CLJS compiler.

thheller09:01:41

I'm still missing why having Util is a problem suddenly? if its only in :clj branches everything is fine and I don't understand the context of the problem

thheller09:01:29

pre-compilation is already optional. just don't use it if you don't need it

thheller09:01:48

the entire :bootstrap target is fine with just compiling cljs.core. there is no need to precompile anything else

thheller09:01:30

it is just there to things like maria.cloud (which is what I wrote all this for) load faster. it saves a couple seconds of initialization time to not have the client compile everything on page load

thheller09:01:41

like 10sec+, which was a significant speedup

thheller09:01:33

I do not use self-hosted much myself, so I'm not very clear on any of the details myself. I know it works and that a few people use it as is

thheller09:01:41

I don't know anything using macrovich though

mbertheau09:01:59

Oh, I can switch it off? I'll try that then and see how that behaves differently apart from the performance aspect.

thheller09:01:36

well, you have to supply the compilation parts then on the client

mbertheau09:01:39

So that's just self-hosted cljs without :bootstrap at all then? The part I'd like to use, that :bootstrap provides, is making the source for namespaces available. Just without the precompilation.

thheller09:01:29

if you want custom compilation you write the code for that custom compilation

thheller09:01:52

bootstrap does exactly what it does and not more

mbertheau09:01:57

Well, not so much custom, just that it should all be self-hosted CLJS 🙂

thheller09:01:04

that is custom

thheller09:01:25

the specify :entries [cljs.core] in :bootstrap and do the rest yourself

thheller09:01:39

there is no need to specify anything else. it is just there as a convenience.

thheller09:01:59

self-hosted is a very complex topic and it is impossible to cover all aspects simply

thheller09:01:11

things are going to vary greatly depending what you are building this for

thheller09:01:32

ie. running node means the sources and code are loaded entirely differently than in the browser

thheller09:01:47

pre-compilation is provided as a convenient but OPTIONAL "service"

thheller09:01:45

there are some other things that pre-dated :target :bootstrap that tried to do the same thing

thheller09:01:58

and some others I can't remember the names of

thheller09:01:43

maybe they can provide some useful background

mbertheau09:01:13

Alright, I learned a lot, thanks! This is my take-away: :bootstrap compiles namespaces in normal mode (i.e. :macro false) with the CLJ ClojureScript compiler. It'll correspondingly compile any required macro namespaces with Clojure and use these to expand macros. It'll also compile any required namespaces in macro mode with the CLJ ClojureScript compiler, producing JS output. (Here there's no difference between using the CLJ ClojureScript compiler or a JS-hosted CLJS ClojureScript compiler.) Effectively, if a namespace is required as both a normal and a macro namespace, it'll be compiled three times: • a) once explicitly by :bootstrap with the CLJ ClojureScript compiler in non-macro mode as ClojureScript to produce output JS and analysis for the browser-side CLJS compiler • b) once somewhat implicitly by the CLJ ClojureScript compiler with the Clojure compiler in macro mode as Clojure, to do macro expansion for a) • c) once explicitly by :bootstrap with the CLJ ClojureScript compiler in macro-mode as ClojureScript to produce output JS and analysis for the browser-side CLJS compiler Is this correct?

thheller09:01:50

> It'll correspondingly compile any required macro namespaces

thheller09:01:12

no. it'll not compile them at all. it'll (require 'that.ns) in CLJ only. the compiler never looks at them.

thheller09:01:04

and we need to be explicit about .cljc in this case. cljc is always multiple files in one, which makes it more difficult to understand

thheller09:01:17

lets takes a regular .cljs file with macros in .clj

thheller09:01:37

for self hosted/boostrap the cljs file is compiled once and the clj file is compiled once. for normal compilation only the cljs file is looked at and compiled by the cljs compiler. the clj file is loaded by clojure and a black box as far as the cljs compiler is concerned

thheller09:01:01

the same is true for cljc just that there is only one file

mbertheau09:01:44

OK, let me type that out to account for that.

mbertheau10:01:00

With the following :bootstrap configuration

{:entries [cljs.js my.separate]
 :macros  [my.separate]}
and a file my/separate.cljs:
(ns my.separate
  (:require-macros [my.separate]))

;; main code omitted
and a file my/separate.clj:
(ns my.separate)

;; macro implementations and supporting code omitted
we're asking the :bootstrap build to prepare everything so that I can require my.separate:
(ns ...
  (:require [my.separate]))
and get access to both macros and normal functions in it. Effectively, the following things will happen during the :bootstrap build: 1. :bootstrap uses the CLJ ClojureScript compiler to compile the my.separate namespace in non-macro mode. This uses the my/separate.cljs file, taking #?(:cljs branches. It produces JS output suitable for execution in a JS runtime. 2. During this process, the CLJ ClojureScript compiler requests the my.separate namespace in macro mode. The file my/separate.clj is provided to it. It is compiled as Clojure, taking #?(:clj branches. The result is used for macro expansion in step 1. 3. Step 1 finishes and produces JS output as well as analysis results. 4. :bootstrap uses the CLJ ClojureScript compiler to compile the my.separate namespace in macro mode. This uses the my/separate.clj file. It is compiled as ClojureScript, taking #?(:cljs branches. It produces JS output suitable for the macro expansion phase of the self-hosted ClojureScript compiler. my/separate.clj was compiled twice: once as Clojure in macro-mode, and once as ClojureScript, also in macro-mode. Here is what happens when combining the non-macro and macro namespaces in one .cljc-file: With the following :bootstrap configuration
{:entries [cljs.js my.combined]
 :macros  [my.combined]}
and a file my/combined.cljc:
(ns my.combined
  #?(:cljs (:require-macros [my.combined])))

;; macro implementations and supporting code omitted
;; main code omitted
we're asking the :bootstrap build to prepare everything so that I can require my.combined:
(ns ...
  (:require [my.combined]))
and get access to both macros and normal functions in it. Effectively, the following things will happen during the :bootstrap build: 1. :bootstrap uses the CLJ ClojureScript compiler to compile the my.combined namespace in non-macro mode. This uses the my/combined.cljc file, taking #?(:cljs branches. It produces JS output suitable for execution in a JS runtime. 2. During this process, the CLJ ClojureScript compiler requests the my.combined namespace in macro mode. The file my/combined.cljc is provided to it. It is compiled as Clojure, taking #?(:clj branches. The result is used for macro expansion in step 1. 3. Step 1 finishes and produces JS output as well as analysis results. 4. :bootstrap uses the CLJ ClojureScript compiler to compile the my.combined namespace in macro mode. This uses the my/combined.cljc file. It is compiled as ClojureScript, taking #?(:cljs branches. It produces JS output suitable for the macro expansion phase of the self-hosted ClojureScript compiler. my/combined.cljc was compiled three times: once as ClojureScript in non-macro mode, once as Clojure in macro-mode, and once as ClojureScript in macro-mode. @thheller, is that accurate?

thheller13:01:05

> 2. During this process, the CLJ ClojureScript compiler requests ...

thheller13:01:30

no, as I explained above ... the clojurescript compiler does absolutely nothing with those files

thheller13:01:38

except require them.

thheller13:01:58

also cljs/clj files don't take any #? branches. thats CLJC only

thheller13:01:22

also the macro compilation happens after all cljs compilation is completed

thheller13:01:42

> my/combined.cljc was compiled three times: once as ClojureScript in non-macro mode, once as Clojure in macro-mode, and once as ClojureScript in macro-mode.

thheller13:01:54

again .. 2 times by the cljs compiler

thheller13:01:18

the third time it is loaded by clojure as a regular clojure file. it is not touched or compiled in any way by the cljs compiler

thheller13:01:40

this is true for cljs and cljc files

mbertheau18:01:22

Alright, thanks! Back from holidays now, continuing this inquiry 🙂 Here's the corrected text: With the following :bootstrap configuration

{:entries [cljs.js my.separate]
 :macros  [my.separate]}
and a file my/separate.cljs:
(ns my.separate
  (:require-macros [my.separate])

;; main code omitted
and a file my/separate.clj:
(ns my.separate)

;; macro implementations and supporting code omitted
we're asking the :bootstrap build to prepare everything so that I can require my.separate:
(ns ...
  (:require [my.separate]))
and get access to both macros and normal functions in it. Effectively, the following things will happen during the :bootstrap build with regards to the my.separate namespace: 1. :bootstrap uses the CLJ ClojureScript compiler to compile the my.separate namespace in non-macro mode. This uses the my/separate.cljs file. It produces JS output suitable for execution in a JS runtime. 2. During this process, the CLJ ClojureScript compiler requires the my.separate namespace. The file my/separate.clj is used. It is compiled as Clojure. The result is used for macro expansion in step 1. 3. Step 1 finishes and produces JS output as well as analysis results. 4. :bootstrap uses the CLJ ClojureScript compiler to compile the my.separate namespace in macro mode. This uses the my/separate.clj file. It is compiled as ClojureScript. It produces JS output suitable for the macro expansion phase of the self-hosted ClojureScript compiler. my/separate.clj was compiled twice: once as Clojure (implicitly by requiring it during CLJS compilation), and once as ClojureScript in macro-mode. Here is what happens when combining the non-macro and macro namespaces in one .cljc-file: With the following :bootstrap configuration:
{:entries [cljs.js my.combined]
 :macros  [my.combined]}
and a file my/combined.cljc:
(ns my.combined
  #?(:cljs (:require-macros [my.combined])))

;; macro implementations and supporting code omitted
;; main code omitted
we're asking the :bootstrap build to prepare everything so that I can require my.combined:
(ns ...
  (:require [my.combined]))
Effectively, the following things will happen during the :bootstrap build with regards to the my.combined namespace: 1. :bootstrap uses the CLJ ClojureScript compiler to compile the my.combined namespace in non-macro mode. This uses the my/combined.cljc file, taking #?(:cljs branches. It produces JS output suitable for execution in a JS runtime. 2. During this process, the CLJ ClojureScript compiler requires the my.combined namespace. The file my/combined.cljc is used. It is compiled as Clojure, taking #?(:clj branches. The result is used for macro expansion in step 1. 3. Step 1 finishes and produces JS output as well as analysis results. 4. :bootstrap uses the CLJ ClojureScript compiler to compile the my.combined namespace in macro mode. This uses the my/combined.cljc file. It is compiled as ClojureScript, taking #?(:cljs branches. It produces JS output suitable for the macro expansion phase of the self-hosted ClojureScript compiler. my/combined.cljc was compiled three times: once as ClojureScript in non-macro mode, once as Clojure (again, implicitly by requiring it during CLJS compilation), and once as ClojureScript in macro-mode. And two observations: • if you need differing macro implementations for CLJ and CLJS, there's no way around .cljc files, correct? • In the first situation, a .clj file is compiled as a ClojureScript file, producing JS output, correct? @thheller, is that all accurate?

thheller19:01:18

in all this summary I'd remove "It is compiled as Clojure." as it just confuses what is going on IMHO

thheller19:01:58

no and yes to the last two questions

thheller19:01:27

yes, the .clj file is compiled to produce JS output

thheller19:01:47

the macro can emit what it wants in case of .clj too. Using reader conditionals for that is not the only way

thheller19:01:27

and FWIW you do not need :macros [my.combined] in the build config. it is infered that this is require'd via the :require-macros. It is only needed for namespaces that are not otherwise required

mbertheau20:01:56

Alright, thanks!