Deps 与 CLI 指南
Table of Contents
1. 运行 REPL 及使用库
下载并安装工具后, 通过运行 clj 工具来启动一个 REPL:
$ clj
Clojure 1.12.0
user=>
进入 REPL 后, 输入 Clojure 表达式并按回车键进行求值. 输入 Control-D 退出 REPL:
user=> (+ 2 3) # 输入表达式后按 `enter` 键进行求值 5 # 表达式的结果 user=> # 在这里输入 Ctrl-D 退出 REPL (不会打印) $
有许多 Clojure 和 Java 库可供使用, 几乎可以提供需要的任何功能. 例如, 考虑常用的 Clojure 库 clojure.java-time, 用于处理日期和时间.
要使用这个库, 需要将其声明为一个依赖, 这样工具就可以确保它已被下载并添加到 classpath 中. 大多数项目的 readme 文件都会显示要使用的名称和版本. 创建一个 deps.edn 文件来声明依赖:
{:deps {clojure.java-time/clojure.java-time {:mvn/version "1.1.0"}}}
如果不知道有哪些版本, 可以使用 find-versions 工具, 它会按排序顺序列出所有可用的坐标:
$ clj -X:deps find-versions :lib clojure.java-time/clojure.java-time ...omitted {:mvn/version "1.0.0"} {:mvn/version "1.1.0"}
使用 clj 工具重启 REPL:
$ clj Downloading: clojure/java-time/clojure.java-time/1.1.0/clojure.java-time-1.1.0.pom from clojars Downloading: clojure/java-time/clojure.java-time/1.1.0/clojure.java-time-1.1.0.jar from clojars Clojure 1.12.0 user=> (require '[java-time.api :as t]) nil user=> (str (t/instant)) "2022-10-07T16:06:50.067221Z"
第一次使用某个依赖时, 会看到关于库正在下载的消息. 一旦文件被下载 (通常到 ~/.m2 或 ~/.gitlibs), 将来就会被重用. 用同样的方法将其他库添加到 deps.edn 文件中, 并探索 Clojure 或 Java 库.
2. 编写程序
很快你就会想要构建并保存自己使用这些库的代码. 创建一个新目录, 并将这个 deps.edn 文件复制进去:
$ mkdir hello-world $ cp deps.edn hello-world $ cd hello-world $ mkdir src
默认情况下, clj 工具会在 src 目录中寻找源文件. 创建 src/hello.clj:
(ns hello (:require [java-time.api :as t])) (defn time-str "Returns a string representation of a datetime in the local time zone." [instant] (t/format (t/with-zone (t/formatter "hh:mm a") (t/zone-id)) instant)) (defn run [opts] (println "Hello world, the time is" (time-str (t/instant))))
3. 使用 main 函数
这个程序有一个入口函数 run, 可以通过 clj 使用 -X 来执行:
$ clj -X hello/run Hello world, the time is 12:19 PM
4. 使用本地库
项目变大之后, 把应用的一部分移到一个库中很常见. clj 工具使用本地坐标来支持那些只存在于你本地磁盘上的项目. 让我们将这个应用中与 java-time 相关的部分提取到一个并行的 time-lib 目录中的库里. 最终的结构看起来会是这样:
├── time-lib
│ ├── deps.edn
│ └── src
│ └── hello_time.clj
└── hello-world
├── deps.edn
└── src
└── hello.clj
在 time-lib 目录下, 使用你已有的 deps.edn 文件的副本, 并创建一个文件 src/hello_time.clj:
(ns hello-time (:require [java-time.api :as t])) (defn now "Returns the current datetime" [] (t/instant)) (defn time-str "Returns a string representation of a datetime in the local time zone." [instant] (t/format (t/with-zone (t/formatter "hh:mm a") (t/zone-id)) instant))
更新 hello-world/src/hello.clj 中的应用以使用你的库:
(ns hello (:require [hello-time :as ht])) (defn run [opts] (println "Hello world, the time is" (ht/time-str (ht/now))))
修改 hello-world/deps.edn 以使用一个本地坐标, 该坐标指向 time-lib 库的根目录 (确保更新为机器上的路径):
{:deps {time-lib/time-lib {:local/root "../time-lib"}}}
可以从 hello-world 目录运行应用来测试所有东西:
$ clj -X hello/run Hello world, the time is 12:22 PM
5. 使用 git 库
要分享这个库, 可以通过将项目推送到一个公共或私有的 git 仓库, 然后让其他人通过 git 依赖坐标来使用它, 从而实现这一目标.
首先, 为 time-lib 创建一个 git 库:
cd ../time-lib git init git add deps.edn src git commit -m 'init'
然后去一个公共的 git 仓库托管平台 (如 GitHub), 按照说明创建并发布这个 git 仓库.
我们还想给这个发布版本打上标签, 这样它就有一个有意义的版本号:
git tag -a 'v0.0.1' -m 'initial release' git push --tags
最后, 修改应用以使用 git 依赖. 需要以下信息:
- 仓库库 - Clojure CLI 使用一个约定, 如果使用像
io.github.yourname/time-lib这样的库名来代表 GitHub URLhttps://github.com/yourname/time-lib.git, 那么 URL 就不需要指定. - tag -
v0.0.1是我们上面创建的. - sha - 该 tag 对应的短 sha, 如果有本地仓库, 可以用
git rev-parse --short v0.0.1^{commit}找到, 如果是远程仓库, 可以用git ls-remote https://github.com/yourname/time-lib.git v0.0.1. 也可以通过 GitHub 仓库查看 tags 及其对应的 commit 来找到它.
更新 hello-world/deps.edn 以使用 git 坐标:
{:deps {io.github.yourname/time-lib {:git/tag "v0.0.1" :git/sha "4c4a34d"}}}
现在可以再次运行这个应用, 使用 (共享的) git 仓库库. 第一次运行时, 当 clj 下载并缓存仓库和 commit 工作树时, 会在控制台上看到额外的消息:
$ clj -X hello/run Cloning: https://github.com/yourname/time-lib Checking out: https://github.com/yourname/time-lib at 4c4a34d Hello world, the time is 02:10 PM
现在朋友们也可以使用 time-lib 了!
6. 其他示例
随着程序变得越来越复杂, 需要创建标准 classpath 的变体. Clojure 工具支持使用别名 (aliases) 来修改 classpath, 别名是 deps 文件的一部分, 只有在提供了相应别名时才会被使用. 可以做的一些事情包括:
- 包含一个测试源码目录
- 使用测试运行器运行所有测试
- 添加一个可选依赖
- 从命令行添加一个依赖
- 准备源码依赖库
- 覆盖一个依赖的版本
- 使用本地磁盘上的一个 jar 文件
- 预先 (AOT) 编译
- 运行一个套接字服务器远程 REPL
6.1. 包含测试源码目录
通常, 项目的 classpath 默认只包含项目源码, 不包含其测试源码. 可以在 classpath 构建的 make-classpath 步骤中, 将额外的路径作为主 classpath 的修改项添加进去.
为此, 添加一个别名 :test, 其中包含额外的相对源码路径 "test":
{:deps {org.clojure/core.async {:mvn/version "1.3.610"}} :aliases {:test {:extra-paths ["test"]}}}
应用该 classpath 修改, 并通过调用 clj -A:test -Spath 来检查修改后的 classpath:
$ clj -A:test -Spath test: src: /Users/me/.m2/repository/org/clojure/clojure/1.12.0/clojure-1.12.0.jar: ... same as before (split here for readability)
注意 test 目录现在已经被包含在 classpath 中了.
6.2. 使用测试运行器运行所有测试
可以扩展上一节中的 :test 别名, 以包含 cognitect-labs test-runner 来运行所有的 clojure.test 测试:
扩展 :test 别名:
{:deps {org.clojure/core.async {:mvn/version "1.3.610"}} :aliases {:test {:extra-paths ["test"] :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} :main-opts ["-m" "cognitect.test-runner"] :exec-fn cognitect.test-runner.api/test}}}
然后使用默认配置执行测试运行器 (运行 test/ 目录下所有 -test 命名空间中的测试):
clj -X:test
6.3. 添加可选依赖
deps.edn 文件中的别名也可以用来添加影响 classpath 的可选依赖:
{:aliases {:bench {:extra-deps {criterium/criterium {:mvn/version "0.4.4"}}}}}
这里, :bench 别名用于添加一个额外的依赖, 即 criterium 基准测试库.
你可以通过添加 :bench 别名来修改依赖解析, 从而将这个依赖添加到你的 classpath 中: clj -A:bench.
6.4. 从命令行添加依赖
在不修改现有 deps.edn 文件或创建新文件的情况下实验一个库会很有帮助.
$ clojure -Sdeps '{:deps {org.clojure/core.async {:mvn/version "1.5.648"}}}' Clojure 1.12.0 user=> (require '[clojure.core.async :as a]) nil
请注意, 由于转义规则, 最好将配置数据用单引号括起来.
6.5. 准备源码依赖库
一些依赖在使用前需要一个准备步骤才能被添加到 classpath 中. 这些库应该在它们的 deps.edn 中说明这个需求:
{:paths ["src" "target/classes"] :deps/prep-lib {:alias :build :fn compile :ensure "target/classes"}}
包含顶层键 :deps/prep-lib 告诉 tools.deps classpath 构建过程, 准备这个库需要额外的步骤, 这可以通过调用 :build 别名中的 compile 函数来执行. 一旦准备步骤完成, 它应该创建路径 "target/classes", 并且可以检查这个路径来确认完成情况.
你可以像依赖任何其他基于源码的库 (可以是 git 或本地) 一样依赖这个库:
{:deps {my/lib {:local/root "../needs-prep"}}}
如果你然后尝试将该库包含在你的 classpath 中, 你会看到一个错误:
$ clj Error building classpath. The following libs must be prepared before use: [my/lib]
然后你可以告诉 CLI 使用这个命令来准备 (对于特定库版本, 这是一个一次性操作):
$ clj -X:deps prep Prepping io.github.puredanger/cool-lib in /Users/me/demo/needs-prep $ clj Clojure 1.12.0 user=>
6.6. 覆盖依赖
你可以组合使用多个别名. 例如, 这个 deps.edn 文件定义了两个别名 - :old-async 用于强制使用一个旧的 core.async 版本, :bench 用于添加一个额外的依赖:
{:deps {org.clojure/core.async {:mvn/version "0.3.465"}} :aliases {:old-async {:override-deps {org.clojure/core.async {:mvn/version "0.3.426"}}} :bench {:extra-deps {criterium/criterium {:mvn/version "0.4.4"}}}}}
按如下方式同时激活两个别名: clj -A:bench:old-async.
6.7. 包含本地磁盘上的 jar 文件
有时你可能需要直接引用磁盘上的一个 jar 文件, 而这个文件并不存在于 Maven 仓库中, 比如一个数据库驱动 jar.
使用本地坐标直接指向一个 jar 文件而不是一个目录来指定本地 jar 依赖:
{:deps {db/driver {:local/root "/path/to/db/driver.jar"}}}
6.8. 预先 (AOT) 编译
当使用 gen-class 或 gen-interface 时, Clojure 源码必须被预先编译 (ahead-of-time compiled) 以生成 Java 类.
这可以通过调用 compile 来完成. 编译后的 class 文件的默认目标位置是 classes/, 这个目录需要被创建并添加到 classpath 中:
$ mkdir classes
编辑 deps.edn 将 "classes" 添加到 paths 中:
{:paths ["src" "classes"]}
在 src/my_class.clj 中使用 gen-class 声明一个类:
(ns my-class) (gen-class :name my_class.MyClass :methods [[hello [] String]]) (defn -hello [this] "Hello, World!")
然后你可以在另一个源文件 src/hello.clj 中使用 :import 引用这个类. 注意, 这个命名空间也通过 :require 添加了, 以便编译可以自动找到所有依赖的命名空间并编译它们.
(ns hello (:require [my-class]) (:import (my_class MyClass))) (defn -main [& args] (let [inst (MyClass.)] (println (.hello inst))))
你可以在 REPL 中编译, 或者运行一个脚本来进行编译:
$ clj -M -e "(compile 'hello)"
然后运行 hello 命名空间:
$ clj -M -m hello Hello, World!
有关完整的参考信息, 请参阅 Compilation and Class Generation.
6.9. 运行套接字服务器远程 REPL
Clojure 内置了对运行套接字服务器的支持, 特别是使用它们来托管远程 REPL.
要配置一个套接字服务器 REPL, 将以下基本配置添加到你的 deps.edn 中:
{:aliases {:repl-server {:exec-fn clojure.core.server/start-server :exec-args {:name "repl-server" :port 5555 :accept clojure.core.server/repl :server-daemon false}}}}
然后通过使用别名调用来启动服务器:
clojure -X:repl-server
如果你愿意, 也可以在命令行上覆盖默认参数 (或添加额外的选项):
clojure -X:repl-server :port 51234
你可以使用 netcat 从另一个终端连接:
nc localhost 51234 user=> (+ 1 1) 2
使用 Ctrl-D 退出 REPL, 使用 Ctrl-C 退出服务器.
6.10. 列出所有依赖
内置的 :deps 别名中有几个有用的工具, 可以用来探索你的项目所使用的全部传递性依赖 (以及它们的许可证).
要列出你的 classpath 中包含的全部依赖, 使用 clj -X:deps list. 例如, 在本指南顶部的 hello-world 应用中, 你会看到类似这样的东西:
% clj -X:deps list clojure.java-time/clojure.java-time 1.1.0 (MIT) org.clojure/clojure 1.12.0 (EPL-1.0) org.clojure/core.specs.alpha 0.2.62 (EPL-1.0) org.clojure/spec.alpha 0.3.218 (EPL-1.0) time-lib/time-lib ../cli-getting-started/time-lib
你的应用所使用的全部传递性依赖会按字母顺序列出, 并附带版本和许可证信息. 有关其他打印选项, 请参阅 api 文档.
如果你想了解依赖的树形结构以及版本选择是如何做出的, 使用 clj -X:deps tree:
% clj -X:deps tree org.clojure/clojure 1.12.0 . org.clojure/spec.alpha 0.3.218 . org.clojure/core.specs.alpha 0.2.62 time-lib/time-lib /Users/alex.miller/tmp/cli-getting-started/time-lib . clojure.java-time/clojure.java-time 1.1.0
这里没有进行版本选择, 但如果需要, 请参阅文档了解更多关于如何在树中解释选择的信息.
这两个辅助函数都接受一个可选的 :aliases 参数, 如果你希望在应用一个或多个别名的情况下检查依赖列表或树, 例如 clj -X:deps list :aliases '[:alias1 :alias2]'.
原文作者: Alex Miller