Apache Ant's task ecosystem, fluent from Clojure. Tasks are
functions; resource collections (fileset, path, dirset,
union, restrict, …) are lazy java.io.File seqs you can
filter/map/transduce over — and pass straight back as
children of any task.
Pre-1.0 alpha. Architecture is stable; surface APIs may shift slightly during the alpha period based on real-world feedback. Once
1.0.0lands, semver applies normally.
(require '[clj-ant.core :as a]
'[clj-ant.tasks :as t]
'[clojure.string :as str])
;; Build a release zip from ordinary files and files inside archives.
;; The plan is data, so lint it before Ant touches the filesystem.
(let [version "1.2.3"
app-dir "target/release/app"
configs (->> (a/files (t/fileset :dir "etc"
:includes "**/*.tmpl,**/*.properties"
:excludes "**/secrets/**"))
(remove #(str/ends-with? (.getName %) ".bak")))
plan [(t/delete :dir "target/release")
(t/mkdir :dir app-dir)
;; A live Clojure seq of java.io.File values, copied by Ant.
;; The filterchain streams token replacement; no temp strings.
(t/copy :todir (str app-dir "/conf")
configs
(t/filterchain
(t/tokenfilter
(t/replacestring :from "@VERSION@" :to version))))
;; Copy directly from inside zip/jar files, without unpacking
;; them first. zipfileset is just another Ant resource collection.
(t/copy :todir (str app-dir "/public")
(t/zipfileset :src "vendor/admin-ui.zip"
:includes "dist/**"
:prefix "admin"))
(t/copy :todir (str app-dir "/licenses")
(t/zipfileset :src "target/app.jar"
:includes "META-INF/LICENSE*,META-INF/NOTICE*"))
(t/zip :destfile (str "target/app-" version ".zip")
(t/fileset :dir app-dir))]]
(when-some [issues (seq (a/lint plan))]
(throw (ex-info "Invalid Ant plan" {:issues issues})))
(a/ant plan))
That single expression weaves together things vanilla Clojure rarely
composes cleanly: Ant's pattern grammar, lazy resource collections,
Clojure filtering, streaming text transforms, archive-entry copying,
plan validation, and zip creation. No XML, no shelling out to zip /
unzip, no temporary extraction just to grab files from a jar.
Clojure has excellent build tooling for Clojure code (tools.build,
tools.deps, bb). But the long tail of real-world build operations
— sign a jar, scp it somewhere, restart a remote service, replace
tokens in a config file, audit zip entries, run a command per file
— is exactly what Apache Ant has been good at for 20 years.
clj-ant gives you Ant's ~470 tasks and types as Clojure functions
that return data, with full Ant semantics underneath: property
expansion, refid, macrodef, target dependency resolution, custom
taskdefs. Compose them with the rest of your Clojure code freely.
;; deps.edn
{:deps {io.github.mbjarland/clj-ant {:mvn/version "VERSION"}}}
Replace VERSION with the current Clojars version shown by the badge
above.
Requires JDK 8+. Released jars include the compiled Java bridge
class — downstream consumers do not need to run javac.
For babashka:
(require '[babashka.pods :as pods])
(pods/load-pod ["clojure" "-M:pod"])
(require '[clj-ant.pod :as a] '[clj-ant.tasks :as t])
| Doc | Read it for |
|---|---|
| doc/intro.md | Where to start. |
| doc/examples.md | Recipe cookbook (templating, bulk find-replace, smart copy, archive surgery, parallel pipelines, SSH, watch mode, …). |
| doc/architecture.md | Design rationale, layer model, anti-patterns. For contributors. |
| doc/babashka.md | The bb pod story. |
| doc/tools-build.md | Interop with clojure.tools.build. |
| doc/roadmap.md | What's done, what's planned. |
| doc/pre-release.md | Operational checklist for v1.0. |
For the underlying Ant tasks themselves — what each one does, what attributes they take, what nested elements they accept — the canonical reference is:
Every wrapper in clj-ant.tasks has a docstring with a direct link
to its corresponding Ant manual page. Browse the full list there
for tasks not yet covered in the cookbook.
Every task returns a plain map. Nothing executes until the tree
is handed to a/ant:
(t/copy :todir "out"
(t/fileset :dir "src" :includes "**/*.clj"))
;; => {:tag :copy :attrs {:todir "out"}
;; :children [{:tag :fileset :attrs {:dir "src" ...} ...}]}
So you can update, walk, assoc plans before running them.
(->> (t/fileset :dir "src" :includes "**/*.clj")
a/files ; lazy seq of java.io.File
(filter #(> (.lastModified %) cutoff))
(mapv #(.getName %)))
Anything that's an Ant ResourceCollection (fileset, filelist,
path, dirset, restrict, intersect, union, …) flows out via
a/files and a/resources. Round-trip back into a (t/copy ...)
without any string-join glue:
(let [recent (->> (a/files (t/fileset :dir "src"))
(filter #(> (.lastModified %) cutoff)))]
(a/ant (t/copy :todir "out" recent)))
Many small calls in a REPL loop? Reuse one Ant Project:
(a/with-session [s {:level :info}]
(a/ant (t/property :name "v" :value "1.2.3"))
(a/ant (t/echo :message "v=${v}")))
50-call benchmark on a 2-task plan: ~575 ms fresh → ~23 ms
sessioned → ~14 ms with (a/prepare ...).
For long-running builds, execute-async! returns a Run that
behaves like a promise/future:
(let [run (a/execute-async! [(t/scp :file "big.tar"
:todir "deploy@host:/srv/"
:keyfile "..." :trust "true")]
:on-event #(println (:phase %)))]
;; ...do other work...
(when (slow?) (a/cancel! run)) ; interrupts the build thread
@run) ; blocks for the result
IO tasks (<scp>, <get>, <sshexec>) honour the interrupt
cleanly. Pure-CPU tasks (<javac>, large <copy>) often don't
observe it, so :cancelled? true lands on the result map either
way to reflect caller intent.
The "edit, save, see rebuild" loop bb developers expect, for any clj-ant pipeline:
(def stop (a/watch [(t/javac :srcdir "src" :destdir "out")
(t/copy :todir "deploy" (t/fileset :dir "out"))]
:paths ["src"]
:poll-ms 300
:session (a/session {:level :warn})))
;; ...edit src/...
(stop)
Polling-based, so it works the same on Linux, macOS, and Windows.
Pair with :session for the cheapest re-runs.
When Ant raises a BuildException four levels into nested
elements, the error coming back is ex-info you can pattern-
match in code rather than a stringly-typed mystery:
(let [r (a/ant (t/copy :tdoir "/tmp"))] ; typo: tdoir
(when-let [err (:error r)]
(let [{:keys [clj-ant/elements ant/message]} (ex-data err)]
(println message "in" (pr-str elements)))))
;; copy doesn't support the "tdoir" attribute in [#Element{...}]
build.xml(a/ant (a/from-xml "build.xml")) ; default target
(a/ant :targets ["jar"] (a/from-xml "build.xml")) ; pick one
;; static analysis over a corpus
(for [^File f (fs/glob "." "**/build.xml")
hit (a/elements (a/from-xml f)
#(and (= :scp (:tag %))
(= "true" (-> % :attrs :trust))))]
{:file (str f) :scp-target (-> hit :attrs :file)})
;; rewrite and re-emit compact XML
(-> (a/from-xml "build.xml")
(a/transform #(cond-> %
(= :copy (:tag %))
(assoc-in [:attrs :preservelastmodified] "true")))
a/to-xml)
to-xml re-emits data-only element trees. Trees containing real Java
Ant objects, such as direct FileSets or lazy file seq children, should
be run directly because they cannot be represented faithfully as XML.
(a/deftask :slack-notify
(fn [{:keys [channel msg webhook]}]
(slack/post webhook channel msg)))
(a/ant
(t/jar :destfile "app.jar" ...)
(a/element :slack-notify :webhook url
:channel "#deploys"
:msg "shipped ${version}")) ; ${version} expanded
The Clojure fn participates fully: build-listener events, macrodef
parameter expansion, <antcall> targeting, the lot.
(a/lint (t/copy :tdoir "out"))
;; => [{:tag :copy
;; :path []
;; :errors {:tdoir ["disallowed key"]}
;; :suggestions {:tdoir [:todir]}}]
(a/ant :validate? true (t/copy :tdoir "out"))
;; ExceptionInfo: Validation failed: 1 issue(s)
(a/describe :copy)
;; => {:tag :copy
;; :description "Copies a file or resource collection ..."
;; :manual-url "https://ant.apache.org/manual/Tasks/copy.html"
;; :attrs {"todir" {:type java.io.File
;; :description "The directory to copy to."
;; :required "..."}
;; ...}
;; ...}
(a/lint (t/mkdir))
;; => [{:tag :mkdir
;; :path []
;; :errors {:dir ["missing required attribute (Required: Yes)"]}}]
user=> (doc t/copy)
clj-ant.tasks/copy
([& {:keys [todir tofile overwrite encoding ...] :as attrs} & nested])
Copies a file or resource collection ...
Attributes:
:todir File
:tofile File
:overwrite boolean
...
https://ant.apache.org/manual/Tasks/copy.html
Cursive / CIDER / clojure-lsp read :arglists for keyword
completion, so typing (t/copy : brings up the attribute names
inline.
<scp> and <sshexec> ship with the Terrapin-fixed com.github.mwiede
JSch fork (CVE-2023-48795 patched). The everyday "deploy and restart"
chain is one expression; see doc/examples.md for the
SSH recipes.
(require '[babashka.pods :as pods])
(pods/load-pod ["clojure" "-M:pod"])
(require '[clj-ant.pod :as a]
'[clj-ant.tasks :as t])
(a/with-session [s]
(a/execute-in s [(t/get :src url :dest "/tmp/v.zip")
(t/unzip :src "/tmp/v.zip" :dest "/opt/v")]))
Streaming events, files, sessions — same surface as the JVM API. See doc/babashka.md.
git clone https://github.com/mbjarland/clj-ant.git
cd clj-ant
clj -T:build javac # one-time, compiles the Java bridge
clj -M:test # run the test suite
Released jars include the pre-compiled bridge — only contributors
need the javac step.
src/clj/clj_ant/core.clj the runner + sessions + execute! + ant
src/clj/clj_ant/spec.clj malli schemas from IntrospectionHelper
src/clj/clj_ant/tasks.clj auto-generated wrappers (one per Ant
task, type, and nested element)
src/clj/clj_ant/pod.clj babashka pod
src/gen/clj_ant/gen.clj the generator (clj -X:gen)
src/java/cljant/ the single Java bridge class
test/clj_ant/core_test.clj
doc/ examples / architecture / babashka / etc.
build.clj tools.build entry points
deps.edn
clj -T:build javac # compile src/java/** -> target/classes
clj -T:build jar # build the jar
clj -T:build install # install to local maven repo
clj -T:build deploy # deploy to clojars (needs CLOJARS_*)
clj -X:gen # regenerate clj-ant.tasks from Ant
clj -M:test # run tests via kaocha
clj -M:pod # bb pod entry point
Bug reports, recipes for the cookbook, and PRs welcome. The pipeline:
test/clj_ant/core_test.clj
as the model — kaocha, with tmp-dir for filesystem fixtures.tasks.clj by hand. It's generated; run
clj -X:gen after gen.clj changes or an Ant version bump.This project stands on top of Apache Ant
(Apache 2.0). The data-first design draws inspiration from how
ProjectHelper2
already builds an UnknownElement AST during XML parse — the
"don't fight the framework" insight that makes everything else
fall out cheaply.
The babashka pod uses babashka/pods and a hand-rolled bencode codec.
SSH support comes via org.apache.ant:ant-jsch plus the
maintained com.github.mwiede:jsch
fork (the original com.jcraft:jsch is unpatched against
CVE-2023-48795).
Eclipse Public License 1.0 — see LICENSE. Apache Ant is distributed separately under Apache 2.0.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |