diff --git a/.gitignore b/.gitignore index 50f1b56..3242e09 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ pom.xml.asc /boot /build.clj /docs +*.iml diff --git a/README.md b/README.md index 165ac70..8c8e667 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Stories in Ready](https://badge.waffle.io/tailrecursion/boot.core.png?label=ready&title=Ready)](https://waffle.io/tailrecursion/boot.core) # boot.core Please see the main [Boot Repository][1] for more info. diff --git a/project.clj b/project.clj index 8724330..906d382 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject tailrecursion/boot.core "2.3.1" +(defproject tailrecursion/boot.core "2.5.0" :description "A script interpreter for Clojure. Also a build tool." :url "https://github.com/tailrecursion/boot.core" :license {:name "Eclipse Public License" diff --git a/src/tailrecursion/boot/core.clj b/src/tailrecursion/boot/core.clj index 9370383..5850589 100644 --- a/src/tailrecursion/boot/core.clj +++ b/src/tailrecursion/boot/core.clj @@ -42,7 +42,7 @@ (let [deps (resolve 'tailrecursion.boot.loader/dependencies) add! (resolve 'tailrecursion.boot.loader/add-dependencies!)] (add! new (:repositories env)) - (if deps @@deps (into (or old []) new)))) + (into (or old []) new))) (defn- add-directories! "Add URLs (directories or JAR files) to the classpath." @@ -76,8 +76,8 @@ :out-path "out" :src-paths #{} :src-static #{} - :repositories #{"http://clojars.org/repo/" - "http://repo1.maven.org/maven2/"} + :repositories {"clojars" "http://clojars.org/repo/" + "central" "http://repo1.maven.org/maven2/"} :test "test" :target "target" :resources "resources" @@ -145,7 +145,7 @@ (fn [key old-value new-value env] key) :default ::default) (defmethod merge-env! ::default [key old new env] new) -(defmethod merge-env! :repositories [key old new env] (into (or old #{}) new)) +(defmethod merge-env! :repositories [key old new env] (into (or old {}) (if (set? new) (zipmap new new) new))) (defmethod merge-env! :src-static [key old new env] (into (or old #{}) new)) (defmethod merge-env! :src-paths [key old new env] (into (or old #{}) new)) (defmethod merge-env! :dependencies [key old new env] (add-dependencies! old new env)) @@ -169,7 +169,7 @@ (if k (get @boot-env k not-found) @boot-env)) (defn add-sync! - "Specify directories to sync after build event. The `dst` argument is the + "Specify directories to sync after build event. The `dst` argument is the destination directory. The `srcs` are an optional list of directories whose contents will be copied into `dst`. The `add-sync!` function is associative. @@ -186,7 +186,7 @@ (defn consume-src! "Tasks use this function to declare that they \"consume\" certain files. Files - in staging directories which are consumed by tasks will not be synced to the + in staging directories which are consumed by tasks will not be synced to the `:out-path` at the end of the build cycle. The `filter` argument is a function which will be called with the seq of artifact `java.io.File` objects from the task staging directories. It should return a seq of files to be comsumed. @@ -232,7 +232,7 @@ (tmp/tmpfile? (tmpreg) f)) (defn mktmp! - "Create a temp file and return its `File` object. If `mktmp!` has already + "Create a temp file and return its `File` object. If `mktmp!` has already been called with the given `key` the tmpfile will be truncated. The optional `name` argument can be used to customize the temp file name (useful for creating temp files with a specific file extension, for example)." diff --git a/src/tailrecursion/boot/core/task.clj b/src/tailrecursion/boot/core/task.clj index 27d161f..800402c 100644 --- a/src/tailrecursion/boot/core/task.clj +++ b/src/tailrecursion/boot/core/task.clj @@ -224,7 +224,7 @@ (recur (core/make-event event))))) (defn files-changed? - [& [type]] + [& [type quiet?]] (let [dirs (remove core/tmpfile? (core/get-env :src-paths)) watchers (map file/make-watcher dirs) since (atom 0)] @@ -237,9 +237,13 @@ (clean :hash))] (if-let [mods (->> (or type :time) (get info) seq)] (do - (let [path (file/path (first mods)) - ok "\033[34m↳ Elapsed time: %6.3f sec ›\033[33m 00:00:00 \033[0m" - fail "\n\033[31m%s\033[0m\n\033[34m↳ Elapsed time: %6.3f sec ›\033[33m 00:00:00 \033[0m"] + (let [path (file/path (first mods)) + ok-v "\033[34m↳ Elapsed time: %6.3f sec ›\033[33m 00:00:00 \033[0m" + ok-q "Elapsed time: %6.3f sec\n" + fail-v "\n\033[31m%s\033[0m\n\033[34m↳ Elapsed time: %6.3f sec ›\033[33m 00:00:00 \033[0m" + fail-q "\n%s\nElapsed time: %6.3f sec\n" + ok (if quiet? ok-q ok-v) + fail (if quiet? fail-q fail-v)] (when (not= 0 @since) (println)) (reset! since (:time event)) (print-time ok fail (continue (assoc event :watch info))))) @@ -249,16 +253,19 @@ m (mod (long (/ diff 60)) 60) h (mod (long (/ diff 3600)) 24)] (core/sync!) - (printf "\033[33m%s%02d:%02d:%02d \033[0m" pad h m s)))))))) + (when-not quiet? (printf "\033[33m%s%02d:%02d:%02d \033[0m" pad h m s))))))))) (core/deftask watch "Watch `:src-paths` and call its continuation when files change. - The `:type`option specifies how changes to files are detected and can be - either `:time`or `:hash` (default `:time`). The `:msec` option specifies the - polling interval in milliseconds (default 200)." - [& {:keys [type msec] :or {type :time msec 200}}] - (comp (auto msec) (files-changed? type))) + Options: + + :type Specifies how changes to files are detected and can be either + :time or :hash (default :time). + :msec Specifies the polling interval in milliseconds (default 200). + :quiet When set to true no ANSI colors or updating timer are printed. " + [& {:keys [type msec quiet] :or {type :time msec 200 quiet false}}] + (comp (auto msec) (files-changed? type quiet))) (core/deftask syncdir "Copy/sync files between directories. diff --git a/src/tailrecursion/boot/core/task/repl.clj b/src/tailrecursion/boot/core/task/repl.clj new file mode 100644 index 0000000..348fd18 --- /dev/null +++ b/src/tailrecursion/boot/core/task/repl.clj @@ -0,0 +1,170 @@ +(ns tailrecursion.boot.core.task.repl + "Start a repl session with the current project." + (:require [clojure.set :as set] + [clojure.string :as string] + [clojure.java.io :as io] + [tailrecursion.boot.core :as core])) + +(def default-cfg + "Default configuration is deep-merged with the options passed to repl task" + {:host "127.0.0.1" + :port 0 + :middlewares [] + :timeout 60000 + :init-ns 'user + :repl-options {:history-file (str (io/file ".repl-history")) + :input-stream System/in}}) + +(defn deep-merge + "Recursively merges maps. If vals are not maps, chooses last value" + [& vals] + (if (every? map? vals) + (apply merge-with deep-merge vals) + (last vals))) + +(def ^:private nrepl-port-file (io/file ".nrepl-port")) + +(def ^:private ack-server + (delay ((resolve 'clojure.tools.nrepl.server/start-server) + :bind (:host default-cfg) + :handler ((resolve 'clojure.tools.nrepl.ack/handle-ack) + (resolve 'clojure.tools.nrepl.server/unknown-op))))) + +(defn ^:private nrepl-started-msg + [host port] + (str "nREPL server started on port " port " on host " host + " - nrepl://" host ":" port)) + +(defn ^:private start-server + [{:as cfg + :keys [host middlewares ack-port]}] + (let [headless? (nil? ack-port) + cfg (-> (set/rename-keys cfg {:host :bind}) + (assoc :handler (apply (resolve 'clojure.tools.nrepl.server/default-handler) middlewares)) + (select-keys [:bind :port :handler :ack-port])) + {:as server :keys [port]} (->> (apply concat cfg) + (apply (resolve 'clojure.tools.nrepl.server/start-server)))] + (when headless? + (println (nrepl-started-msg host port))) + (spit (doto nrepl-port-file .deleteOnExit) port) + @(promise))) + +(defn ^:private start-server-in-thread + [{:as cfg + :keys [host timeout]}] + ((resolve 'clojure.tools.nrepl.ack/reset-ack-port!)) + (let [ack-port (:port @ack-server)] + (-> (bound-fn [] + (start-server (assoc cfg :ack-port ack-port))) + (Thread.) + (.start))) + (if-let [repl-port ((resolve 'clojure.tools.nrepl.ack/wait-for-ack) timeout)] + (do (println (nrepl-started-msg host repl-port)) + repl-port) + (throw (ex-info "REPL server launch timed out." {})))) + +(defn ^:private options-for-reply + [opts & {:keys [attach port]}] + (as-> opts opts + (apply dissoc opts (concat [:init] (if attach [:host :port]))) + (merge opts (cond attach {:attach (str attach)} + port {:port port} + :else {})) + (set/rename-keys opts {:prompt :custom-prompt + :welcome :custom-help}) + (if (:port opts) (update-in opts [:port] str) opts))) + +(defn ^:private client + [{:keys [repl-options]} attach] + ((resolve 'reply.main/launch-nrepl) + (options-for-reply repl-options :attach attach))) + +(defn ^:private connect-string + [opts] + (as-> (str (first opts)) x + (string/split x #":") + (remove string/blank? x) + (-> (drop-last (count x) [(:host default-cfg) + (try (slurp nrepl-port-file) + (catch Exception _ ""))]) + (concat x)) + (string/join ":" x) + (if (re-find #":\d+($|/.*$)" x) + x + (throw (ex-info "Port is required" {:connect-string x}))))) + +(defn wrap-init-ns + [init-ns] + (with-local-vars + [wrap-init-ns' + (fn [h] + ;; this needs to be a var, since it's in the nREPL session + (with-local-vars [init-ns-sentinel nil] + (fn [{:keys [session] :as msg}] + (when-not (@session init-ns-sentinel) + (swap! session assoc + (var *ns*) + (try (require init-ns) (create-ns init-ns) + (catch Throwable t (create-ns 'user))) + init-ns-sentinel true)) + (h msg))))] + (doto wrap-init-ns' + ;; set-descriptor! currently nREPL only accepts a var + ((resolve 'clojure.tools.nrepl.middleware/set-descriptor!) + {:requires #{(resolve 'clojure.tools.nrepl.middleware.session/session)} + :expects #{"eval"}}) + (alter-var-root (constantly @wrap-init-ns'))))) + +(defn repl-cfg + ([opts] (repl-cfg opts {})) + ([opts event] + (as-> (apply hash-map opts) cfg + (deep-merge default-cfg (get event ::config {}) cfg) + (update-in cfg [:middlewares] conj (wrap-init-ns (:init-ns cfg)))))) + +(defn headless + [opts] + (start-server (repl-cfg opts core/*event*))) + +(def ^:private client-mode? (complement #{:headless :pass-through})) + +(defn ^:private load-dynamic-dependencies + [[cmd & opts :as args]] + (core/set-env! :dependencies '[[org.clojure/tools.nrepl "0.2.3"]]) + (require 'clojure.tools.nrepl.ack) + (require 'clojure.tools.nrepl.server) + (when (client-mode? cmd) + (core/set-env! :dependencies '[[reply "0.3.0"]]) + (require 'reply.main))) + +(core/deftask repl + "Start a repl session for the current project. + +Subcommands: + + | [:host \"127.0.0.1\"] [:port random] [:middlewares []] [:init-ns 'user] + +:headless [:host host] [:port port] [:middlewares []] + This will launch an nREPL server and wait, rather than connecting + a client to it. + +:pass-through [:host host] [:port port] [:middlewares []] + Like :headless, but does not block the task chain. + +:connect [dest] + Connects to an already running nREPL server. Dest can be: + - host:port -- connects to the specified host and port; + - port -- host defaults to localhost + + If no dest is given, resolves the host as described above + and the port from .nrepl-port in the project root." + [& [cmd & opts :as args]] + (load-dynamic-dependencies args) + (core/with-pre-wrap + (condp = cmd + :connect (client default-cfg (connect-string opts)) + :headless (headless opts) + :pass-through (future (headless opts)) + (let [cfg (repl-cfg args core/*event*)] + (->> (start-server-in-thread cfg) + (client cfg)))))) diff --git a/src/tailrecursion/boot/deps.clj b/src/tailrecursion/boot/deps.clj index c291207..5a6eb12 100644 --- a/src/tailrecursion/boot/deps.clj +++ b/src/tailrecursion/boot/deps.clj @@ -23,15 +23,54 @@ (defn ^:deprecated resolve-deps* [coords repos] (require 'cemerick.pomegranate.aether) (let [resolve-dependencies (resolve 'cemerick.pomegranate.aether/resolve-dependencies)] - (->> (resolve-dependencies :coordinates coords :repositories (zipmap repos repos)) - (kahn/topo-sort) - (map (fn [x] {:dep x :jar (.getPath (:file (meta x)))}))))) + (->> (resolve-dependencies :coordinates coords :repositories repos) + (kahn/topo-sort) + (map (fn [x] {:dep x :jar (.getPath (:file (meta x)))}))))) + +(def ^:private memoized-resolve-deps! (atom nil)) + +(defn resolve-deps! + "FIXME: look at this function. just look at it." + [] + (require 'tailrecursion.boot.loader) + (require 'tailrecursion.boot.classlojure.core) + (when-let [get-loader (resolve 'tailrecursion.boot.loader/get-classloader)] + (compare-and-set! memoized-resolve-deps! nil + (let [eval-in (resolve 'tailrecursion.boot.classlojure.core/eval-in)] + (memoize + (fn [coords repos] + (eval-in (get-loader) + `(do (require + '[clojure.set :as ~'x-set] + '[cemerick.pomegranate.aether :as ~'x-aether] + '[tailrecursion.boot-classloader :as ~'x-loader]) + (let [res# (fn [x#] (x-aether/resolve-dependencies + :coordinates x# + :repositories (zipmap '~repos '~repos))) + sort# (partial group-by (comp zero? count second)) + proc# (fn [x#] (->> x# (map (juxt ffirst (comp set (partial map first) second))))) + derp# (fn [x#] (->> x# first vector res# + (filter (comp (partial = (ffirst x#)) ffirst)) + first second)) + diff# (fn [x# y#] (x-set/difference y# x#)) + d# (res# '~coords) + deps# (->> d# (reduce (fn [xs# x#] (assoc xs# (ffirst x#) (first x#))) {})) + dd# (->> d# (map (juxt first derp#)) proc# sort#) + dd# (loop [sorted# (dd# true) unsorted# (dd# false)] + (if-not (seq unsorted#) + (map first sorted#) + (let [set# (set (map first sorted#)) + ddd# (->> unsorted# + (map (juxt first (comp (partial diff# set#) second))) + sort#)] + (recur (into sorted# (ddd# true)) (ddd# false)))))] + (->> dd# (map (fn [x#] {:dep (deps# x#) :jar (.getPath (:file (meta (deps# x#))))})))))))))) + @memoized-resolve-deps!)) (defn deps [env] (require 'tailrecursion.boot.loader) - (let [resolve-deps! (resolve 'tailrecursion.boot.loader/resolve-dependencies!) - {repos :repositories coords :dependencies} @env] - (->> ((or resolve-deps! resolve-deps*) coords repos) + (let [{repos :repositories coords :dependencies} @env] + (->> ((or (resolve-deps!) resolve-deps*) coords repos) (map :jar) (filter #(.endsWith % ".jar")) (map #(JarFile. (io/file %)))