|
| 1 | +--- |
| 2 | +title: A deeper understanding of Clojure CLI tools |
| 3 | +authors: |
| 4 | + - practicalli |
| 5 | +date: 2019-07-21 |
| 6 | +draft: false |
| 7 | +categories: |
| 8 | + - clojure-cli |
| 9 | +tags: |
| 10 | + - tools-deps |
| 11 | + - clojure-cli |
| 12 | +draft: true |
| 13 | +--- |
| 14 | + |
| 15 | + |
| 16 | +{align=right loading=lazy style="width:240px"} |
| 17 | + |
| 18 | +CLI tools make Clojure very accessible and simple to install as they are a essentially a wrapper for running Clojure code using the `java` command and use additional libraries to manage dependencies, class paths, create projects and build java archive (jar) files. |
| 19 | + |
| 20 | +Its quite common to use the `java` command to run your code in production, usually defined in a shell script. Leiningen can be used to run your application in production too, however, because Leiningen creates 2 JVM instances (one for itself and one for the application), its more efficient to just use the `java` command. |
| 21 | + |
| 22 | +Leiningen does provides a very rich set of templates that speed up development with Clojure and has a multitude of plugins. Plugins provide a rich source of features but they are not very composable, especially compared to the Clojure language itself. |
| 23 | + |
| 24 | +Clojure CLI tools provide a minimal but elegant layer on top of the `java` command and enables libraries, configuration and code to compose together just like Clojure functions. So we will continuing the exploration of Clojure CLI tools and dig a little deeper under the covers to understand how they work and how to configure projects to be very flexible, especially the different sources of code you can use . |
| 25 | + |
| 26 | +> Additional content can be found in [Using Clojure tools section of Practicalli Clojure](http://practical.li/clojure/clojure-cli/) |
| 27 | +
|
| 28 | +<!-- more --> |
| 29 | + |
| 30 | + |
| 31 | +## Under the covers of CLojure CLI |
| 32 | + |
| 33 | +Using the command `lein new app classic` creates a simple project called `classic` containing some source code and test code. We can use `lein repl` to give instant feedback on the evaluation of the code in our project. |
| 34 | + |
| 35 | +This command also compiles our code to Java bytecode, so it can run on the JVM just like compiled Java or Scala code. |
| 36 | + |
| 37 | +`lein jar` and more commonly `lein uberjar` is used to package up our code into a single file. These commands compile the Clojure code into classes when Ahead Of Time compilation is used. Any namespaces with `(:gen-class)` directive included in compiled into a JVM bytecode class is `lein uberjar` creates a single file that contains our application and the Clojure library, so we can use with the java command line |
| 38 | + |
| 39 | +`java -cp target/myproject-standalone.jar` |
| 40 | + |
| 41 | +If I had created a library project, with `lein new classic`, then I would need to specify clojure.main and the main class for the `java` command to work correctly. |
| 42 | + |
| 43 | +`java -cp target/myproject-standalone.jar clojure.main -m classic.core` |
| 44 | + |
| 45 | + |
| 46 | +It is also possible to run the compiled source code, however, we will also need to add Clojure as a dependency. There is a copy of the Clojure library in my maven cache from previous projects I have worked on. |
| 47 | + |
| 48 | +`java -cp target/uberjar/classes/:/home/jr0cket/.m2/repository/org/clojure/clojure/1.8.0/clojure-1.8.0.jar classic.core` |
| 49 | + |
| 50 | + |
| 51 | +If I just wanted to run a repl, I can call clojure.main as my namespace |
| 52 | + |
| 53 | +`java -cp /home/jr0cket/.m2/repository/org/clojure/clojure/1.8.0/clojure-1.8.0.jar clojure.main` |
| 54 | + |
| 55 | +Already there are a few things to remember. As your project gets bigger then the command you use will get bigger and harder to manage safely, its often put into scripts but then there is no real validation that you got the script right, without some manual testing |
| 56 | + |
| 57 | +`java $JVM_OPTS -cp target/todo-list.jar clojure.main -m todo-list.core $PORT` |
| 58 | + |
| 59 | + |
| 60 | +## Clojure CLI |
| 61 | + |
| 62 | +CLI tools project only requires a `deps.edn` file. |
| 63 | + |
| 64 | +`~/.clojure/deps.edn` is created the first time you run the `clojure` command. |
| 65 | + |
| 66 | +A default `deps.edn` file comes with the CLI tools install, e.g. `/usr/local/lib/clojure/deps.edn` on Linux. This file contains a few basic options that are applied to all projects. |
| 67 | + |
| 68 | +`src` is set as the relative path to the source code |
| 69 | + |
| 70 | +The dependencies include `1.12.0` version of the Clojure library. |
| 71 | + |
| 72 | +Aliases define additional libraries that will only be included during development, e.g. `:deps` which provides extra features |
| 73 | + |
| 74 | +Maven central and Clojars are repositories containing Clojure Library Dependencies which the Clojure CLI downloads them from. |
| 75 | + |
| 76 | +```clojure |
| 77 | +{ |
| 78 | + :paths ["src"] |
| 79 | + |
| 80 | + :deps { |
| 81 | + org.clojure/clojure {:mvn/version "1.12.0"} |
| 82 | + } |
| 83 | + |
| 84 | + :aliases { |
| 85 | + :deps {:replace-paths [] |
| 86 | + :replace-deps {org.clojure/tools.deps.cli {:mvn/version "0.11.72"}} |
| 87 | + :ns-default clojure.tools.deps.cli.api |
| 88 | + :ns-aliases {help clojure.tools.deps.cli.help}} |
| 89 | + :test {:extra-paths ["test"]} |
| 90 | + } |
| 91 | + |
| 92 | + :mvn/repos { |
| 93 | + "central" {:url "https://repo1.maven.org/maven2/"} |
| 94 | + "clojars" {:url "https://repo.clojars.org/"} |
| 95 | + } |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | + |
| 100 | +## A simple project configuration |
| 101 | + |
| 102 | + |
| 103 | +```clojure |
| 104 | +{:paths ["src"] |
| 105 | + |
| 106 | + :deps |
| 107 | + {org.clojure/clojure {:mvn/version "1.12.0"}} |
| 108 | + |
| 109 | + :aliases |
| 110 | + {:test {:extra-paths ["test"] |
| 111 | + :extra-deps {io.github.cognitect-labs/test-runner |
| 112 | + {:git/tag "v0.5.1" :git/sha "dfb30dd"}} |
| 113 | + :main-opts ["-m" "cognitect.test-runner"] |
| 114 | + :exec-fn cognitect.test-runner.api/test}}``` |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +Dependencies from a Git repository are automatically downloaded and built, within a directory called `~/.gitlibs`. |
| 119 | + |
| 120 | + |
| 121 | +## Time for some Test Driven Development |
| 122 | + |
| 123 | +Create a new file in the `test` directory called `core_test.clj` that contains a test with two assertions. |
| 124 | + |
| 125 | +The `clojure.test` namespace is included in the `org.clojure/clojure` dependency, so we do not have to add anything to the `deps.edn` file |
| 126 | + |
| 127 | +```clojure title="test/practicalli/simple_test.clj" |
| 128 | +(ns practicalli.simple-test |
| 129 | + (:require [practicalli.simple :as simple] |
| 130 | + [clojure.test :refer [deftest testing is]])) |
| 131 | + |
| 132 | + |
| 133 | +(deftest core-tests |
| 134 | + (testing "The correct welcome message is returned" |
| 135 | + (is (= (simple/-main) |
| 136 | + "Hello World!")) |
| 137 | + |
| 138 | + (is (= (simple/-main "Welcome to the Clojure CLI") |
| 139 | + "Hello World! Welcome to the Clojure CLI")))) |
| 140 | +``` |
| 141 | + |
| 142 | +We run the failing tests with the following command |
| 143 | + |
| 144 | +```shell-session |
| 145 | +clojure -X:test |
| 146 | + |
| 147 | +Checking out: https://github.com/cognitect-labs/test-runner.git |
| 148 | + |
| 149 | +Running tests in #{"test"} |
| 150 | +Syntax error compiling at (practicalli/simple_test.clj:8:26). |
| 151 | +No such var: sut/-main |
| 152 | + |
| 153 | +Full report at: |
| 154 | +/tmp/clojure-3370388766424088668.edn |
| 155 | +``` |
| 156 | + |
| 157 | +You can see that the first time we are using the test-runner the CLI tools download the source code from the Git repository. |
| 158 | + |
| 159 | +> NOTE: Using a Git commit provides just a stable dependency as Maven or other tool. The only risk is if you are using a shared repository and a force commit is made that replaces the commit you have as dependency, but that will have a different hash value, so you will notice that kind of change when running your code. |
| 160 | + |
| 161 | + |
| 162 | +## And now some code |
| 163 | + |
| 164 | +Everything is working correctly and the tests are failing because we have not written the code that the test is using. So write the application code and make the test pass and execute the test runner again. |
| 165 | + |
| 166 | +```clojure |
| 167 | +(ns practicalli.simple) |
| 168 | + |
| 169 | +(defn -main [] |
| 170 | + (println "Hello world!")) |
| 171 | +``` |
| 172 | + |
| 173 | + |
| 174 | +## Extra dependencies |
| 175 | + |
| 176 | + |
| 177 | +## Over-ride |
| 178 | + |
| 179 | +Use different versions of dependencies in your project that is set globally. One example is if you are actively building a project, you may want to include the latest commit on a feature branch. Or you may be using a third party library and want to test out a new beta version. Or perhaps you are releasing a library and want to test it with earlier versions of Clojure, for example. |
| 180 | + |
| 181 | + |
| 182 | +### Example |
| 183 | + |
| 184 | + |
| 185 | + |
| 186 | +## JVM options |
| 187 | + |
| 188 | +Passing options to the Java Virtual Machine can be very important to shape the performance dynamics of your Clojure application. For example, not enough memory allocation can really grind your application to a halt. I experienced this with a third party Java project, they only had 512Mb as the memory allocation size and after a number of uses we working with it then it would steadily grind to a halt. Doubling the JVM memory allocation made the application fly for hundreds of concurrent users. |
| 189 | + |
| 190 | + |
| 191 | +## Configuration options useful for CLJS |
| 192 | + |
| 193 | +:output-dir to define where the resulting JavaScript file is written too when compiling ClojureScript. This is used for a different build, e.g. `deploy` to |
| 194 | + |
| 195 | + |
| 196 | +## Deployment |
| 197 | + |
| 198 | +We saw that Leiningen created a single file that we can use to deploy our application and call from the `java` command line. |
| 199 | + |
| 200 | +TODO: tools-build example |
| 201 | + |
| 202 | + |
| 203 | +## Running |
| 204 | + |
| 205 | +To run the generated jar file |
| 206 | + |
| 207 | +java -cp simple.jar clojure.main -m simple.core |
| 208 | + |
| 209 | +> depstar does not do any ahead of time compilation (AOT) so your application may start up more slowly as the code first needs to be compiled into Java byte code. |
| 210 | + |
| 211 | +<https://github.com/clojure/clojure/commit/653b8465845a78ef7543e0a250078eea2d56b659> |
| 212 | + |
| 213 | +Thank you. |
| 214 | +[@jr0cket](https://twitter.com/jr0cket) |
0 commit comments