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 URL https://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-classgen-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

Author: 青岛红创翻译

Created: 2025-11-18 Tue 09:41