Babashka 脚本化与任务自动化:原生镜像、bb 任务与生产实践

深入 Babashka 的脚本化与自动化实践:原生镜像(GraalVM native-image)原理与毫秒级启动优势、bb 任务(bb.edn)与脚本组织、内置库与 pods 扩展、文件与进程与 HTTP 操作、本地与 CI 自动化实战、与 JVM Clojure 的取舍边界,帮你把 Babashka 用成一套轻量、快速、可移植的脚本与任务运行器。

Clojure 在 JVM 上很美,但写「跑一次就完」的脚本时,JVM 的启动开销、依赖分发和冷启动就成了负担。Babashka(bb)把 Clojure 解释器(SCI)用 GraalVM native-image 编译成原生二进制,启动只需几毫秒,却保留了 Clojure 的语法与大部分核心库。本文从原生镜像原理讲到 CI 集成,覆盖 bb.edn 任务、脚本组织、内置库与 pods、文件/进程/HTTP 操作、自动化实战,以及与 JVM Clojure 的取舍边界,帮你把 Babashka 用成一套轻量、快速、可移植的脚本运行器。

1. Babashka 是什么

1.1 脚本场景的痛点

在 JVM 上跑 Clojure 脚本,三个绕不开的负担:

  • 启动慢:clojure -M script.clj 要先起 JVM、加载类、解析依赖,冷启动常在数百毫秒到数秒。
  • 分发难:脚本要跑在同事机器、CI runner、容器里,都得先装 JDK 与 Clojure CLI。
  • 依赖解析等待:deps.edn 首次解析要联网下载,离线环境直接卡住。

Babashka 的答案是:把 Clojure 解释器编译成一个独立的原生二进制。

1.2 原生镜像原理

Babashka 由三块组成:

SCI(Small Clojure Interpreter)——纯 Clojure 写的解释器
  ↓ 编译期
GraalVM native-image —— 把 JVM 字节码 AOT 成机器码
  ↓
bb 二进制 —— 单文件、无 JVM 依赖、毫秒启动

关键点:Babashka 并不是把「你的脚本」编译成原生镜像(那是 native-image 干的事),而是预先把解释器本身编译好;你的脚本运行时仍由 SCI 解释执行。

1.3 启动对比

运行方式冷启动依赖分发体积
clojure -M500ms~3sJDK + CLI数百 MB
clojure -M 首次带 deps数秒~数十秒需联网同上
Babashka bb5~20ms无单文件数十 MB
bb 热启动1~5ms无同上

心智:Babashka 把「Clojure 的语法」和「JVM 的启动成本」解耦了——你得到熟悉的语言,却付出接近 shell 的启动代价。

2. 安装与 bb 任务

2.1 安装

# macOS
brew install borkdude/brew/babashka

# 或官方安装脚本
curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install
chmod +x install && ./install

# 验证
bb --version

2.2 bb.edn 任务定义

bb.edn 是项目的任务与依赖清单:

{:paths ["src" "script"]
 :deps  {org.clojure/data.json {:mvn/version "2.5.0"}}
 :tasks
 {;; 简单任务:调用外部命令
  test   {:doc "运行测试"
          :task (shell "clojure -M:test")}

  ;; 带额外依赖的任务
  lint   {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2024.08.01"}}
          :requires ([clj-kondo.main])
          :task (clj-kondo.main/main "--lint" "src")}

  ;; 组合任务:依赖其他任务
  ci     {:doc "CI 流水线"
          :depends [lint test]}

  ;; 默认任务(直接 bb 时执行)
  default {:task (println "用法: bb <task>")}}}

运行:

bb lint        # 跑 lint 任务
bb ci          # 先 lint 再 test
bb             # 跑 default
bb tasks       # 列出所有任务

2.3 任务依赖与并行

bb.edn 的任务依赖是串行的;需要并行可在任务体里用 future:

{:tasks
 {build    {:task (do (println "compile") nil)}
  lint     {:task (do (println "lint") nil)}
  parallel {:task (do (future (shell "bb build"))
                      (future (shell "bb lint"))
                      nil)}}}

