Skip to content

Commit 3e92615

Browse files
draft: deeper understanding of clojure cli
1 parent 8a8479a commit 3e92615

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
![Practicalli Clojure Logo](https://github.com/practicalli/graphic-design/blob/live/topic-images/clojure-logo-name.png?raw=true){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

Comments
 (0)