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 中查找要使用的最新 TAG 和 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 传递样式构建的映射——参数作为交替的键和值提供。

本指南的其余部分演示了单个常见用例以及如何使用 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 中交互式地开发它。

  • 作为单一用途的程序,在顶部的 var 集中构建共享数据是可以的。

  • 我们选择在“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 构建

一个常见的案例是需要将一两个 Java 实现类引入一个主要由 Clojure 组成的项目中。在这种情况下,您需要编译 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 任务也可以用作此库的 prep 任务

任务文档

有关详细的任务文档,请参阅 API 文档

原作者:Alex Miller