{: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}}}
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 程序来满足这些用例。
最常见的 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。
重要的是,您的主 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 源代码一起包含。在此设置中,我们将假设您的 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