Find file Copy path
1124 lines (978 sloc) 53.8 KB
(ns boot.task.built-in
[clojure.pprint :as pp]
[ :as io]
[clojure.set :as set]
[clojure.string :as string]
[boot.filesystem :as fs]
[boot.gpg :as gpg]
[boot.pod :as pod]
[boot.jar :as jar]
[boot.git :as git]
[boot.file :as file]
[boot.repl :as repl]
[boot.core :as core]
[boot.main :as main]
[boot.util :as util]
[boot.tmpdir :as tmpd]
[boot.from.table.core :as table]
[boot.from.digest :as digest]
[boot.task-helpers :as helpers]
[boot.task-helpers.notify :as notify]
[boot.pedantic :as pedantic])
[ File]
[java.nio.file.attribute PosixFilePermissions]
[java.util Arrays]
[java.util.concurrent LinkedBlockingQueue TimeUnit]
[ ToolProvider DiagnosticCollector Diagnostic$Kind]))
;; Tasks ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(core/deftask help
"Print usage info and list available tasks."
(core/with-pass-thru [_]
(let [tasks (#'helpers/available-tasks 'boot.user)
opts (->> main/cli-opts (mapv (fn [[x y z]] ["" (str x " " y) z])))
envs [["" "BOOT_AS_ROOT" "Set to 'yes' to allow boot to run as root."]
["" "BOOT_CERTIFICATES" "Specify certificate file paths."]
["" "BOOT_CLOJARS_REPO" "Specify the url for the 'clojars' Maven repo."]
["" "BOOT_CLOJARS_MIRROR" "Specify the mirror url for the 'clojars' Maven repo."]
["" "BOOT_CLOJURE_VERSION" "The version of Clojure boot will provide (1.8.0)."]
["" "BOOT_CLOJURE_NAME" "The artifact name of Clojure boot will provide (org.clojure/clojure)."]
["" "BOOT_COLOR" "Set to 'no' to turn colorized output off."]
["" "BOOT_FILE" "Build script name (build.boot)."]
["" "BOOT_GPG_COMMAND" "System gpg command (gpg)."]
["" "BOOT_HOME" "Directory where boot stores global state (~/.boot)."]
["" "BOOT_WATCHERS_DISABLE" "Set to 'yes' to turn off inotify/FSEvents watches."]
["" "BOOT_JAVA_COMMAND" "Specify the Java executable (java)."]
["" "BOOT_JVM_OPTIONS" "Specify JVM options (Unix/Linux/OSX only)."]
["" "BOOT_LOCAL_REPO" "The local Maven repo path (~/.m2/repository)."]
["" "BOOT_MAVEN_CENTRAL_REPO" "Specify the url for the 'maven-central' Maven repo."]
["" "BOOT_MAVEN_CENTRAL_MIRROR" "Specify the mirror url for the 'maven-central' Maven repo."]
["" "BOOT_VERSION" "Specify the version of boot core to use."]
["" "BOOT_WARN_DEPRECATED" "Set to 'no' to suppress deprecation warnings."]]
files [["" "./" "Specify boot options for this project."]
["" "./profile.boot" "A script to run after the global profile.boot but before the build script."]
["" "BOOT_HOME/" "Specify global boot options."]
["" "BOOT_HOME/profile.boot" "A script to run before running the build script."]]
br #(conj % ["" "" ""])]
(printf "\n%s\n"
(-> [["" ""] ["Usage:" "boot OPTS <task> TASK_OPTS <task> TASK_OPTS ..."]]
(table/table :style :none)
(printf "%s\n\nDo `boot <task> -h` to see usage info and TASK_OPTS for <task>.\n"
(->> (-> [["" "" ""]]
(into (#'helpers/set-title opts "OPTS:")) (br)
(into (#'helpers/set-title (#'helpers/tasks-table tasks) "Tasks:")) (br)
(into (#'helpers/set-title envs "Env:")) (br)
(into (#'helpers/set-title files "Files:"))
(table/table :style :none)
(string/split #"\n"))
(map string/trimr)
(string/join "\n"))))))
(core/deftask ^{:deprecated "2.6.0"} checkout
"Checkout dependencies task. DEPRECATED. (Use -c, --checkouts Boot option.)
This task facilitates working on a project and its dependencies at the same
time, by extracting the dependency jar contents into the fileset. Transitive
dependencies will be added to the class path automatically.
You'll need at least two boot instances---one to build the dependency jar and
the other to build the project. For example:
$ boot watch pom -p foo/bar -v 1.2.3-SNAPSHOT jar install
to build the dependency jar, and
$ boot repl -s watch checkout -d foo/bar:1.2.3-SNAPSHOT cljs serve
to build the project with the checkout dependency [foo/bar \"1.2.3\"]."
[d dependencies ID:VER [[sym str]] "The vector of checkout dependencies."]
(let [env (core/get-env)
prev (atom nil)
jars (->> dependencies
(map (comp io/file #(pod/resolve-dependency-jar env %))))
deps (->> dependencies
(mapcat #(pod/resolve-nontransitive-dependencies env %))
(map :dep)
(remove pod/dependency-loaded?))
names (map (memfn getName) jars)
dirs (map (memfn getParent) jars)
tmps (reduce #(assoc %1 %2 (core/tmp-dir!)) {} names)
warn (delay
"The checkout task is deprecated. Please use the --checkouts boot option instead.\n"))
adder #(core/add-source %1 %2 :exclude pod/standard-jar-exclusions)]
(when (seq deps)
(util/info "Adding checkout dependencies:\n")
(doseq [dep deps]
(util/info "\u2022 %s\n" (pr-str dep))))
(core/merge-env! :dependencies (vec deps)
:source-paths (set dirs))
(core/cleanup (core/set-env! :source-paths #(apply disj % dirs)))
(core/with-pre-wrap [fs]
(let [diff (->> (core/fileset-diff @prev fs)
(filter (comp (set names) core/tmp-path))
(map (juxt core/tmp-path core/tmp-file)))]
(reset! prev fs)
(doseq [[path file] diff]
(let [tmp (tmps path)]
(core/empty-dir! tmp)
(util/info "Checking out %s...\n" path)
(pod/unpack-jar (.getPath file) tmp)))
(->> tmps vals (reduce adder fs) core/commit!)))))
(core/deftask ^{:deprecated "2.7.0"} speak
"Audible notifications during build. DEPRECATED. Use the notify task.
Default themes: system (the default), ordinance, pillsbury, and
woodblock. New themes can be included via jar dependency with the
sound files as resources:
└── notify
├── <theme-name>_failure.mp3
├── <theme-name>_success.mp3
└── <theme-name>_warning.mp3
Sound files specified individually take precedence over theme sounds."
[t theme NAME str "The notification sound theme."
s success FILE str "The sound file to play when the build is successful."
w warning FILE str "The sound file to play when there are warnings reported."
f failure FILE str "The sound file to play when the build fails."]
(let [tmp (core/tmp-dir!)
resource #(vector %2 (format "boot/notify/%s_%s.mp3" %1 %2))
resources #(map resource (repeat %) ["success" "warning" "failure"])
themefiles (into {}
(let [rs (when theme (resources theme))]
(when (and (seq rs) (every? (comp io/resource second) rs))
(for [[x r] rs]
(let [f (io/file tmp (.getName (io/file r)))]
(pod/copy-resource r f)
[(keyword x) (.getPath f)])))))
success (or success (:success themefiles))
warning (or warning (:warning themefiles))
failure (or failure (:failure themefiles))]
(fn [next-task]
(fn [fileset]
(util/with-let [_ (next-task fileset)]
(if (zero? @core/*warnings*)
(pod/with-call-worker (boot.notify/success! ~theme ~success))
(pod/with-call-worker (boot.notify/warning! ~theme ~(deref core/*warnings*) ~warning))))
(catch Throwable t
(pod/with-call-worker (boot.notify/failure! ~theme ~failure))
(throw t)))))))
(core/deftask ^{:boot/from :jeluard/boot-notify} notify
"Audible and visual notifications during build.
Default audio themes: system (the default), ordinance, pillsbury and
woodblock. New themes can be included via jar dependency with the
sound files as resources:
└── notify
├── <theme-name>_failure.mp3
├── <theme-name>_success.mp3
└── <theme-name>_warning.mp3
Sound files specified individually take precedence over theme sounds.
For visual notifications, there is a default implementation that
tries to use the `terminal-notifier' or `osascript` programs on OS X
systems, and the `notify-send' program on Linux systems.
You can also supply custom notification functions via the *-notify-fn
options. Both are functions that take one argument which is a map of
The audible notification function will receive a map with three keys
- :type, :file, and :theme.
The visual notification function will receive a map with four keys
- :title, :uid, :icon, and :message.
The `notify` task always attempts to catch any exceptions that are
thrown so as not to cause your build to fail. If no notifications
are happening even if you specify `audible` or `visual`, you can see
any exceptions that are being caught by changing
`boot.util/*verbosity*` (at the command-line use: `boot -v` or `boot
[a audible bool "Play an audible notification"
v visual bool "Display a visual notification"
A audible-notify-fn FN sym "A function to be used for audible notifications in place of the default method."
V visual-notify-fn FN sym "A function to be used for visual notifications in place of the default method"
T theme NAME str "The name of the audible notification sound theme"
s soundfiles KEY=VAL {kw str} "Sound files overriding theme sounds. Keys can be :success, :warning or :failure"
m messages KEY=VAL {kw str} "Templates overriding default messages. Keys can be :success, :warning or :failure"
t title TITLE str "Title of the notification"
i icon ICON str "Full path of the file used as notification icon"
u uid UID str "Unique ID identifying this boot process"]
(let [themefiles (notify/get-themefiles theme (core/tmp-dir!))
sounds (merge themefiles soundfiles)
title (or title "Boot")
base-visual-opts {:title title
:uid (or uid title)
:icon (or icon (notify/boot-logo))}
messages (merge {:success "Success!" :warning "%s warning/s" :failure "%s"}
audible-notify! (or audible-notify-fn notify/audible-notify!)
visual-notify! (or visual-notify-fn notify/visual-notify!)
notify! (fn [type message]
(when audible
(audible-notify! {:type type
:file (get sounds type)
:theme theme}))
(when visual
(visual-notify! (assoc base-visual-opts
:message message))))]
(fn [next-task]
(fn [fileset]
(util/with-let [_ (next-task fileset)]
(if (zero? @core/*warnings*)
(notify! :success (:success messages))
(notify! :warning (format (:warning messages)
(deref core/*warnings*)))))
(catch Throwable t
(notify! :failure (format (:failure messages)
(.getMessage t)))
(throw t)))))))
(core/deftask show
"Print project/build info (e.g. dependency graph, etc)."
[C fake-classpath bool "Print the project's fake classpath."
c classpath bool "Print the project's full classpath."
d deps bool "Print project dependency graph."
e env bool "Print the boot env map."
f fileset bool "Print the build fileset object."
l list-pods bool "Print the names of all active pods."
p pedantic bool "Print graph of dependency conflicts."
P pods REGEX regex "The name filter used to select which pods to inspect."
U update-snapshots bool "Include snapshot versions in updates searches."
u updates bool "Print newer releases of outdated dependencies."
v verify-deps bool "Include signature status of each dependency in graph."]
(let [usage (delay (*usage*))
pretty-str #(with-out-str (pp/pprint %))
updates (or updates update-snapshots)
sort-pods #(sort-by (memfn getName) %)]
(core/with-pass-thru [fs]
(cond fileset (helpers/print-fileset fs)
classpath (println (or (core/get-env :boot-class-path) ""))
fake-classpath (println (or (core/get-env :fake-class-path) ""))
list-pods (doseq [p (->> pod/pods (map key) sort-pods)]
(println (.getName p)))
(let [pattern (or pods #"^core$")]
(doseq [p (->> pattern pod/get-pods sort-pods)
:let [pod-name (.getName p)]
:when (not (#{"worker" "aether"} pod-name))]
(let [pod-env (if (= pod-name "core")
(pod/with-eval-in p boot.pod/env))]
(when pods (util/info "\nPod: %s\n\n" pod-name))
(cond (or deps verify-deps) (print (pod/with-call-worker (boot.aether/dep-tree ~pod-env ~verify-deps)))
env (println (pretty-str (assoc pod-env :config (boot.App/config))))
updates (mapv prn (pod/outdated pod-env :snapshots update-snapshots))
pedantic (pedantic/prn-conflicts pod-env)
:else @usage))))))))
(defn- relativize
[local-repo path]
(if-not local-repo
(io/file path)
(let [canonical-local-repo (.getCanonicalFile (io/file local-repo))]
(io/file local-repo (file/relative-to canonical-local-repo path))))))
(defn- dep-conflicts
[{:keys [dependencies] :as env}]
(let [non-transitive? (set (map first dependencies))]
(into {} (->> (pedantic/dep-conflicts env)
(remove (comp non-transitive? first))))))
(core/deftask with-cp
"Specify Boot's classpath in a file instead of as Maven coordinates.
The --file option is required -- this specifies the PATH of the file that
will contain the JAR paths as a string suitable for passing to java -cp. Use
the minus sign, like --file -, to indicate stdin, stdout.
The default behavior if the --write flag is not specified is to read the
file specified with --file and load all the JARs onto the classpath. If the
--write flag is given the --dependencies (or the default depenedncies from
the boot env, e.g. (get-env :dependencies), if --dependencies isn't provided)
are resolved and the resulting list of JAR paths are written to the file in
a format suitable for passing to java -cp.
The --safe flag configures the task to throw an exception when writing the
classpath file if there are any unresolved dependency conflicts. These
conflicts can be resolved by adding :exclusions and by overriding transitive
dependencies with direct dependencies.
The --local-repo option specifies the PATH where the dependency JARs are
stashed. The default if this option is not given is to use the Maven local
repository setting from the boot environment. This option only applies in
combination with the --write option.
The --scopes option can be used to specify which dependency scopes to include
in the classpath file. The default scopes are compile, runtime, and provided."
[s safe bool "Throw an exception if there are unresolved dependency conflicts."
w write bool "Resolve dependencies and write the classpath file."
d dependencies EDN edn "The (optional) Maven dependencies to resolve and write to the classpath file."
e exclusions SYM [sym] "The vector of Maven group/artifact ids to globally exclude."
f file PATH str "The file containing JARs in java -cp format."
l local-repo PATH str "The (optional) project directory in which to stash resolved JARs."
S scopes SCOPE #{str} "The set of dependency scopes to include (default compile, runtime, provided)."]
(core/with-pass-thru [_]
(let [scopes (or scopes #{"compile" "runtime" "provided"})
file-in (if (= file "-") (System/in) file)
file-out (if (= file "-") (System/out) file)
include? #(scopes (:scope (util/dep-as-map %)))
env-opts (select-keys *opts* [:local-repo :exclusions :dependencies])
exclude (partial pod/apply-global-exclusions exclusions)]
(assert file "Expected --file option.")
(if-not write
(doseq [path (string/split (slurp file-in) #":")]
(pod/add-classpath path))
(let [env (-> (merge-with #(or %2 %1) pod/env env-opts)
(update-in [:dependencies] #(exclude (filter include? %))))]
(if-let [conflicts (and safe (not-empty (dep-conflicts env)))]
(throw (ex-info "Unresolved dependency conflicts." {:conflicts conflicts}))
(let [resolved (pod/resolve-dependency-jars env)
relative-paths (map (partial relativize local-repo) resolved)
source-paths (:source-paths env)]
(spit file-out (apply str (->> (concat source-paths relative-paths)
(interpose ":")
(into [])))))))))))
(core/deftask wait
"Wait before calling the next handler.
Waits forever if the --time option is not specified."
[t time MSEC int "The interval in milliseconds."]
(if (zero? (or time 0))
(core/with-post-wrap [_] @(promise))
(core/with-pass-thru [fs] (Thread/sleep time))))
(defn- mk-posix-file-permissions [posix-string]
(when posix-string
(if (boot.App/isWindows)
(util/warn "Filemode not supported on Windows\n")
(PosixFilePermissions/fromString posix-string)))
(catch IllegalArgumentException _
(util/warn "Could not parse mode string, ignoring...\n"))))
(core/deftask target
"Writes output files to the given directory on the filesystem."
[d dir PATH #{str} "The set of directories to write to (target)."
m mode VAL str "The mode of written files in 'rwxrwxrwx' format"
L no-link bool "Don't create hard links."
C no-clean bool "Don't clean target before writing project files."]
(let [dir (or (seq dir) ["target"])
sync! (#'core/fileset-syncer dir :clean (not no-clean))]
(core/with-pass-thru [fs]
(util/info "Writing target dir(s)...\n")
(sync! fs :link (not no-link) :mode (mk-posix-file-permissions mode)))))
(core/deftask watch
"Call the next handler when source files change."
[q quiet bool "Suppress all output from running jobs."
v verbose bool "Print which files have changed."
M manual bool "Use a manual trigger instead of a file watcher."
d debounce MS int "Debounce time (how long to wait for filesystem events) in milliseconds."
i include REGEX #{regex} "The set of regexes the paths of changed files must match for watch to fire."
e exclude REGEX #{regex} "The set of regexes the paths of changed files must not match for watch to fire."]
(fn [next-task]
(fn [fileset]
(let [q (LinkedBlockingQueue.)
k (gensym)
return (atom fileset)
srcdirs (->> (map (comp :dir val) (core/get-checkouts))
(into (core/user-dirs fileset))
(map (memfn getPath)))
watcher (apply file/watcher! :time srcdirs)
incl-excl (if-not (or (seq include) (seq exclude))
(let [f (partial file/keep-filters? include exclude)]
(partial filter (comp f io/file second))))
watch-target (if manual core/new-build-at core/last-file-change)]
(.offer q (System/currentTimeMillis))
(add-watch watch-target k #(.offer q %4))
(core/cleanup (remove-watch watch-target k))
(when-not quiet (util/info "\nStarting file watcher (CTRL-C to quit)...\n\n"))
(loop [ret (util/guard [(.take q)])]
(when ret
(if-let [more (.poll q (or debounce 10) TimeUnit/MILLISECONDS)]
(recur (conj ret more))
(let [start (System/currentTimeMillis)
etime #(- (System/currentTimeMillis) start)
changed (when-not manual (incl-excl (watcher)))
should-fire? (or manual (not (empty? changed)))]
(when should-fire?
(when verbose
(doseq [[op p _] changed]
(util/info (format "\u25C9 %s %s\n" op p)))
(util/info "\n"))
(binding [*out* (if quiet (new *out*)
*err* (if quiet (new *err*)]
(try (reset! return (-> fileset core/reset-fileset core/commit! next-task))
(catch Throwable ex (util/print-ex ex)))
(util/info "Elapsed time: %.3f sec\n\n" (float (/ (etime) 1000)))))
(recur (util/guard [(.take q)]))))))
(core/deftask repl
"Start a REPL session for the current project.
If no bind/host is specified the REPL server will listen on and
the client will connect to
If no port is specified the server will choose a random one and the client
will read the .nrepl-port file and use that.
The *default-middleware* and *default-dependencies* atoms in the boot.repl
namespace contain vectors of default REPL middleware and REPL dependencies to
be loaded when starting the server. You may modify these in your build.boot
[s server bool "Start REPL server only."
c client bool "Start REPL client only."
C no-color bool "Disable colored REPL client output."
e eval EXPR edn "The form the client will evaluate in the boot.user ns."
b bind ADDR str "The address server listens on."
H host HOST str "The host client connects to."
i init PATH str "The file to evaluate in the boot.user ns."
I skip-init bool "Skip default client initialization code."
p port PORT int "The port to listen on and/or connect to."
P pod NAME str "The name of the pod to start nREPL server in (core)."
n init-ns NS sym "The initial REPL namespace."
m middleware SYM [sym] "The REPL middleware vector."
x handler SYM sym "The REPL handler (overrides middleware options)."]
(when (string? eval)
(util/warn "When passing :eval to the repl task in your build.boot, use a quoted form instead of a string\n"))
(let [cpl-path (.getPath (core/tmp-dir!))
srv-opts (->> [:bind :port :init-ns :middleware :handler :pod]
(select-keys *opts*))
cli-opts (-> *opts*
(select-keys [:host :port :history])
(assoc :standalone true
:custom-eval eval
:custom-init init
:color (and @util/*colorize?* (not no-color))
:skip-default-init skip-init))
deps (remove pod/dependency-loaded? @repl/*default-dependencies*)
repl-svr (delay (apply core/launch-nrepl (mapcat identity srv-opts)))
repl-cli (delay (pod/with-call-worker (boot.repl-client/client ~cli-opts)))]
(comp (core/with-pass-thru [fs]
(when (or server (not client)) @repl-svr))
(core/with-post-wrap [_]
(when (or client (not server)) @repl-cli)))))
(core/deftask bare-repl
"Start a bare REPL session for the current project.
Compared to the repl task, the bare-repl task starts up more quickly but
lacks features such as nREPL connectivity and colored stacktraces.
Use the rlwrap Unix tool to add readline functionality:
# rlwrap boot bare-repl"
[e eval EXPR edn "The form the client will evaluate in the boot.user ns."
i init PATH str "The file to evaluate in the boot.user ns."
n init-ns NS sym "The initial REPL namespace."]
(core/with-pass-thru [_]
(repl/launch-bare-repl *opts*)))
(core/deftask socket-server
"Start a socket server.
The default behavior is to serve a simple REPL handled by
clojure.core.server/repl. To serve a different handler function, specify a
symbol using `--accept'.
If no bind address is specified, the socket server will listen on
If no port is specified, an open port will be chosen automatically. The port
number is written to .socket-port in the current directory.
The REPL can be accessed with the command
$ nc localhost $(cat .socket-port)"
[b bind ADDR str "The address server listens on."
p port PORT int "The port to listen to."
a accept ACCEPT sym "Namespaced symbol of the accept function to invoke."]
(let [repl-soc (delay (repl/launch-socket-server *opts*))]
(core/with-pass-thru [fs]
(core/deftask pom
"Create project pom.xml file.
The project and version must be specified to make a pom.xml.
Note that if you want to install some other artifact along with the main one,
for instance the classic sources or javadoc artifact, you have to add the
classifier to your pom.xml, which translates to adding :classifier to this
[p project SYM sym "The project id (eg. foo/bar)."
v version VER str "The project version."
d description DESC str "The project description."
c classifier STR str "The project classifier."
P packaging STR str "The project packaging type, i.e. war, pom"
u url URL str "The project homepage url."
s scm KEY=VAL {kw str} "The project scm map (KEY is one of url, tag, connection, developerConnection)."
l license NAME:URL {str str} "The map {name url} of project licenses."
o developers NAME:EMAIL {str str} "The map {name email} of project developers."
D dependencies SYM:VER [[sym str]] "The project dependencies vector (overrides boot env dependencies)."
a parent SYM:VER=PATH [sym str str] "The project dependency vector of the parent project, path included."]
(let [tgt (core/tmp-dir!)
tag (or (:tag scm) (util/guard (git/last-commit)))
scm (when scm (assoc scm :tag tag))
deps (or dependencies (:dependencies (core/get-env)))
opts (assoc *opts*
:scm scm
:dependencies deps
:developers developers
:classifier classifier
:packaging (or packaging "jar")
:parent parent)]
(when-not (and project version)
(throw (Exception. "need project and version to create pom.xml")))
(let [[project version] (pod/canonical-coord [project version])
[gid aid] (util/extract-ids project)
pomdir (io/file tgt "META-INF" "maven" gid aid)
xmlfile (io/file pomdir "pom.xml")
propfile (io/file pomdir "")]
(boot.pom/spit-pom! ~(.getPath xmlfile) ~(.getPath propfile) ~opts))
(core/with-pre-wrap [fs]
(util/info "Writing %s and %s...\n" (.getName xmlfile) (.getName propfile))
(-> fs (core/add-resource tgt :meta {:project project}) core/commit!)))))
(core/deftask sift
"Transform the fileset, matching paths against regexes.
The --to-asset, --to-resource, and --to-source options move matching paths
to the corresponding section of the fileset. This can be used to make source
files into resource files, for example, etc. If --invert is also specified
the transformation is done to paths that DO NOT match.
The --add-asset, --add-resource, and --add-source options add the contents
of a directory to the fileset as assets, resources, or sources, respectively.
The --invert option has no effect on these options.
The --add-jar option extracts the contents of a jar file on the classpath
and adds them to the fileset. The PROJECT part of the argument specifies the
group-id/artifact-id symbol associated with the jar, and the MATCH portion
selects which entries in the jar will be extracted. If --invert is also
specified then entries whose paths DO NOT match the regex will be extracted.
The --with-meta option specifies a set of metadata keys files in the fileset
must have. Files without one of these keys will be filtered out. If --invert
is also specified then files that DO have one of these keys will be filtered
out, instead.
The --add-meta option adds a key to the metadata map associated with paths
matching the regex portion of the argument. For example:
boot sift --add-meta 'foo$':bar
merges {:bar true} into the metadata map associated with all paths that end
with 'foo'. If --invert is also specified the metadata is added to paths
that DO NOT match the regex portion.
The --move option applies a find/replace transformation on all paths in the
output fileset. The --invert option has no effect on this operation.
The --include option specifies a set of regexes that will be used to filter
the fileset. Only paths matching one of these will be kept. If --invert is
also specified then only paths NOT matching one of the regexes will be kept."
[a to-asset MATCH #{regex} "The set of regexes of paths to move to assets."
r to-resource MATCH #{regex} "The set of regexes of paths to move to resources."
s to-source MATCH #{regex} "The set of regexes of paths to move to sources."
A add-asset PATH #{str} "The set of directory paths to add to assets."
R add-resource PATH #{str} "The set of directory paths to add to resources."
S add-source PATH #{str} "The set of directory paths to add to sources."
j add-jar PROJECT:MATCH {sym regex} "The map of jar to path regex of entries in jar to unpack."
w with-meta KEY #{kw} "The set of metadata keys files must have."
M add-meta MATCH:KEY {regex kw} "The map of path regex to meta key to add."
m move MATCH:REPLACE {regex str} "The map of regex to replacement path strings."
i include MATCH #{regex} "The set of regexes that paths must match."
v invert bool "Invert the sense of matching."]
(let [v? (:invert *opts*)
*opts* (dissoc *opts* :invert)
action (partial helpers/sift-action v?)
process (reduce-kv #(comp (action %2 %3) %1) identity *opts*)]
(core/with-pre-wrap [fs]
(util/info "Sifting output files...\n")
(util/dbug* "%s\n" (util/pp-str (assoc *opts* :invert v?)))
(-> fs process core/commit!))))
(core/deftask add-repo
"Add all files in project git repo to fileset.
The ref option (default HEAD) facilitates pulling files from tags or specific
[u untracked bool "Add untracked (but not ignored) files."
r ref REF str "The git reference for the desired file tree."]
(let [tgt (core/tmp-dir!)]
(core/with-pre-wrap [fs]
(core/empty-dir! tgt)
(util/info "Adding repo files...\n")
(doseq [p (core/git-files :ref ref :untracked untracked)]
(file/copy-with-lastmod (io/file p) (io/file tgt p)))
(-> fs (core/add-resource tgt) core/commit!))))
(core/deftask uber
"Add jar entries from dependencies to fileset.
Use this task before the packaging task (jar, war, etc.) to create
uberjars, uberwars, etc. This provides the means to package the project
with all of its dependencies included.
By default, entries from dependencies with the following scopes will be
copied to the fileset: compile, runtime, and provided. The --include-scope
and --exclude-scope options may be used to add or remove scope(s) from this
The --as-jars option pulls in dependency jars without exploding them such
that the jarfiles themselves are copied into the fileset. When using the
--as-jars option you need a special classloader like a servlet container
(e.g. Tomcat, Jetty) that will add the jars to the application classloader.
When jars are exploded, the --include and --exclude options control which
paths are added to the uberjar; a path is only added if it matches an
--include regex and does not match any --exclude regexes.
The --exclude option default is:
#{ #\"(?i)^META-INF/INDEX.LIST$\"
#\"(?i)^META-INF/[^/]*\\.(MF|SF|RSA|DSA)$\" }
And --include option default is:
#{ #\".*\" }
If exploding the jars results in duplicate entries, they will be merged
using the rules specified by the --merge option. A merge rule is a
[regex fn] pair, where fn takes three parameters:
- an InputStream for the previous entry,
- an InputStream of the new entry,
- and an OutputStream that will replace the entry.
The --merge option default is:
[[ #\"data_readers.clj$\" into-merger ]
[ #\"META-INF/services/.*\" concat-merger ]
[ #\".*\" first-wins-merger ]]
The merge rule regular expressions are tested in order, and the fn from
the first match is applied.
Setting the --include, --exclude, or --merge options replaces the default."
[j as-jars bool "Copy entire jar files instead of exploding them."
s include-scope SCOPE #{str} "The set of scopes to add."
S exclude-scope SCOPE #{str} "The set of scopes to remove."
i include MATCH #{regex} "The set of regexes that paths must match."
e exclude MATCH #{regex} "The set of regexes that paths must not match."
m merge REGEX=FN [[regex code]] "The list of duplicate file mergers."]
(let [tgt (core/tmp-dir!)
cache (core/cache-dir! ::uber :global true)
dfl-scopes #{"compile" "runtime" "provided"}
scopes (-> dfl-scopes
(set/union include-scope)
(set/difference exclude-scope))
scope? #(contains? scopes (:scope (util/dep-as-map %)))
jars (-> (core/get-env)
(update-in [:dependencies] (partial filter scope?))
jars (remove #(.endsWith (.getName %) ".pom") jars)
checkouts (->> (core/get-checkouts)
(filter (comp scope? :dep val))
(into {}))
co-jars (->> checkouts (map (comp :jar val)))
co-dirs (->> checkouts (map (comp :dir val)))
exclude (or exclude pod/standard-jar-exclusions)
merge (or merge pod/standard-jar-mergers)
reducer (fn [xs jar]
xs (digest/md5 jar) (partial pod/unpack-jar jar)
:include include :exclude exclude :mergers merge))
co-reducer #(core/add-resource
%1 %2 :include include :exclude exclude :mergers merge)]
(core/with-pre-wrap [fs]
(when (seq jars)
(util/info "Adding uberjar entries...\n"))
(when as-jars
(doseq [jar (reduce into [] [jars co-jars])]
(let [hash (digest/md5 jar)
name (str hash "-" (.getName jar))
src (io/file cache hash)]
(when-not (.exists src)
(util/dbug* "Caching jar %s...\n" name)
(file/copy-atomically jar src))
(util/dbug* "Adding cached jar %s...\n" name)
(file/hard-link src (io/file tgt name)))))
(core/commit! (if as-jars
(core/add-resource fs tgt)
(reduce co-reducer (reduce reducer fs jars) co-dirs))))))
(core/deftask web
"Create project web.xml file.
The --serve option is required. The others are optional."
[s serve SYM sym "The 'serve' callback function."
c create SYM sym "The 'create' callback function."
d destroy SYM sym "The 'destroy' callback function."
C context-create SYM sym "The context 'create' callback function, called when the servlet is first loaded by the container."
D context-destroy SYM sym "The context 'destroyed' callback function, called when the servlet is unloaded by the container."]
(let [tgt (core/tmp-dir!)
xmlfile (io/file tgt "WEB-INF" "web.xml")
implp 'tailrecursion/clojure-adapter-servlet
implv "0.2.1"
classes #"^tailrecursion/.*\.(class|clj)$"
webxml (delay
(util/info "Adding servlet impl...\n")
(core/get-env) tgt [implp implv] classes)
(util/info "Writing %s...\n" (.getName xmlfile))
(boot.web/spit-web! ~(.getPath xmlfile)
(core/with-pre-wrap [fs]
(assert (and (symbol? serve) (namespace serve))
(format "serve function must be namespaced symbol (%s)" serve))
(-> fs (core/add-resource tgt) core/commit!))))
(core/deftask aot
"Perform AOT compilation of Clojure namespaces."
[a all bool "Compile all namespaces."
n namespace NS #{sym} "The set of namespaces to compile."]
(when (empty? *opts*)
(util/warn "No flags specified for aot task, skipping...\n"))
(let [tgt (core/tmp-dir!)
pod-env (update-in (core/get-env) [:directories] conj (.getPath tgt))
compile-pod (future (pod/make-pod pod-env))]
(core/with-pre-wrap [fs]
(core/empty-dir! tgt)
(let [all-nses (->> fs core/fileset-namespaces)
nses (->> all-nses (set/intersection (if all all-nses namespace)) sort)]
(pod/with-eval-in @compile-pod
(binding [*compile-path* ~(.getPath tgt)]
(doseq [[idx ns] (map-indexed vector '~nses)]
(boot.util/info "Compiling %s/%s %s...\n" (inc idx) (count '~nses) ns)
(compile ns)))))
(-> fs (core/add-resource tgt) core/commit!))))
(core/deftask javac
"Compile java sources."
[o options OPTIONS [str] "List of options passed to the java compiler."]
(let [tgt (core/tmp-dir!)]
(core/with-pre-wrap [fs]
(let [throw? (atom nil)
diag-coll (DiagnosticCollector.)
compiler (or (ToolProvider/getSystemJavaCompiler)
(throw (Exception. "The java compiler is not working. Please make sure you use a JDK!")))
file-mgr (.getStandardFileManager compiler diag-coll nil nil)
opts (->> ["-d" (.getPath tgt)
"-cp" (core/get-env :boot-class-path)]
(concat options)
(into-array String) Arrays/asList)
handler {Diagnostic$Kind/ERROR util/fail
Diagnostic$Kind/WARNING util/warn
Diagnostic$Kind/MANDATORY_WARNING util/warn}
srcs (some->> (core/input-files fs)
(core/by-ext [".java"])
(map core/tmp-file)
(into-array File)
(.getJavaFileObjectsFromFiles file-mgr))]
(when (seq srcs)
(util/info "Compiling %d Java source files...\n" (count srcs))
(-> compiler (.getTask *err* file-mgr diag-coll opts nil srcs) .call)
(doseq [d (.getDiagnostics diag-coll) :let [k (.getKind d)]]
(when (= Diagnostic$Kind/ERROR k) (reset! throw? true))
(let [log (handler k util/info)]
(if (nil? (.getSource d))
(log "%s: %s\n"
(.toString k)
(.getMessage d nil))
(log "%s: %s, line %d: %s\n"
(.toString k)
(.. d getSource getName)
(.getLineNumber d)
(.getMessage d nil)))))
(.close file-mgr)
(when @throw? (throw (Exception. "java compiler error")))))
(-> fs (core/add-resource tgt) core/commit!))))
(defn- sift-poms
[fileset project]
(let [poms (->> (core/output-files fileset)
(core/by-name ["pom.xml"]))
prj-match (when project
(str "/" (pod/full-id project) "/pom.xml"))
project? #(if-not prj-match
(:project %)
(.endsWith (core/tmp-path %) prj-match))]
(if (< (count poms) 2) poms (->> poms (filter project?)))))
(core/deftask jar
"Build a jar file for the project."
[f file PATH str "The target jar file name."
M manifest KEY=VAL {str str} "The jar manifest map."
m main MAIN sym "The namespace containing the -main function."
p project SYM sym "The project symbol -- used to find the correct pom.xml file."]
(let [old-fs (atom nil)
tgt (core/tmp-dir!)
out (atom nil)]
(core/with-pre-wrap [fs]
(let [new-fs (core/output-fileset fs)
[pom & p] (map core/tmp-file (sift-poms fs project))
{:keys [project version]}
(when (and pom (not (seq p)))
(boot.pom/pom-xml-parse-string ~(slurp pom))))
pomname (when (and project version)
(str (name project) "-" version ".jar"))
fname (or file pomname "project.jar")
out* (io/file tgt fname)]
(when (and project (seq p))
(util/warn "Multiple pom.xml files for project: %s\n" project))
(when (not= @out out*)
(when (and @out (.exists @out))
(file/move @out out*))
(reset! out out*))
(util/info "Writing %s...\n" fname)
(jar/update-jar! @out @old-fs (reset! old-fs new-fs) manifest main)
(-> fs (core/add-resource tgt) core/commit!)))))
(core/deftask war
"Create war file for web deployment."
[f file PATH str "The target war file name."]
(let [tgt (core/tmp-dir!)]
(core/with-pre-wrap [fs]
(core/empty-dir! tgt)
(let [warname (or file "project.war")
warfile (io/file tgt warname)
inf? #(contains? #{"META-INF" "WEB-INF"} %)
->war #(let [r (core/tmp-path %)
r' (file/split-path r)
path (->> (if (.endsWith r ".jar")
["lib" (last r')]
(into ["classes"] r'))
(into ["WEB-INF"]))]
(if (inf? (first r')) r (.getPath (apply io/file path))))
entries (core/output-files fs)
index (->> entries (mapv (juxt ->war #(.getPath (core/tmp-file %)))))]
(util/info "Writing %s...\n" (.getName warfile))
(jar/spit-jar! (.getPath warfile) index {} nil)
(-> fs (core/add-resource tgt) core/commit!)))))
(core/deftask zip
"Build a zip file for the project."
[f file PATH str "The target zip file name."]
(let [old-fs (atom nil)
tgt (core/tmp-dir!)
fname (or file "")
out (io/file tgt fname)]
(core/with-pre-wrap [fs]
(let [new-fs (core/output-fileset fs)]
(util/info "Writing %s...\n" fname)
(jar/update-zip! out @old-fs (reset! old-fs new-fs))
(-> fs (core/add-resource tgt) core/commit!)))))
(core/deftask install
"Install project jar to local Maven repository.
The --file option allows installation of arbitrary jar files. If no
file option is given then any jar artifacts created during the build
will be installed.
The pom.xml file that's required when installing a jar can usually be
found in the jar itself. However, sometimes a jar might contain more
than one pom.xml file or may not contain one at all.
The --pom option can be used in these situations to specify which
pom.xml file to use. The optarg denotes either the path to a pom.xml
file in the filesystem or a subdir of the META-INF/maven/ dir in which
the pom.xml contained in the jar resides.
Given a jar file (warp-0.1.0.jar) with the following contents:
│ └── maven
│ └── tailrecursion
│ └── warp
│ ├──
│ └── pom.xml
└── tailrecursion
└── warp.clj
The jar could be installed with the following boot command:
$ boot install -f warp-0.1.0.jar -p tailrecursion/warp
Note that if you want to install some other artifact along with the main one,
for instance the classic sources or javadoc artifact, you have to add the
classifier to your pom.xml, which translates to adding :classifier to the pom
[f file PATH str "The jar file to install."
p pom PATH str "The pom.xml file to use."]
(core/with-pass-thru [fs]
(let [jarfiles (or (and file [(io/file file)])
(->> (core/output-files fs)
(core/by-ext [".jar"])
(map core/tmp-file)))]
(when-not (seq jarfiles) (throw (Exception. "can't find jar file")))
(doseq [jarfile jarfiles]
(util/info "Installing %s...\n" (.getName jarfile))
(boot.aether/install ~(core/get-env) ~(.getPath jarfile) ~pom))))))
(core/deftask push
"Deploy jar file to a Maven repository.
If the file option is not specified the task will look for jar files
created by the build pipeline. The jar file(s) must contain pom.xml
The repo option is required. The repo option is used to get repository
map from Boot envinronment. Additional repo-map option can be used to
add options, like credentials, or to provide complete repo-map if Boot
envinronment doesn't hold the named repository.
If you receive a \"Could not sign ... gpg: no default secret key: secret key
not available\" error, make sure boot is using the right gpg executable. You
can use the BOOT_GPG_COMMAND environment variable for that.
In order to use gpg2, for instance, run:
BOOT_GPG_COMMAND=gpg2 boot push --gpg-sign ..."
[f file PATH str "The jar file to deploy."
P pom PATH str "The pom.xml file to use (see install task)."
F file-regex MATCH #{regex} "The set of regexes of paths to deploy."
g gpg-sign bool "Sign jar using GPG private key."
k gpg-user-id KEY str "The name or key-id used to select the signing key."
^{:deprecated "Check GPG help about changing GNUPGHOME."}
K gpg-keyring PATH str "The path to secring.gpg file to use for signing."
p gpg-passphrase PASS str "The passphrase to unlock GPG signing key."
r repo NAME str "The name of the deploy repository."
e repo-map REPO edn "The repository map of the deploy repository."
t tag bool "Create git tag for this version."
B ensure-branch BRANCH str "The required current git branch."
C ensure-clean bool "Ensure that the project git repo is clean."
R ensure-release bool "Ensure that the current version is not a snapshot."
S ensure-snapshot bool "Ensure that the current version is a snapshot."
T ensure-tag TAG str "The SHA1 of the commit the pom's scm tag must contain."
V ensure-version VER str "The version the jar's pom must contain."]
(let [tgt (core/tmp-dir!)]
(core/with-pass-thru [fs]
(core/empty-dir! tgt)
(let [jarfiles (or (and file [(io/file file)])
(->> (core/output-files fs)
(core/by-ext [".jar"])
((if (seq file-regex) #(core/by-re file-regex %) identity))
(map core/tmp-file)))
; Get options from Boot env by repo name
r (get (->> (core/get-env :repositories) (into {})) repo)
repo-map (merge r (when repo-map ((core/configure-repositories!) repo-map)))]
(when-not (and repo-map (seq jarfiles))
(throw (Exception. "missing jar file or repo not found")))
(doseq [f jarfiles]
(let [{{t :tag} :scm
v :version
c :classifier} (pod/pom-xml-map f pom)
b (util/guard (git/branch-current))
commit (util/guard (git/last-commit))
tags (util/guard (git/ls-tags))
clean? (util/guard (git/clean?))
snapshot? (.endsWith v "-SNAPSHOT")
main-artifact? (nil? c)
artifact-map (when gpg-sign
(let [gpg-options {:gpg-key gpg-user-id
:gpg-passphrase gpg-passphrase}
jar-artifact (do (util/info "Signing %s...\n" (.getName f))
(gpg/sign-jar tgt f gpg-options))
;; If it has :classifier, it is probably not the
;; main artifact so we skip signing its pom
pom-artifact (when main-artifact?
(util/info "Signing POM %s...\n" pom)
(gpg/sign-pom tgt f pom gpg-options))]
(into {} [jar-artifact pom-artifact])))]
(util/dbug* "Signed artifact-map %s\n" artifact-map)
(assert (or (not ensure-branch) (= b ensure-branch))
(format "current git branch is %s but must be %s" b ensure-branch))
(assert (or (not ensure-clean) clean?)
"project repo is not clean")
(assert (or (not ensure-release) (not snapshot?))
(format "not a release version (%s)" v))
(assert (or (not ensure-snapshot) snapshot?)
(format "not a snapshot version (%s)" v))
(assert (or (not ensure-tag) (not t) (= t ensure-tag))
(format "scm tag in pom doesn't match (%s, %s)" t ensure-tag))
(when (and ensure-tag (not t))
(util/warn "The --ensure-tag option was specified but scm info is missing from pom.xml\n"))
(assert (or (not ensure-version) (= v ensure-version))
(format "jar version doesn't match project version (%s, %s)" v ensure-version))
(util/info "Deploying %s...\n" (.getName f))
~(core/get-env) ~[repo repo-map] ~(.getPath f) ~pom ~artifact-map))
(when tag
(if (and tags (= commit (get tags tag)))
(util/info "Tag %s already created for %s\n" tag commit)
(do (util/info "Creating tag %s...\n" v)
(git/tag v "release"))))))))))
(core/deftask call
"Call a function or evaluate a piece of code
Multiple functions or forms can be specified. For namespaced functions, the
namespace is automatically required. Execution occurs before tasks appearing
later in the pipeline (or after the tasks, if --post is specified)."
[f function SYMBOL [sym] "Functions to execute"
e eval FORM [edn] "Forms to execute"
p print bool "Print results to *out*"
P post bool "Execute after rather than before subsequent tasks"
o once bool "Run only once if used with watch"]
(let [called? (atom false)
run (fn []
(when (or (not once) (not @called?))
(doseq [ns (->> function
(map #(some-> % namespace symbol))
(remove nil?)
(require ns))
(doseq [sym function]
(if-let [f (resolve sym)]
(util/dbug "Invoking %s...\n" sym)
(cond-> (f)
print println))
(throw (RuntimeException. (str "Could not resolve symbol " sym)))))
(doseq [form eval]
(util/dbug "Evaluating %s...\n" (pr-str form))
(cond-> (clojure.core/eval form)
print println))
(reset! called? true)))]
(if post
(core/with-post-wrap [fileset]
(core/with-pre-wrap [fileset]