Clojure

tools.build ガイド

tools.build は、Clojureプロジェクトをビルドするための関数のライブラリです。ユーザーが呼び出し可能なターゲット関数を作成するためのビルドプログラムで使用されることを意図しています。また、APIドキュメントも参照してください。

ビルドはプログラムである

tools.buildの背後にある哲学は、プロジェクトのビルドは本質的にプログラムであるということです。つまり、プロジェクトのソースファイルから1つ以上のプロジェクト成果物を作成するための一連の命令です。このプログラムを、お気に入りのプログラミング言語であるClojureで記述したいと考えており、tools.buildは、ビルドで一般的に必要な関数をまとめたライブラリであり、柔軟な方法で接続できます。ビルドプログラムの記述には、他の宣言型アプローチよりも少し多くのコードが必要になりますが、将来にわたって簡単に拡張またはカスタマイズでき、プロジェクトとともに成長するビルドを作成できます。

セットアップ

インストール手順はありません。tools.buildは、ビルドプログラムが使用する単なるライブラリです。deps.ednにエイリアスを作成し、tools.buildを依存関係として含め、ビルドプログラムへのソースパスを含めます。ビルドは、Clojure CLI(-Tを使用)でプロジェクト「ツール」として簡単に実行できるように設計されています。Clojure CLIでは、「ツール」は機能を提供するプログラムであり、プロジェクトのdepsやクラスパスは使用しません。-T:an-aliasで実行されるツールは、すべてのプロジェクトのdepsとパスを削除し、"."をパスとして追加し、:an-aliasで定義されている他のdepsまたはパスを含めます。

そのため、deps.ednに、ビルドクラスパスを定義し、ビルドソースへのパスを含むエイリアスが必要です。例を次に示します。

{:paths ["src"] ;; project paths
 :deps {}       ;; project deps

 :aliases
 {;; Run with clj -T:build function-in-build
  :build {:deps {io.github.clojure/tools.build {:git/tag "TAG" :git/sha "SHA"}}
          :ns-default build}}}

https://github.com/clojure/tools.build#release-informationで、使用する最新のTAGとSHAを見つけてください。

このガイドのgit depとClojure CLIの例では、Clojure CLI 1.10.3.933以上を使用していることを前提としています。

上記のように、-Tを使用してツールを実行すると、プロジェクトの:pathsと:depsを含まないクラスパスが作成されます。-T:buildを使用すると、:buildエイリアスからの:paths:depsのみが使用されます。ルートのdeps.ednは依然として含まれており、Clojureもプルインされます(ただし、tools.buildの依存関係としても含まれます)。:pathsはここでは指定されていないため、追加のパスは追加されませんが、-Tはプロジェクトルート"."をデフォルトでパスとして含めます。

したがって、clj -T:build jarを実行すると、次の有効なクラスパスが使用されます。

  • "." (-Tによって追加)

  • org.clojure/clojure (ルートのdeps.edn :depsから) および推移的なdeps

  • org.clojure/tools.build (:buildエイリアスの:depsから) および推移的なdeps

:ns-defaultは、クラスパスで指定された関数を見つけるためのデフォルトのClojure名前空間を指定します。ローカルパスはデフォルトの"."のみであるため、プロジェクトのルートにあるbuild.cljにビルドプログラムがあるはずです。パスのルート(:buildエイリアスの:paths経由)と、それらのパスルートに対するビルドプログラム自体の名前空間は、完全にあなたの制御下にあることに注意してください。それらをプロジェクトのサブディレクトリに配置することもできます。

そして最後に、コマンドラインでビルドで実行する関数(ここではjar)を指定します。その関数は、build名前空間で実行され、-Xと同じ引数渡しスタイルを使用して構築されたマップが渡されます。引数は、交互のキーと値として提供されます。

このガイドの残りの部分では、個々の一般的なユースケースと、tools.buildプログラムでそれらをどのように満たすかを示します。

ソースライブラリjarのビルド

最も一般的なClojureビルドでは、Clojureソースコードを含むjarファイルを作成します。tools.buildでこれを行うには、次のタスクを使用します。

  • create-basis - プロジェクトのベースを作成するため(注:これにより、副作用としてdepsがダウンロードされます)

  • copy-dir - Clojureソースとリソースをワーキングディレクトリにコピーするため

  • write-pom - ワーキングディレクトリにpomファイルを書き込むため

  • jar - ワーキングディレクトリをjarファイルにまとめるため

build.cljは次のようになります。

(ns build
  (:require [clojure.tools.build.api :as b]))

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def jar-file (format "target/%s-%s.jar" (name lib) version))

;; delay to defer side effects (artifact downloads)
(def basis (delay (b/create-basis {:project "deps.edn"})))

(defn clean [_]
  (b/delete {:path "target"}))

(defn jar [_]
  (b/write-pom {:class-dir class-dir
                :lib lib
                :version version
                :basis @basis
                :src-dirs ["src"]})
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir})
  (b/jar {:class-dir class-dir
          :jar-file jar-file}))

注意すべき点

  • これは通常のClojureコードです。エディタでこの名前空間をロードし、REPLでインタラクティブに開発できます。

  • 単一目的のプログラムとして、上部のvarのセットで共有データを構築してもかまいません。

  • 「target」ディレクトリでビルドし、「target/classes」にjarの内容をアセンブルすることを選択していますが、これらのパスには特別なものはありません。完全にあなたの制御下にあります。また、これらのパスや他のパスをここで複数の場所で繰り返していますが、重複を適切に感じる程度に削除できます。

  • ユーザーが呼び出すbuild/jarのような大きな関数を組み立てるために、tools.buildのタスク関数を使用しました。これらの関数はパラメーターマップを受け取り、ここでは構成可能なパラメーターを提供しないことを選択しましたが、提供することもできます!