心法:bb.edn 是「Makefile 的 Clojure 版」——任务用数据描述、依赖显式声明、可引用 Clojure 函数。把 lint/test/build/release 都收进 bb.edn,团队只需记一个命令 bb。

3. 脚本与命令行

3.1 直接执行脚本

bb -e '(println (+ 1 2))'          # 执行表达式
bb script.clj                      # 执行文件
bb -f script.clj                   # 同 -f
cat script.clj | bb -              # 从 stdin

脚本就是一个普通 Clojure 文件:

;;; script.clj
(require '[babashka.fs :as fs]
         '[babashka.process :refer [shell]])

(let [files (fs/glob "." "**/*.clj")]
  (println "找到" (count files) "个文件")
  (shell "wc" "-l" (map str files)))

3.2 参数解析

Babashka 内置 babashka.cli,无需额外依赖:

(require '[babashka.cli :as cli])

(def opts
  (cli/parse-opts *command-line-args*
                  {:spec {:port  {:coerce :long :default 8080}
                          :env   {:coerce :keyword}
                          :debug {:coerce :boolean}}}))

(println "端口" (:port opts) "环境" (:env opts))
bb script.clj --port 9000 --env prod --debug
# => 端口 9000 环境 :prod

3.3 shebang 让脚本可执行

#!/usr/bin/env bb
(require '[babashka.fs :as fs])
(println (fs/absolutize "."))
chmod +x deploy.clj
./deploy.clj

心法:shebang + bb 等于「Clojure 版 Python 脚本」——一个文件、可执行、无需编译,直接进 git 当运维脚本。这是 Babashka 最被低估的用法。

4. 内置库与 pods

4.1 内置库清单

Babashka 预置了一批常用库,require 即用,无需声明依赖:

命名空间用途
babashka.fs文件系统(跨平台)
babashka.process子进程与 shell
babashka.http-clientHTTP 客户端
babashka.cli命令行参数解析
babashka.jsonJSON 读写
cheshire.coreJSON(兼容别名)
clojure.core.async异步通道
clojure.data.csvCSV 读写
clojure.set集合操作
clojure.string字符串处理

4.2 pods 突破内置库边界

pods 是用其他语言(Go/Rust/Clojure)写的外部进程,通过 stdio 上的 EDN 协议与 bb 通信,把「Babashka 没内置的能力」接进来:

bb 进程  <── EDN 消息 ──>  pod 进程(Go 写的二进制)

常见 pods:pod-babashka-go-sqlite3(SQLite)、pod-babashka-postgresql、pod-babashka-lanterna(终端 UI)。

4.3 使用 pod

;; bb.edn 声明 pod
{:pods {org.babashka/go-sqlite3 {:version "0.3.5"}}}

;; 代码里加载
(require '[babashka.pods :as pods])
(pods/load-pod 'org.babashka/go-sqlite3 "0.3.5")
(require '[pod.babashka.go-sqlite3 :as sqlite])

(def db (sqlite/open "app.db"))
(sqlite/execute! db ["create table t (id integer)"])

心法:内置库覆盖 80% 场景,pods 补剩下 20%——pods 有进程启动与序列化开销,别把它当 JVM 依赖用;能用内置库就用内置库,真需要 SQLite/Postgres 这类重能力时才上 pod。

5. 文件与进程操作

5.1 文件读写

(require '[babashka.fs :as fs]
         '[clojure.string :as str])

;; 读
(slurp "config.edn")
(str/split-lines (slurp "data.txt"))

;; 写
(spit "out.txt" "hello\n")

;; 复制/移动/删除
(fs/copy "a.txt" "b.txt")
(fs/move "b.txt" "sub/b.txt" {:replace-existing true})
(fs/delete "tmp.txt")

5.2 目录遍历与匹配

;; 递归找所有 .clj 文件
(fs/glob "." "**/*.clj")

;; 过滤大文件
(->> (fs/glob "." "**/*")
     (filter fs/regular-file?)
     (filter #(> (fs/size %) (* 10 1024 1024)))
     (map str))

;; 创建临时目录
(let [tmp (fs/create-temp-dir)]
  (spit (fs/file tmp "x.txt") "data"))

5.3 子进程

(require '[babashka.process :as p :refer [shell process]])

;; 简单执行(继承 stdout)
(shell "git" "status")

;; 捕获输出
(-> (shell {:out :string} "git" "rev-parse" "HEAD")
    :out
    str/trim)

;; 流式管道
(-> (process {:out :string} "cat" "big.txt")
    :out
    (str/split-lines)
    count)

;; 管道串联
(->> (process "find . -name '*.clj'")
     (p/process "wc -l" {:in (:out *1)}))

心法:babashka.process 是 shell 脚本的 Clojure 替身——shell 处理同步执行,process 处理流式管道,pipeline 串联多进程。用它替代脆弱的 bash 管道,脚本立刻可读、可测、可移植。

6. HTTP 与网络

6.1 HTTP 客户端

(require '[babashka.http-client :as http]
         '[babashka.json :as json])

;; GET
(def resp (http/get "https://api.github.com/repos/babashka/babashka"))
(:status resp)                               ;; 200
(json/read-str (:body resp) :key-fn keyword) ;; 解析 JSON

;; POST
(http/post "https://httpbin.org/post"
           {:headers {"Content-Type" "application/json"}
            :body    (json/write-str {:name "bb" :ver "1.0"})})

;; 超时
(http/get "https://example.com" {:timeout 3000})

6.2 起一个 HTTP 服务

Babashka 内置 org.httpkit.server,可直接起服务:

(require '[org.httpkit.server :as server])

(defn handler [req]
  {:status  200
   :headers {"Content-Type" "application/json"}
   :body    "{\"ok\":true}"})

(server/run-server handler {:port 8080})
@(promise)   ;; 阻塞不退

6.3 处理 JSON 与 EDN

(require '[babashka.json :as json])

(json/write-str {:a 1 :b [1 2 3]})           ;; => "{\"a\":1,\"b\":[1,2,3]}"
(json/read-str "{\"x\":1}" :key-fn keyword)  ;; => {:x 1}

心法:Babashka 内置 HTTP 客户端与服务端——小工具、健康检查、webhook 接收器、内网 API 代理都能用几十行搞定,不必拉整个 JVM web 栈。

7. 任务自动化实战

7.1 构建与发布脚本

#!/usr/bin/env bb
;;; release.clj —— 打 tag、推送、发布
(require '[babashka.process :refer [shell]]
         '[babashka.cli :as cli]
         '[clojure.string :as str])

(def {:keys [version dry-run]}
  (cli/parse-opts *command-line-args*
                  {:spec {:version {:coerce :string :require true}
                          :dry-run {:coerce :boolean}}}))

(defn run! [& args]
  (println "执行" (str/join " " args))
  (when-not dry-run (apply shell args)))

(run! "git" "tag" (str "v" version))
(run! "git" "push" "origin" (str "v" version))
(println "发布" version (if dry-run "(演练)" "完成"))

7.2 文件监听与热重载

;; 简易文件监听:变更即执行任务
(require '[babashka.fs :as fs]
         '[babashka.process :refer [shell]])

(defn watch-loop [dir f]
  (let [seen (atom {})]
    (loop []
      (doseq [file (fs/glob dir "**/*.clj")]
        (let [m (fs/last-modified-time file)]
          (when (not= m (get @seen file))
            (swap! seen assoc file m)
            (f file))))
      (Thread/sleep 1000)
      (recur))))

(watch-loop "src" (fn [f] (println "变更:" f) (shell "bb test")))

7.3 数据库迁移

;;; migrate.clj —— 顺序执行迁移文件
(require '[babashka.fs :as fs]
         '[babashka.pods :as pods]
         '[clojure.string :as str])
(pods/load-pod 'org.babashka/go-sqlite3 "0.3.5")
(require '[pod.babashka.go-sqlite3 :as sqlite])

(def db (sqlite/open "app.db"))
(sqlite/execute! db ["create table if not exists schema_version (v text)"])

(doseq [f (sort (fs/glob "migrations" "*.sql"))]
  (println "执行" (str f))
  (doseq [stmt (str/split (slurp f) #";")]
    (when (seq (str/trim stmt))
      (sqlite/execute! db [stmt]))))

心法:自动化脚本的三件套是「参数化 + 幂等 + 干跑」——用 babashka.cli 收参数、重复执行不出错、--dry-run 先演练。Babashka 让这三件套几十行搞定,不必为此起一个 Gradle 项目。

8. CI 集成

8.1 GitHub Actions 安装

name: ci
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: DeLaGuardo/setup-clojure@12.5
        with:
          clojure: '1.12'
      - name: 安装 Babashka
        uses: turtlequeue/setup-babashka@v1.7.0
        with:
          babashka-version: 1.12.196
      - run: bb ci

8.2 用 bb 做 CI 胶水

Babashka 最适合当 CI 里的「胶水层」——把多步逻辑写成一个脚本:

;;; ci.clj
(require '[babashka.process :refer [shell]])

(defn step [name f]
  (println (str "== " name " =="))
  (let [r (f)]
    (when-not (zero? (:exit r))
      (println "失败:" name)
      (System/exit 1))))

(step "lint"  #(shell "clojure -M:clj-kondo --lint src"))
(step "test"  #(shell "clojure -X:test"))
(step "build" #(shell "clojure -T:build jar"))
(println "全部通过")

8.3 常见陷阱

陷阱现象规避
用了 JVM 专属类ClassNotFoundException查内置库清单
依赖 mvn 版本太重启动变慢优先内置库
假设有反射/动态类加载运行时报错改纯 Clojure 写法
忽略 pod 启动开销循环里反复 loadpod 只加载一次
用 System/exit 无清理临时文件残留用 try/finally

心法:CI 里把「环境准备」交给 action,把「业务步骤」交给 bb 脚本——这样本地和 CI 跑的是同一段 Clojure,避免「在我机器上能过」的经典问题。

9. 与 JVM Clojure 的取舍

9.1 能力边界

Babashka 并不是完整 Clojure:

  • 不能加载任意 JVM 库(只有内置 + pods + 部分纯 Clojure 库)。
  • 无 gen-class、无动态类加载、无 JNI、无反射。
  • STM(ref/dosync)不可用,多线程原语受限。
  • defrecord/deftype 语义有差异。

9.2 选型对比

维度BabashkaJVM Clojure
启动毫秒秒级
吞吐与长期运行不适合强
任意 JVM 库否是
脚本分发单文件需 JVM
并发原语有限完整
适用场景脚本/任务/CLI/CI服务/大数据/长驻进程

9.3 混合策略

最实用的组合是「bb 管外围,JVM 管核心」:

开发期:bb lint / bb test(毫秒反馈)
构建期:bb 调 clojure -T:build(重活交给 JVM)
运行期:JVM 服务常驻,bb 做运维脚本/健康检查/日志巡检

心法:选型不看「哪个更强」,看「这段代码活多久」——跑一次就退的脚本用 bb,常驻服务用 JVM;两者不是替代关系,而是同一条工具链上的不同齿轮。

10. 速查表与一句话记忆

需求命令或写法
跑表达式bb -e '(println 1)'
跑脚本bb script.clj
定义任务bb.edn 的 :tasks
任务依赖:depends [a b]
参数解析babashka.cli/parse-opts
文件系统babashka.fs
子进程babashka.process/shell
HTTPbabashka.http-client
起服务org.httpkit.server
JSONbabashka.json
扩展能力pods
shebang#!/usr/bin/env bb

一句话记忆:Babashka = GraalVM 原生镜像 + SCI 解释器(毫秒启动、单文件分发)→ bb.edn 任务化(Makefile 的 Clojure 版)→ 内置库覆盖八成场景、pods 补两成 → fs/process/http-client 替代 shell 管道 → CLI 参数化 + 幂等 + 干跑 → CI 里当胶水层 → 跑一次就退用 bb、常驻服务用 JVM——它不取代 Clojure,而是把 Clojure 装进了 shell 脚本的位置。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「clojure」更多文章

  1. Datalog 查询与 Datomic/Xtdb:数据即事实、pull、时间旅行与架构
  2. Clojure 静态检查与格式化工具链:clj-kondo、cljfmt、zprint 与 CI
  3. Clojure 认证授权与安全实践:Ring 安全链、JWT、密码哈希与审计