clojure CLI (tools.deps)を使いやすくするためのTips

[Clojureコマンドラインツール](https://clojure.org/guides/deps_and_cli)とその中核ライブラリである[clojure/tools.deps](https://github.com/clojure/tools.deps.alpha)は、2016年のClojure/Conjキーノート[Spec-ulation](https://github.com/clj-nakano/spec-ulation-ja)で述べられているように、Rich HickeyにとってClojure、Datomicに続く新たな開発テーマとなっています。

現時点のclojure CLIはLeiningenと比較すると遥かにシンプルです。Leiningenの代替として、日常の開発で普段使いができるか、試してみることにしました。Tipsを順次追加していく予定です。


## Gitプライベートリポにアクセスするための設定

LeiningenはMavenと同様、アーティファクトをJarにパッケージングし、Mavenリポジトリ（またはClojars）にJarをアップロードするというモデルになっています。Clojure CLIは、Jarにパッケージすることなく、Gitリポジトリに直接アクセスすることで依存関係を解決する手段を提供しています。

```clojure:deps.edn
{:deps {
   com.fzakaria/slf4j-timbre   {:mvn/version "0.3.6"} 
   my-group/my-artifact        {:git/url "git@github.com:my-group/my-artifact.git" 
                                :rev "3a2a5ef8"}
   com.rpl/specter             {:mvn/version "1.0.2"}
   ...}}
```
- パブリックGitリポの場合は`https`スキームで、プライベートの場合は`ssh`スキームで`git/url`を記述します。
- `ssh`を利用する場合、sshキーがssh-agentに追加されていないと認証エラーが発生します。[GitHubのヘルプ](https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/#adding-your-ssh-key-to-the-ssh-agent)を参照して、ssh-agentへの追加を行ってください。
- Gitによる参照の場合、参照元のプロジェクトにも`deps.edn`が設定されている必要があります。現時点では、Gitで参照しているプロジェクトの`pom.xml`や`project.clj`を解析して、再帰的な依存関係解決はサポートされてないようです。

## nREPLの起動

REPLの起動時に読み込まれる`user.clj`内でnREPLサーバを起動し、ポート番号を`.nrepl-port`ファイルに出力することで、CIDERやfireplace.vimが自動的にnREPLに接続してエディタからREPL操作ができるようになります。

```clojure:user.clj
(ns user
  (:require [clojure.tools.nrepl.server :refer [start-server stop-server]]))

(defn- available-port []
  (with-open [s (java.net.ServerSocket. 0)]
    (.getLocalPort s)))

(let [port (available-port)]
  (defonce server (start-server :port port))
  (spit ".nrepl-port" port))


```

## 依存関係衝突の解決方法
Leiningenでは`lein deps :tree`で再帰的依存関係木構造を表示すると同時に衝突しているライブラリの表示をしてくれます。`deps.tree.alpha`には`-Stree`オプションがありますが、衝突を表示しないようなので、

```
clj -Spom
```

で`pom.xml`を生成し、`mvn dependency:tree -Dverbose`で衝突したライブラリを含めて表示することができます。

## ローカルrepoの指定
標準では`$HOME/.m2`にJarファイルなどのアーティファクトがダウンロードされますが、Dockerで依存関係Jarファイルを全てコピーして、実行時に依存関係をダウンロードせずに実行したい場合など、必要な依存関係を別のディレクトリに保存したい場合があります。
`deps.edn`に、`:mvn/local-repo "ディレクトリパス"`を指定することでダウンロード先を変更することができます。

## 依存関係解決順序
tools.depsとMavenでは依存関係の解決順序が異なることに留意してください。
同一ライブラリの複数バージョンが依存関係に見つかった場合の解決方法は、
1. 直接定義されている場合はそのバージョンが勝つ
2. 間接的に依存している場合は、新しいバージョンが勝つ
3. ２次的に依存している場合、たとえ新しいバージョンがあったとしても、その親の依存関係が採用されていない場合は無視される。
となります。

## 依存関係の除外
Leiningenの`:dependencies`と同様に、依存関係のJarから除外したいパッケージ名や名前空間を指定することができます。

```clojure:deps.edn
{:deps {
   org.clojure/clojure {:mvn/version "1.8.0"}
   ...
   org.apache.maven/maven-aether-provider
   {:mvn/version "3.3.9"
    :exclusions [org.eclipse.aether/aether-api org.eclipse.aether/aether-spi
                 org.eclipse.aether/aether-util org.eclips.aether/aether-impl]}}}
```

1.9.0.317で、`clj -Spom`で出力された`pom.xml`にもこの指定が反映されるようになりました。clojure CLIを最新版に更新してください。[JIRAチケット](https://dev.clojure.org/jira/browse/TDEPS-34)


## 変更したファイルの自動リローディングと差分テスト
下記のコードを:dev aliases内の`extra-paths`で指定したディレクトリ`dev/clj`に入れておくことで、`(reload)`を実行すると、変更された名前空間とそこから参照されている名前空間がリロードされ、その後関連するテストだけが選択的に再実行されます。

```clojure:deps.edn
{:aliases {:dev {:extra-paths ["dev/clj" "test"]
                 :extra-deps {cljfmt     {:mvn/version "0.5.7"} 
                              ns-tracker {:mvn/version "0.3.1"}
                              eftest     {:mvn/version "0.4.2"}}}}}

```

```clojure:user.clj
(ns user
  (:require [eftest.runner   :refer [find-tests run-tests]]
            [ns-tracker.core :refer [ns-tracker]]))

(def modified-namespaces
  (ns-tracker ["src/clj" "resources" "test"]))

(defn reload []
  (let [updated-ns (modified-namespaces)]
    (doseq [ns-sym updated-ns]
      (require ns-sym :reload))
    (when updated-ns (run-tests (find-tests updated-ns)))))
```

```clojure:REPL
[kenji@k2n-mbp13: ] clj -R:dev -C:dev
Clojure 1.9.0
(エディタでソースコードを変更し、保存する）
user=> (reload)

2/2   100% [==================================================]  ETA: 00:00

Ran 2 tests in 0.021 seconds
2 assertions, 0 failures, 0 errors.
{:test 2, :pass 2, :fail 0, :error 0, :type :summary, :duration 21.093365}
（変更したソースコードがREPLに反映されると同時に、変更したコードを参照しているテストだけが選択的に実行される）
```
ファイルを保存した際に`(reload)`をトリガーすることも可能です。Vim/NeoVimの場合は下記の行を`.vimrc`か`.nvimrc`に保存します。

```
:autocmd BufWritePost *.clj :Eval (user/reload)
```

## UberjarやAWS Lambdaのパッケージング
- [juxt/pack](https://github.com/juxt/pack.alpha)を使うと、`deps.edn`で定義した情報を元にAWS Lambda用のZipファイルや単体で実行可能なuberjarファイルを生成することができます。
- [luchiniatwork/cambada](https://github.com/luchiniatwork/cambada)を使うと、jar, uberjarに加え、[Graal VM](https://www.graalvm.org/)を用いたnative imageの作成が可能です。

## Private Maven Repoへのアクセス
- 現時点では、パスワードで保護されたmaven repoへのアクセスができないようです。[JIRAチケット](https://dev.clojure.org/jira/browse/TDEPS-9)でパッチが提案されています。
- 依存関係のライブラリが、ClojarsかMaven Central以外のリポジトリから、Leiningenのproject.cljに明示的にリポジトリを指定して、そこからライブラリをダウンロードしている場合、`tools.deps`はその情報を利用できないのでダウンロードに失敗します。一旦`lein deps :tree`を実行し、ダウンロードに失敗しているライブラリを利用しているプロジェクトを特定し、その`project.clj`で記述されているリポジトリを`deps.edn`に追加する必要があります。

```title:ダウンロードに失敗している例
clj -Spath
Error building classpath. Could not find artifact com.jezhumble.javasysmon:javasysmon:jar:0.3.5 in central (https://repo1.maven.org/maven2/)
org.eclipse.aether.resolution.ArtifactResolutionException: Could not find artifact com.jezhumble.javasysmon:javasysmon:jar:0.3.5 in central (https://repo1.maven.org/maven2/)
        at org.eclipse.aether.internal.impl.DefaultArtifactResolver.resolve(DefaultArtifactResolver.java:422)
...
````

`javasysmon`が標準のmaven repoからダウンロードできていません。今回は既存のLeiningen用`project.clj`があるので、`lein deps :tree`で依存ツリーを取得します。ない場合は、一つ一つ確認していくしかありませんね... 上記の`pom.xml`を出力する回避策も、`clj -Spom`が失敗するのでここでは利用不可です。

````
lein deps :tree
...
 [lambdacd "0.13.5"]
   [cheshire "5.4.0" :exclusions [[org.jvnet.winp/winp]]]
     [com.fasterxml.jackson.core/jackson-core "2.4.4"]
     [com.fasterxml.jackson.dataformat/jackson-dataformat-cbor "2.4.4"]
     [com.fasterxml.jackson.dataformat/jackson-dataformat-smile "2.4.4"]
     [tigris "0.1.1"]
   [clj-time "0.9.0" :exclusions [[org.jvnet.winp/winp]]]
     [joda-time "2.6"]
   [clj-timeframes "0.1.0" :exclusions [[org.jvnet.winp/winp]]]
     [com.gfredericks/test.chuck "0.2.1"]
       [com.andrewmcveigh/cljs-time "0.3.11"]
     [org.clojure/math.combinatorics "0.1.1"]
     [org.clojure/test.check "0.9.0"]
   [cljsjs/moment "2.10.6-4" :exclusions [[org.jvnet.winp/winp]]]
   [com.jezhumble/javasysmon "0.3.6" :exclusions [[org.jvnet.winp/winp]]] ;; <=== このファイルがダウンロードできていない
   [compojure "1.1.8" :exclusions [[org.jvnet.winp/winp]]]
...
````

依存関係ツリーをみると、`lambdacd`が要求しているので、Githubでそのproject.cljを見てみます。

````title:lambdacd/project.clj
(defproject lambdacd "0.14.0-SNAPSHOT"
  :description "A library to create a continous delivery pipeline in code."
  :url "http://github.com/flosell/lambdacd"
  :license {:name "Apache License, version 2.0"
            :url "http://www.apache.org/licenses/LICENSE-2.0.html"}
  :min-lein-version "2.5.0"
  :deploy-repositories [["clojars" {:creds :gpg}]
                        ["releases" :clojars]]
  :repositories [["gocd" "https://dl.bintray.com/gocd-maven-repo/generic/gocd"]] ; for jezhumbles javasysmon
  :source-paths ["src/clj" "src/cljs"]
...
````

"gocd"という名のリポジトリが定義されているのがわかります。そこで、このURLを`deps.edn`に追記します。

```title:deps.edn
{:mvn/repos {"gocd" {:url "https://dl.bintray.com/gocd-maven-repo/generic/gocd"}}
 :deps 
 {lambdacd {:mvn/version "0.13.5"}
  lambdacd-git {:mvn/version "0.4.0"}
...
````

これでダウンロードできるようになりました。
[JIRAチケット](https://dev.clojure.org/jira/browse/TDEPS-46)

## 複数のリポジトリを更新しながらの開発
gitによる依存関係の取得が可能になったことで、例えば[Duct](https://github.com/duct-framework/duct)コンポーネントを独立したgit repoにし、アプリケーションからそのコンポーネントをgit経由で利用する、という使い方が容易になりました。
しかし、開発中、いちいち変更をコミットし、githubにプッシュするのは開発フローの重荷になるので、回避策を考えます。

### `local/root`を活用する
`tools.deps`はローカルにあるJarファイルを依存関係に組み込むために[`local/root`を提供](https://clojure.org/guides/deps_and_cli#local_jar)しています。これをgitリポに対しても適用させることができます。もちろん依存するリポジトリも`deps.edn`を持っていることが条件となります。Transitive dependencyもきちんと動作しているようです。

```clojure
{:deps {;;clj-nakano/my-duct-component    {:git/url "git@github.com:clj-nakano/my-duct-component.git"
        ;;                                      :sha     "8ab0541fda5a28ca3ede6dc6e0b89cf9379d328b"}
        clj-nakano/my-duct-component      {:local/root "/Users/kenji/ghq/github.com/clj-nakano/my-duct-component"}}

   
```

## 依存関係の解決結果をグラフィカルに表示する
2019年11月23日に[tools.deps.graph](https://github.com/clojure/tools.deps.graph)が公開されました。

## 最新の動向
Alex MillerがClojure/conj 2019で[Composable Tools](https://www.youtube.com/watch?v=7CM7Ef-dPWQ&list=PLZdCLR02grLqSy15ALLAZDU6LGpAJDrAQ&index=29&t=0s)で最新機能の追加と今後の開発方針について公演しています。