Skip to content

Boot Troubleshooting

Martin Klepsch edited this page Dec 22, 2017 · 24 revisions

The notorious "No reader function for tag object" exception

May happen if you try to access something external from within a pod. Only forms that can be printed with pr-str and read via read-string can pass between pods. That’s why you see that exception. You want to pass in the string path to the java.io.File object, not the object itself; i.e. pass ~(.getPath tgt) instead of ~tgt. The test it must pass is (= tgt (read-string (pr-str tgt)))

This is due to the way Clojure works; interfaces and classes are created dynamically at runtime, so two clojure runtimes can’t understand each other’s clojure things. Remember that pods are runtimes.

Why is doseq throwing a "Cannot cast java.lang.Character to java.lang.String"

If you use e.g. core/by-ext to create sets of files and then you doseq over the set, you’ll get this exception if you used a string instead of a vector of strings for by-ext.

correct:  (core/by-ext [".clj"] fs)          incorrect:  (core/by-ext ".clj" fs)

Where are my outputs?

Scenario: $ boot aot -a does not generate class files.

Resolution:

  • check your source paths: in build.boot you should have something like (set-env! :source-paths #{"src/clj"} …​)

  • make sure you have some .clj files in your source paths

  • add the show task to your pipeline to see what files are produced by your task. For example, boot aot show --fileset will list the files in the input fileset fed to aot - that’s because boot aot will just pass the input to the output fileset without compiling anything. To make it compile you must pass it a flag, -a for all or -n foo.bar to compile namespace foo.bar. See boot aot -h.

  • to see the results of aot, run boot aot -a show --fileset. Here the aot task will compile all .clj files in your source paths, put them into its output fileset, and pass it to the next task, show, which will display them in a nicely formatted tree. NOTE: show will pass its input unchanged to the next task. If there is no next task, as in this example, nothing will be written to the target path.

  • check your boot.properties files - ~/.boot/boot.properties and ./boot.properties. If you see BOOT_EMIT_TARGET=no, your task will not write its output to disk - you will need to add the target task to your pipeline. See BOOT_EMIT_TARGET.

  • add the target task to your pipeline: boot aot -a show --fileset target -d "build/foo". The job of the target task is to synchronize its input to the target path (directory). Here the contents of "build/foo" should match the listing produced by show --fileset. Of course, you can omit show --fileset from the pipeline, and pass the compiled fileset directly to target.

NOTE: The target task performs a one-way, clean sync. It cleans the target path before it writes the fileset - anything in the target path will be deleted!

Socket server

Clojure 1.8 ships with a socket server. The official docs instruct users to set a java system property that the Clojure runtime will pick up at startup. If you try to do that (via BOOT_JVM_OPTIONS), the socket server will try to bind itself multiple times on the same port due to the fact that Boot launches multiple Clojure runtimes. Hilarity and chaos will ensue. Please consider starting the socket server programmatically, via the API.

For example:

BOOT_CLOJURE_VERSION=1.8.0 boot -i "(do (require 'clojure.core.server) ((resolve 'clojure.core.server/start-server) {:port 9999 :name :repl :accept 'clojure.core.server/repl}))" wait

Why isn’t require working in my pod?

Scenario: you want to run some code in (pod/with-eval-in @pod …​) You added a dependency when you created the pod (e.g. (make-pod (update-in (get-env) [:dependencies] conj '[foo/bar "1.2.3"])); now you need to require the namespace, so you write (require 'foo.bar) (or whatever). But you do some other stuff first, so this require is within an if or do or let or some other block. Now you try to use a var in the namespace, e.g. (foo.bar/baz). And you get a ClassNotFoundException for foo.bar.

Resolution: This is a Clojure issue. Only top level require gets invoked at compile time, which tells Clojure that foo.bar is a namespace symbol. Knowing that, Clojure will treat (foo.bar/baz) as referring to a namespaced symbol (var), rather than invocation of a static method in a Java class.

If your require is not top level, it will fire at runtime, but by then it will be too late: Clojure saw (foo.bar/baz ..) at compile time and interpreted it as a call to a static function baz of a Java class foo.bar. So require executed at runtime will create a Namespace object for foo.bar and put it in the list available using (all-ns), and you can find your symbol in the interns map of the ns (try this: (doseq [[isym ivar] (ns-interns 'foo.bar)] (println "sym: " isym)) but when you try (foo.bar/baz …​) you get ClassNotFoundException, because Clojure still thinks it is an attempt to call a static function in a Java class, which it cannot find.

If you need that kind of dynamism you need to use resolve. Add something like (let [f (resolve 'foo.bar/baz)], and then call f instead of foo.bar/baz.

Watch Task not working in Docker / Vagrant?

Scenario: Changes made to a file mounted with Docker / Vagrant between the guest and host os via NFS; filesystem events are not received correctly.

Resolution: Use rsync in these environments, as NFS does not support inotify. You may also wish to run a repl server in the guest os and connect to it from the host.

pod probs

Pods sometimes behave in surprising ways.

  • At least some namespaces in the enclosing environment are available in pods, but not their aliases. So if you get "No such namespace" for e.g. (util/info "foo"), try (boot.util/info "foo").

General Troubleshooting Tips

  • The verbosity of the task logging can be increased by using the -v flag. For even more verbosity -vv can be used. Eg.

    $ boot -v build
    In the REPL you use it like this (1-3, increasing verbosity):
    (reset! boot.util/*verbosity* 2) (boot (build))
  • Suppose you do boot -v foobar and you get a stack trace. You can then do boot -vb |cat -n and see the matching line numbers. Actually you should do boot -vb foobar to get the exact same generated code; just add -b to the thing that caused the error.

  • boot show is your friend. remember: it’s a task. that means you can insert it in anywhere in a pipeline in order to dump useful info to stdout. For example, boot show -f my-task show -f will print the before and after filesets for my-task.

Clone this wiki locally