deps.ednファイルは次のようになります。

{:paths ["src"]
 :aliases
 {:build {:deps {io.github.clojure/tools.build {:git/tag "TAG" :git/sha "SHA"}}
          :ns-default build}}}

そして、このビルドは次のように実行できます。

clj -T:build clean
clj -T:build jar

これらをコマンドラインで同時に実行できるようにしたいと考えていますが、これは現在進行中です。

コンパイルされたuberjarアプリケーションのビルド

アプリケーションを準備するときは、アプリ全体とライブラリをコンパイルし、全体を単一のuberjarとしてアセンブルするのが一般的です。

メインのClojure名前空間には(:gen-class)が必要です。例:

(ns my.lib.main
  ;; any :require and/or :import clauses
  (:gen-class))

また、その名前空間には次のような関数が必要です。

(defn -main [& args]
  (do-stuff))

コンパイルされたuberjarのビルド例は次のようになります。

(ns build
  (:require [clojure.tools.build.api :as b]))

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))

;; delay to defer side effects (artifact downloads)
(def basis (delay (b/create-basis {:project "deps.edn"})))

(defn clean [_]
  (b/delete {:path "target"}))

(defn uber [_]
  (clean nil)
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir})
  (b/compile-clj {:basis @basis
                  :ns-compile '[my.lib.main]
                  :class-dir class-dir})
  (b/uber {:class-dir class-dir
           :uber-file uber-file
           :basis @basis
           :main 'my.lib.main}))

この例では、compile-cljにメインの名前空間をコンパイルするように指示しています(デフォルトでは、ソースはベースの:pathsからロードされます)。コンパイルは推移的であり、コンパイルされた名前空間によってロードされたすべての名前空間もコンパイルされます。コードが動的にまたはオプションでロードされる場合は、追加の名前空間を追加する必要がある場合があります。

deps.ednとビルドの実行は、前の例と同じになります。

次のコマンドでuberjarビルドを作成できます。

clj -T:build uber

このビルドの出力は、target/lib1-1.2.100-standalone.jarにあるuberjarになります。そのjarには、このプロジェクトのコンパイル済みバージョンと、そのすべての依存関係の両方が含まれています。uberjarには、my.lib.main名前空間(-mainメソッドを持つ必要があります)を参照するマニフェストが含まれており、次のように呼び出すことができます。

java -jar target/lib1-1.2.100-standalone.jar

パラメータ化されたビルド

上記のビルドでは、ビルドの側面をパラメータ化せず、呼び出す関数を選択しただけでした。ビルドをパラメータ化して、dev/qa/prod、またはバージョン、またはその他の要因を区別すると便利な場合があります。コマンドラインでの関数チェーンを考慮して、ビルド関数全体で使用する共通のパラメータセットを確立し、各関数がパラメータを渡すようにすることをお勧めします。

たとえば、ローカル開発環境を設定するために追加の開発リソースセットを含むパラメータ化を考えてみましょう。これを表すために、単純な:env :devのキーと値のペアを使用します。

(ns build
  (:require [clojure.tools.build.api :as b]))

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def jar-file (format "target/%s-%s.jar" (name lib) version))
(def copy-srcs ["src" "resources"])

;; delay to defer side effects (artifact downloads)
(def basis (delay (b/create-basis {:project "deps.edn"})))

(defn clean [params]
  (b/delete {:path "target"})
  params)

(defn jar [{:keys [env] :as params}]
  (let [srcs (if (= env :dev) (cons "dev-resources" copy-srcs) copy-srcs)]
    (b/write-pom {:class-dir class-dir
                  :lib lib
                  :version version
                  :basis @basis
                  :src-dirs ["src"]})
    (b/copy-dir {:src-dirs srcs
                 :target-dir class-dir})
    (b/jar {:class-dir class-dir
            :jar-file jar-file})
    params))

deps.ednと呼び出しのその他の側面は同じままです。

:dev環境をアクティブにする呼び出しは次のようになります。

clj -T:build jar :env :dev

キーと値のパラメータは、jar関数に渡されます。

Java/Clojure混在ビルド

発生する一般的なケースとして、主にClojureプロジェクトにJava実装クラスを1つまたは2つ導入する必要がある場合があります。この場合、Javaクラスをコンパイルし、Clojureソースに含める必要があります。このセットアップでは、Clojureソースがsrc/にあり、Javaソースがjava/にあると想定します(これらを実際にどこに配置するかは、もちろんあなた次第です)。

このビルドでは、JavaソースからコンパイルされたクラスとClojureソースを含むjarを作成します。

(ns build
  (:require [clojure.tools.build.api :as b]))

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def jar-file (format "target/%s-%s.jar" (name lib) version))

;; delay to defer side effects (artifact downloads)
(def basis (delay (b/create-basis {:project "deps.edn"})))

(defn clean [_]
  (b/delete {:path "target"}))

(defn compile [_]
  (b/javac {:src-dirs ["java"]
            :class-dir class-dir
            :basis @basis
            :javac-opts ["--release" "11"]}))

(defn jar [_]
  (compile nil)
  (b/write-pom {:class-dir class-dir
                :lib lib
                :version version
                :basis @basis
                :src-dirs ["src"]})
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir})
  (b/jar {:class-dir class-dir
          :jar-file jar-file}))

ここのcompileタスクは、このライブラリの準備タスクとしても使用できます。

タスクドキュメント

詳細なタスクドキュメントについては、APIドキュメントを参照してください。

オリジナル著者: Alex Miller