Liking cljdoc? Tell your friends :D

clj-ant

Apache Ant's task ecosystem, fluent from Clojure. Tasks are functions; resource collections (fileset, path, dirset, union, restrict, ...) are data until you run them, and can also be realized as lazy java.io.File / Ant Resource sequences via a/files and a/resources.

CI Clojars Project cljdoc License

Pre-1.0 alpha. Architecture is stable; surface APIs may shift slightly during the alpha period based on real-world feedback. Once 1.0.0 lands, semver applies normally.

(require '[clj-ant.core  :as a]
         '[clj-ant.tasks :as t]
         '[clojure.string :as str])

;; Build a distributable site from normal files plus selected entries
;; inside zip/jar files. No shelling out, no temporary unzip step.
(let [version "1.2.3"
      out     "target/site"
      docs    (->> (a/files (t/fileset :dir "doc" :includes "**/*.md"))
                   (remove #(str/includes? (.getName %) "draft")))]
  (a/ant
    (t/delete :dir out)
    (t/mkdir :dir out)

    ;; Clojure decides which files matter; Ant does the copying.
    (t/copy :todir (str out "/docs") docs)

    ;; Copy only built browser assets from a vendor zip.
    (t/copy :todir (str out "/assets")
      (t/zipfileset :src "vendor/admin-ui.zip"
                    :includes "dist/**"
                    :prefix "admin"))

    ;; Pull legal metadata out of the app jar the same way.
    (t/copy :todir (str out "/licenses")
      (t/zipfileset :src (str "target/app-" version ".jar")
                    :includes "META-INF/LICENSE*,META-INF/NOTICE*"))

    ;; Stream token replacement while copying config templates.
    (t/copy :todir (str out "/conf")
      (t/fileset :dir "etc" :includes "**/*.tmpl")
      (t/filterchain
        (t/tokenfilter
          (t/replacestring :from "@VERSION@" :to version))))

    (t/zip :destfile (str "target/site-" version ".zip")
      (t/fileset :dir out))))

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, and zip creation. No XML, no shelling out to zip / unzip, no temporary extraction just to grab files from a jar.

Why

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 wraps Ant's built-in tasks, data types, and nested elements 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.

Install

;; deps.edn
{:deps {io.github.mbjarland/clj-ant {:mvn/version "1.0.0-alpha.5"}}}

Replace the version with the latest one shown by the Clojars badge above when upgrading.

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])

Documentation

DocRead it for
doc/intro.mdWhere to start.
doc/examples.mdRecipe cookbook (templating, bulk find-replace, smart copy, archive surgery, parallel pipelines, SSH, watch mode, …).
doc/architecture.mdDesign rationale, layer model, anti-patterns. For contributors.
doc/babashka.mdThe bb pod story.
doc/tools-build.mdInterop with clojure.tools.build.
doc/roadmap.mdWhat's done, what's planned.
doc/pre-release.mdRelease checklist for alpha and 1.0.

For the underlying Ant tasks themselves — what each one does, what attributes they take, what nested elements they accept — the canonical reference is:

Apache Ant Tasks Overview

Generated wrappers include manual-derived docstrings, and wrappers with known Ant manual pages include direct links. Browse Ant's overview for tasks not yet covered in the cookbook.

Highlights

Data-first

Every generated wrapper returns an Element record, which behaves like a Clojure 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.

Resource collections as Clojure sequences

(->> (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)))

Sessions for tight loops

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 ...).

Async + cancellation

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.

Watch mode

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.

Errors carry the element tree

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{...}]

Read existing build.xml

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

(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.

Clojure functions as first-class Ant tasks

(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, and target execution. Put it inside a target and <antcall> can invoke that target like any other Ant target.

Validation (malli) and rich REPL

(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
([& args])
  Copies a file or resource collection ...
  **Attributes**
  | Attribute | Type | Description | Required |
  | `:todir` | `File` | The directory to copy to. | ... |
  ...
  https://ant.apache.org/manual/Tasks/copy.html

Generated t/* vars keep compact ([& args]) signatures so cljdoc doesn't bury the useful prose under huge parameter lists. Attribute names, Java types, manual prose, required markers, nested elements, and backing classes are still available in generated docstrings, var metadata, and a/describe.

clojure.repl/doc is still the right quick human-facing REPL view for t/* vars. a/describe complements it by returning EDN for tools, UIs, bb pod callers, and validation.

SSH out of the box

<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.

Babashka pod

(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.

Getting started for contributors

git clone https://github.com/mbjarland/clj-ant.git
cd clj-ant
clj -T:build javac     # one-time, compiles the Java bridge
clj -T:build lint      # static linting, warnings fail
clj -M:test            # run the test suite

Released jars include the pre-compiled bridge — only contributors need the javac step.

Project layout

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

Aliases

clj -T:build javac          # compile src/java/** -> target/classes
clj -T:build lint           # run clj-kondo, warnings fail
clj -T:build check          # lint, test, and build the jar
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

Contributing

Bug reports, recipes for the cookbook, and PRs welcome. The pipeline:

  1. Open an issue first for anything beyond a typo fix — saves wasted effort if the change doesn't fit the design.
  2. Read doc/architecture.md. The "anti-patterns" section in particular flags directions that look reasonable but break things.
  3. Tests required for behaviour changes. Use test/clj_ant/core_test.clj as the model — kaocha, with tmp-dir for filesystem fixtures.
  4. Don't edit tasks.clj by hand. It's generated; run clj -X:gen after gen.clj changes or an Ant version bump.
  5. Commit format: summary line, blank line, body wrapped at 80 columns. No AI/LLM attribution trailers.

Acknowledgments

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).

License

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close