Clojure deps & clj guide
Clojure 命令行工具非常有用,但是一直没有仔细了解其功能,每次用的时候,就会手忙脚乱。现在参考 Clojure 官网的 ‘Deps and CLI Guide’ 系统学习学习。
Clojure 命令行的作用:
- 运行 REPL
- 运行 Clojure 程序
- 解析执行 Clojure 语句
所有这些操作,都可能会用到 Clojure 或 Java 的程序库,可以用 deps.edn
文件来指定程序库的来源和版本,从而可以让 Clojure 自动的下载程序库、添加到 JVM classpath。
1. 安装 Clojure 命令行
这个网页 https://clojure.org/guides/install_clojure 介绍了命令行下的安装方法:
curl -O https://download.clojure.org/install/linux-install-1.11.1.1208.sh chmod +x linux-install-1.11.1.1208.sh ./linux-install-1.11.1.1208.sh
运行结果如下:
(这里,我的命令行提示符是 # 符号)
# curl -O https://download.clojure.org/install/linux-install-1.11.1.1208.sh % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1828 100 1828 0 0 858 0 0:00:02 0:00:02 --:--:-- 858 # chmod +x linux-install-1.11.1.1208.sh # ./linux-install-1.11.1.1208.sh Downloading and expanding tar % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 17.1M 100 17.1M 0 0 2471k 0 0:00:07 0:00:07 --:--:-- 3630k Installing libs into /usr/local/lib/clojure Installing clojure and clj into /usr/local/bin Installing man pages into /usr/local/share/man/man1 Removing download Use clj -h for help
让后我们可以用 clj -h
打印出一堆帮助信息:
# clj -h Version: 1.11.1.1208 You use the Clojure tools ('clj' or 'clojure') to run Clojure programs on the JVM, e.g. to start a REPL or invoke a specific function with data. The Clojure tools will configure the JVM process by defining a classpath (of desired libraries), an execution environment (JVM options) and specifying a main class and args. Using a deps.edn file (or files), you tell Clojure where your source code resides and what libraries you need. Clojure will then calculate the full set of required libraries and a classpath, caching expensive parts of this process for better performance. The internal steps of the Clojure tools, as well as the Clojure functions you intend to run, are parameterized by data structures, often maps. Shell command lines are not optimized for passing nested data, so instead you will put the data structures in your deps.edn file and refer to them on the command line via 'aliases' - keywords that name data structures. 'clj' and 'clojure' differ in that 'clj' has extra support for use as a REPL in a terminal, and should be preferred unless you don't want that support, then use 'clojure'. Usage: Start a REPL clj [clj-opt*] [-Aaliases] [init-opt*] Exec fn(s) clojure [clj-opt*] -X[aliases] a/fn? [kpath v]* kv-map? Run tool clojure [clj-opt*] -T[name|aliases] a/fn [kpath v] kv-map? Run main clojure [clj-opt*] -M[aliases] [init-opt*] [main-opt] [arg*] Prepare clojure [clj-opt*] -P [other exec opts] exec-opts: -Aaliases Use concatenated aliases to modify classpath -X[aliases] Use concatenated aliases to modify classpath or supply exec fn/args -T[toolname|aliases] Invoke tool by name or via aliases ala -X -M[aliases] Use concatenated aliases to modify classpath or supply main opts -P Prepare deps - download libs, cache classpath, but don't exec clj-opts: -Jopt Pass opt through in java_opts, ex: -J-Xmx512m -Sdeps EDN Deps data to use as the last deps file to be merged -Spath Compute classpath and echo to stdout only -Spom Generate (or update) pom.xml with deps and paths -Stree Print dependency tree -Scp CP Do NOT compute or cache classpath, use this one instead -Srepro Ignore the ~/.clojure/deps.edn config file -Sforce Force recomputation of the classpath (don't use the cache) -Sverbose Print important path info to console -Sdescribe Print environment and command parsing info as data -Sthreads Set specific number of download threads -Strace Write a trace.edn file that traces deps expansion -- Stop parsing dep options and pass remaining arguments to clojure.main --version Print the version to stdout and exit -version Print the version to stderr and exit init-opt: -i, --init path Load a file or resource -e, --eval string Eval exprs in string; print non-nil values --report target Report uncaught exception to "file" (default), "stderr", or "none" main-opt: -m, --main ns-name Call the -main function from namespace w/args -r, --repl Run a repl path Run a script from a file or resource - Run a script from standard input -h, -?, --help Print this help message and exit Programs provided by :deps alias: -X:deps list List full transitive deps set and licenses -X:deps tree Print deps tree -X:deps find-versions Find available versions of a library -X:deps find-versions Find available versions of a library -X:deps prep Prepare all unprepped libs in the dep tree -X:deps mvn-install Install a maven jar to the local repository cache -X:deps git-resolve-tags Resolve git coord tags to shas and update deps.edn For more info, see: https://clojure.org/guides/deps_and_cli https://clojure.org/reference/repl_and_main
2. 运行 REPL
# clj Clojure 1.11.1 user=> (+ 2 3) 5 user=>
创建 deps.edn
添加程序库,比如 clojure.java-time
用来处理时间对象。
{:deps {clojure.java-time/clojure.java-time {:mvn/version "1.1.0"}}}
命令行中,也可以查询程序库的版本:
# clj -X:deps find-versions :lib clojure.java-time/clojure.java-time {:mvn/version "0.3.0"} {:mvn/version "0.3.1"} {:mvn/version "0.3.2"} {:mvn/version "0.3.3"} {:mvn/version "1.0.0-SNAPSHOT"} {:mvn/version "1.0.0"} {:mvn/version "1.1.0"} {:mvn/version "1.2.0"}
既然,有新版本,那么,我们修改一下 deps.edn
{:deps {clojure.java-time/clojure.java-time {:mvn/version "1.2.0"}}}
再一次在 deps.edn
所在的文件夹运行 clj:
# clj Downloading: clojure/java-time/clojure.java-time/1.2.0/clojure.java-time-1.2.0.pom from clojars Downloading: clojure/java-time/clojure.java-time/1.2.0/clojure.java-time-1.2.0.jar from clojars Clojure 1.11.1 user=> (require '[java-time.api :as t]) nil user=> (str (t/instant)) "2023-01-20T02:19:56.912808900Z"
我们就能在 REPL 中 require
所 deps.edn
指定的程序库啦。因为 clj
会在后台下载程序库,并把程序库加载到 classpath
中供我们调用。
下载到的程序库,一般保存 ~/.m2
或 ~/.gitlibs
,比如刚才的库就在这里:
# ls ~/.m2/repository/clojure/java-time/clojure.java-time/1.2.0/ -rw-r--r-- 1 63991 Jan 20 10:19 clojure.java-time-1.2.0.jar -rw-r--r-- 1 40 Jan 20 10:19 clojure.java-time-1.2.0.jar.sha1 -rw-r--r-- 1 3546 Jan 20 10:19 clojure.java-time-1.2.0.pom -rw-r--r-- 1 40 Jan 20 10:19 clojure.java-time-1.2.0.pom.sha1 -rw-r--r-- 1 218 Jan 20 10:19 _remote.repositories
3. 写程序
接下来,官方指南教我们怎么写程序。
在 deps.edn
同一级文件夹下,创建一个 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)))) (defn -main[] (run {:name "Clojure"}))
然后,运行:
# clj -X hello/run Hello world, the time is 10:28 AM
4. 使用本地代码库
我们也可以提取 time-str
这个函数到另一个工程。这样可以重复使用共用代码。
在上面那个工程文件夹同一级,创建 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
项目的 deps.edn
{:deps {time-lib/time-lib {:local/root "../time-lib"}}
意思 time-lib/time-lib
程序库在本文件夹的上一级文件夹下的 time-lib
hello.clj
就可以简化如下:
(ns hello (:require [hello-time :as ht])) (defn run [opts] (println "Hello world, the time is" (ht/time-str (ht/now)))) (defn -main[] (run {:name "Clojure"}))
运行效果一样:
# clj -X hello/run Hello world, the time is 10:58 AM
5. 使用 Github 库
接着,把刚才创建的 time-lib
文件夹内容 push 到 github:https://github.com/kimim/time-lib
创建tag,查看日志:
# git tag -a '0.0.1' -m 'initial release' # git push --tags Enumerating objects: 1, done. Counting objects: 100% (1/1), done. Writing objects: 100% (1/1), 160 bytes | 80.00 KiB/s, done. Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:kimim/time-lib.git * [new tag] 0.0.1 -> 0.0.1 # git rev-parse --short 0.0.1^{commit} 6514c10
{:deps {io.github.kimim/time-lib {:git/tag "0.0.1" :git/sha "6514c10"}}}
运行效果如下,clj 会去 clone github 库:
# clj -X hello/run Checking out: https://github.com/kimim/time-lib.git at 6514c1022a78ace8548552b3db64f6efa58448e2 Downloading: clojure/java-time/clojure.java-time/1.2.0/clojure.java-time-1.2.0.pom from clojars Downloading: clojure/java-time/clojure.java-time/1.2.0/clojure.java-time-1.2.0.jar from clojars Hello world, the time is 03:23 AM
6. 添加额外的 classpath
clj 可以用 -A
选项添加 deps.edn
中 :alias
指定的库,比如:
{:deps {io.github.kimim/time-lib {:git/tag "0.0.1" :git/sha "6514c10"}} :aliases {:test {:extra-paths ["test"]} :bench {:extra-deps {criterium/criterium {:mvn/version "0.4.4"}}}}}
通过一下命令行参数,就可以在 classpath 中添加 test 文件夹,criterium 库:
clj -A:bench:test
7. Sean Corfield 的 clj-new
大神 Sean Corfield 扩展了很多 clj 的功能: https://github.com/seancorfield/clj-new
添加以下 alias
,就可以运行模块内的 -main
函数、特定函数、测试、编译打包。
:aliases {:run-m {:main-opts ["-m" "hello"]} :run-x {:ns-default hello :exec-fn run :exec-args {:name "Clojure"}} :build {:deps {io.github.seancorfield/build-clj {:git/tag "v0.4.0" :git/sha "54e39ae"}} :ns-default build} :test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"} io.github.cognitect-labs/test-runner {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}
# clj -M:run-m Hello world, the time is 03:50 AM # clj -X:run-x Hello world, the time is 03:50 AM # clj -T:build test Running task for: test Running tests in #{"test"} Testing user Ran 0 tests containing 0 assertions. 0 failures, 0 errors. # clj -T:build ci Running task for: test Running tests in #{"test"} Testing user Ran 0 tests containing 0 assertions. 0 failures, 0 errors. Cleaning target... Writing pom.xml... Skipping coordinate: {:git/tag 0.0.1, :git/sha 6514c1022a78ace8548552b3db64f6efa58448e2, :git/url https://github.com/kimim/time-lib.git, :deps/manifest :deps, :deps/root /home/learn/.gitlibs/libs/io.github.kimim/time-lib/6514c1022a78ace8548552b3db64f6efa58448e2, :parents #{[]}, :paths [/home/learn/.gitlibs/libs/io.github.kimim/time-lib/6514c1022a78ace8548552b3db64f6efa58448e2/src]} Copying src, resources... Compiling hello... Building uberjar target/hello-0.1.0-SNAPSHOT.jar...
当然,为了使用 build
,还需要在 deps.edn
同一层级添加一个 build.clj
文件:
(ns build (:refer-clojure :exclude [test]) (:require [org.corfield.build :as bb])) (def lib 'net.clojars.kimim/hello) (def version "0.1.0-SNAPSHOT") (def main 'hello) (defn test "Run the tests." [opts] (bb/run-tests opts)) (defn ci "Run the CI pipeline of tests (and build the uberjar)." [opts] (-> opts (assoc :lib lib :version version :main main) (bb/run-tests) (bb/clean) (bb/uber)))
大神也开发了 clj-new
可以直接从模板生成工程文件:
安装 clj-new
# clojure -Ttools install com.github.seancorfield/clj-new '{:git/tag "v1.2.399"}' :as clj-new Cloning: https://github.com/clojure/tools.tools.git ...
从模板生成 app
clojure -Tclj-new app :name myname/myapp Downloading: org/clojure/tools.deps.alpha/0.12.1109/tools.deps.alpha-0.12.1109.pom from central ...
运行:
cd myapp/ # ls CHANGELOG.md LICENSE README.md build.clj deps.edn doc pom.xml resources src test # clj -M:run-m Hello, World! # clj -X:run-x Hello, Clojure!
测试
clj -T:build test Checking out: https://github.com/seancorfield/build-clj.git at 0ffdb4c0f2cd7ef484458502b926fbe63efe540b Checking out: https://github.com/clojure/tools.build.git at ba1a2bf421838802e7bdefc541b41f57582e53b6 Checking out: https://github.com/seancorfield/build-uber-log4j2-handler.git at 55fb6f63ea3cc5344e67e87d2322570d4dddd3d5 Downloading: org/clojure/tools.deps.alpha/0.14.1178/tools.deps.alpha-0.14.1178.pom from central Downloading: org/clojure/tools.namespace/1.3.0/tools.namespace-1.3.0.pom from central ... Running task for: test Downloading: org/clojure/test.check/1.1.1/test.check-1.1.1.pom from central Downloading: org/clojure/test.check/1.1.1/test.check-1.1.1.jar from central Running tests in #{"test"} Testing myname.myapp-test FAIL in (a-test) (myapp_test.clj:7) FIXME, I fail. expected: (= 0 1) actual: (not (= 0 1)) Ran 1 tests containing 1 assertions. 1 failures, 0 errors. Execution error (ExceptionInfo) at org.corfield.build/run-task (build.clj:324). Task failed for: test Full report at: /tmp/clojure-12133550799243732811.edn