Clojure

tools.build 指南

tools.build 是用於建置 Clojure 專案的函式庫。它用於建置程式中,以建立使用者可呼叫的目標函式。另請參閱 API 文件

建置是程式

tools.build 背後的哲學是,專案建置本質上是一個程式 - 一系列指令,用於從專案來源檔案建立一個或多個專案成品。我們希望使用我們最喜歡的程式語言 Clojure 來撰寫這個程式,而 tools.build 是建置中常見函式庫,可以用靈活的方式連接在一起。撰寫建置程式確實需要比其他宣告式方法更多的程式碼,但可以很容易地擴充或自訂到未來,建立一個隨著專案成長而成長的建置。

設定

沒有安裝步驟 - tools.build 僅是一個您的建置程式使用的函式庫。您會在您的 deps.edn 中建立一個別名,其中包含 tools.build 作為相依性,以及建置程式的來源路徑。建置被設計成可以輕鬆地作為 Clojure CLI 中的專案「工具」執行(使用 -T)。在 Clojure CLI 中,「工具」是提供功能且不使用您的專案相依性或類別路徑的程式。使用 -T:an-alias 執行的工具會移除所有專案相依性和路徑,新增 "." 作為路徑,並包含在 :an-alias 中定義的任何其他相依性或路徑。

因此,您需要在您的 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 找到最新的標籤和 SHA 以使用。

本指南中的 git 相依性和 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)和傳遞相依性

  • org.clojure/tools.build(來自 :build 別名 :deps)和傳遞相依性

:ns-default 指定預設的 Clojure 命名空間以在類別路徑中找到指定的函式。由於唯一的本機路徑是預設的 ".",我們應預期在我們專案的根目錄中於 build.clj 找到建置程式。請注意,路徑根目錄(透過 :build 別名 :paths)和建置程式本身的命名空間相對於這些路徑根目錄完全在您的控制之下。您可能也希望將它們放在您專案的子目錄中。

然後最後,在命令列中,我們指定在建置中執行的函式,這裡是 jar。該函式將在 build 命名空間中執行,並傳遞使用與 -X 相同的 arg 傳遞樣式建立的地圖 - arg 提供為交替的鍵和值。

本指南的其餘部分將展示個別常見的使用案例,以及如何使用 tools.build 程式滿足這些案例。

來源程式庫 jar 建置

最常見的 Clojure 建置會建立一個包含 Clojure 來源碼的 jar 檔案。若要使用 tools.build 執行此操作,我們將使用下列任務

  • create-basis - 建立專案基礎 (注意:這會下載相依項作為副作用)

  • 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 中互動式地開發它。

  • 作為單一用途的程式,在頂端的變數集中建置共用資料是可以的。

  • 我們選擇在「target」目錄中建置,並在「target/classes」中組裝 jar 內容,但這些路徑沒有什麼特別之處 - 它完全在你控制之下。此外,我們在這裡重複了這些路徑和其他路徑,但你可以移除重複的部分,直到感覺正確為止。

  • 我們使用 tools.build 任務函數為使用者組裝較大的函數,例如 build/jar。這些函數會取得參數映射,我們選擇在此處不提供任何可設定的參數,但你可以提供!

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 和建置執行將與前一個範例相同。

你可以使用以下指令建立 uber jar 建置

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

參數化建置

在上述建置中,我們並未參數化建置的任何面向,只選擇要呼叫哪些函數。您可能會發現參數化建置有助於區分開發/品質保證/生產或版本或其他因素。為考量命令列中的函數串接,建議建立要跨建置函數使用的共同參數集,並讓每個函數傳遞參數。

例如,考慮包含一組額外開發資源的參數化,以設定本機開發人員環境。我們將使用簡單的 :env :dev kv 配對來表示這一點

(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

kv 參數傳遞給 jar 函數。

混合 Java/Clojure 建置

常見的情況是需要在大部分為 Clojure 的專案中引入一或兩個 Java 實作類別。在這種情況下,您需要編譯 Java 類別並將它們包含在 Clojure 原始碼中。在此設定中,我們假設您的 Clojure 原始碼位於 src/ 中,而 Java 原始碼位於 java/ 中(您實際放置這些檔案的位置當然取決於您)。

此建置會建立一個 jar 檔,其中包含從 Java 原始碼和 Clojure 原始碼編譯的類別。

(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