├── test └── lancet │ └── test │ ├── data │ ├── file-1 │ └── file-2 │ ├── coerce.clj │ ├── lancet.clj │ ├── runonce.clj │ └── ant.clj ├── .gitignore ├── examples ├── src │ └── jvm │ │ └── Sample.java └── build-clojure.clj ├── project.clj ├── README ├── LICENSE └── src └── lancet └── core.clj /test/lancet/test/data/file-1: -------------------------------------------------------------------------------- 1 | a file 2 | -------------------------------------------------------------------------------- /test/lancet/test/data/file-2: -------------------------------------------------------------------------------- 1 | another file 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | classes/ 3 | pom.xml 4 | *jar 5 | -------------------------------------------------------------------------------- /examples/src/jvm/Sample.java: -------------------------------------------------------------------------------- 1 | public class Sample {} 2 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lancet "1.0.1" 2 | :description "Dependency-based builds, Clojure Style, with optional Ant underneath." 3 | :license {:name "MIT X11 License"} 4 | :dependencies [[org.apache.ant/ant "1.7.1"] 5 | [org.apache.ant/ant-nodeps "1.7.1"]] 6 | :eval-in-leiningen true) 7 | -------------------------------------------------------------------------------- /test/lancet/test/coerce.clj: -------------------------------------------------------------------------------- 1 | (ns lancet.test.coerce 2 | (:use [clojure.test]) 3 | (:use [lancet.core])) 4 | 5 | (deftest boolean-coerce 6 | (are [_1 _2] (= _1 _2) 7 | (coerce Boolean/TYPE "yes") true 8 | (coerce Boolean/TYPE "YES") true 9 | (coerce Boolean/TYPE "on") true 10 | (coerce Boolean/TYPE "ON") true 11 | (coerce Boolean/TYPE "true") true 12 | (coerce Boolean/TYPE "TRUE") true 13 | (coerce Boolean/TYPE "no") false 14 | (coerce Boolean/TYPE "foo") false)) 15 | 16 | (deftest file-coerce 17 | (is (= (coerce java.io.File "foo") (java.io.File. "foo")))) 18 | 19 | (deftest default-coerce 20 | (is (= (coerce Comparable 10) 10))) 21 | -------------------------------------------------------------------------------- /test/lancet/test/lancet.clj: -------------------------------------------------------------------------------- 1 | (ns lancet.test.lancet 2 | (:use [clojure.test] 3 | [clojure.set :only (intersection)]) 4 | (:use [lancet.core])) 5 | 6 | ;; Predicates 7 | 8 | (deftest test-has-run? 9 | (def #^{:has-run (fn [] :bang)} fn#) 10 | (is (= :bang (has-run? fn#)))) 11 | 12 | (deftest test-reset 13 | (def #^{:reset-fn (fn [] :zap)} fn#) 14 | (is (= :zap (reset fn#)))) 15 | 16 | (deftest test-task-names 17 | (let [some-names #{'echo 'mkdir}] 18 | (is (= (intersection (into #{} (task-names)) some-names) some-names)))) 19 | 20 | (deftest test-safe-ant-name 21 | (are [_1 _2] (= _1 _2) 22 | (safe-ant-name 'echo) 'echo 23 | (safe-ant-name 'import) 'ant-import)) 24 | 25 | (deftest test-define-all-ant-tasks-defines-echo 26 | (let [echo-task (echo {:description "foo"})] 27 | (are [_1 _2] (= _1 _2) 28 | (.getDescription echo-task) "foo" 29 | (class echo-task) org.apache.tools.ant.taskdefs.Echo))) 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Lancet 2 | Dependency-based builds, Clojure Style, with optional Ant underneath 3 | Stuart Halloway 4 | (stu at thinkrelevance dot com) 5 | 6 | This is early days. To try it out: 7 | 8 | (1) Get the root directory of lancet on your CLASSPATH 9 | 10 | (2) Launch Clojure REPL from the examples directory 11 | 12 | (3) (load-file "build-clojure.clj") 13 | 14 | Targets are functions, so you can call them. They will run only once, however. 15 | Targets track whether they have been run via a reference in their metadata. 16 | If you read build-clojure.clj and then read the REPL session below you will 17 | have an idea how it all fits together: 18 | 19 | user=> (load-file "build-clojure.clj") 20 | #=(var user/jar) 21 | user=> (jar) 22 | [mkdir] Created dir: /Users/stuart/relevance/personal/tantalus/lancet/examples/classes 23 | [javac] Compiling 1 source file to classes 24 | [jar] Building jar: /Users/stuart/relevance/personal/tantalus/lancet/examples/clojure.jar 25 | true 26 | user=> (jar) 27 | true 28 | user=> (target-run? jar) 29 | true 30 | -------------------------------------------------------------------------------- /test/lancet/test/runonce.clj: -------------------------------------------------------------------------------- 1 | (ns lancet.test.runonce 2 | (:use [clojure.test]) 3 | (:use [lancet.core])) 4 | 5 | (def counter (ref 0)) 6 | (defn inc-counter [] (dosync (alter counter inc))) 7 | (defn zero-counter! [] (dosync (ref-set counter 0))) 8 | 9 | (deftest test-runonce 10 | (zero-counter!) 11 | (let [[has-run? reset f] (runonce inc-counter)] 12 | 13 | ;; TODO: add nested contexts to test-is, a la PCL 14 | ;; initial state 15 | (are [_1 _2] (= _1 _2) 16 | (has-run?) false 17 | @counter 0) 18 | 19 | ;; run the fn 20 | (are [_1 _2] (= _1 _2) 21 | (f) 1 22 | (has-run?) true 23 | @counter 1) 24 | 25 | ;; run the fn again (no change) 26 | (are [_1 _2] (= _1 _2) 27 | (f) 1 28 | (has-run?) true 29 | @counter 1) 30 | 31 | ;; reset the fn 32 | (reset) 33 | (are [_1 _2] (= _1 _2) 34 | (has-run?) false) 35 | 36 | ;; run the fn again 37 | (are [_1 _2] (= _1 _2) 38 | (f) 2 39 | (has-run?) true 40 | @counter 2))) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Stuart Halloway. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples/build-clojure.clj: -------------------------------------------------------------------------------- 1 | (use 'lancet) 2 | 3 | (def src "src") 4 | (def jsrc (str src "/jvm")) 5 | (def cljsrc (str src "/clj")) 6 | (def build "classes") 7 | (def clojure_jar "clojure.jar") 8 | (def bootclj (str cljsrc "/clojure/boot.clj")) 9 | 10 | (deftarget test-target 11 | "Simple echo to test plumbing" 12 | (echo {:message "test target"})) 13 | 14 | (deftarget init 15 | (tstamp {}) 16 | (mkdir {:dir build})) 17 | 18 | (deftarget compile 19 | "Compile Java sources." 20 | (init) 21 | (javac {:srcdir jsrc :destdir build 22 | :includejavaruntime "yes" 23 | :debug "true" 24 | :target "1.5"})) 25 | 26 | (deftarget clean 27 | "Remove autogenerated files and directories" 28 | (delete {:dir build})) 29 | 30 | (deftarget make-jar 31 | "Create jar file." 32 | (compile) 33 | (jar {:jarfile clojure_jar 34 | :basedir build})) 35 | 36 | ;; TODO: tie together ant tasks and subsidiary data types 37 | ;; such as fileset and manifest 38 | 39 | ;; (target {:name "jar" 40 | ;; :depends "compile" 41 | ;; :description "Create jar file."} 42 | ;; (jar {:jarfile clojure_jar 43 | ;; :basedir build} 44 | ;; (fileset {:dir cljsrc :includes "**/*.clj"}))) 45 | ;; ) 46 | -------------------------------------------------------------------------------- /test/lancet/test/ant.clj: -------------------------------------------------------------------------------- 1 | (ns lancet.test.ant 2 | (:import (java.util.logging Level Logger)) 3 | (:use [clojure.test]) 4 | (:use [lancet.core])) 5 | 6 | ;; not general purpose! 7 | (defn get-field [obj field] 8 | (let [fld (.getDeclaredField (class obj) field)] 9 | (.setAccessible fld true) 10 | (.get fld obj))) 11 | 12 | (deftest test-ant-project 13 | (let [listeners (.getBuildListeners ant-project)] 14 | (is (= (count (filter #(= (class %) org.apache.tools.ant.NoBannerLogger) 15 | listeners)) 16 | 1)))) 17 | 18 | (deftest test-instantiate-task 19 | (let [echo-task (instantiate-task ant-project "echo" {:message "foo"})] 20 | (is (= (get-field echo-task "message") "foo"))) 21 | (is (thrown? IllegalArgumentException (instantiate-task ant-project 22 | "not-a-task-name")))) 23 | 24 | ;; using Logger as an example bean. Amazing how few built-in Java 25 | ;; classes are beans... 26 | (deftest test-property-setters 27 | (let [bean (Logger/getAnonymousLogger)] 28 | (is (nil? (.getLevel bean))) 29 | (set-property! bean "level" Level/SEVERE) 30 | (is (= (.getLevel bean) Level/SEVERE)) 31 | (set-properties! bean {:level Level/INFO}) 32 | (is (= (.getLevel bean) Level/INFO)))) 33 | 34 | (deftest test-property-descriptor 35 | (let [bean (Logger/getAnonymousLogger)] 36 | (is (nil? (property-descriptor bean "foobar"))) 37 | (is (not (nil? (property-descriptor bean "level")))))) 38 | 39 | (defn fileset-names [fs] 40 | (map #(.getName %) (iterator-seq (.iterator fs)))) 41 | 42 | (deftest test-fileset 43 | (let [fs (fileset {:dir "test/lancet/test/data"})] 44 | (is (= ["file-1" "file-2"] (fileset-names fs))))) 45 | 46 | (deftest test-adding-fileset-to-task 47 | ;; absence of an exception demonstrates add(...) call did not blow up 48 | (let [task (instantiate-task ant-project 49 | "copy" 50 | {} 51 | (fileset {:dir "lancet/test/data"}))])) 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/lancet/core.clj: -------------------------------------------------------------------------------- 1 | (ns lancet.core 2 | (:gen-class) 3 | (:import (java.beans Introspector) 4 | (java.util.concurrent CountDownLatch) 5 | (org.apache.tools.ant.types Path) 6 | (org.apache.tools.ant.taskdefs Manifest$Attribute) 7 | (java.util Map))) 8 | 9 | (def #^{:doc "Dummy ant project to keep Ant tasks happy"} 10 | ant-project 11 | (let [proj (org.apache.tools.ant.Project.) 12 | logger (org.apache.tools.ant.NoBannerLogger.)] 13 | (doto logger 14 | (.setMessageOutputLevel org.apache.tools.ant.Project/MSG_INFO) 15 | (.setEmacsMode true) 16 | (.setOutputPrintStream System/out) 17 | (.setErrorPrintStream System/err)) 18 | (doto proj 19 | (.init) 20 | (.addBuildListener logger)))) 21 | 22 | (defmulti coerce (fn [dest-class src-inst] [dest-class (class src-inst)])) 23 | 24 | (defmethod coerce [java.io.File String] [_ str] 25 | (java.io.File. str)) 26 | (defmethod coerce [Boolean/TYPE String] [_ str] 27 | (contains? #{"on" "yes" "true"} (.toLowerCase str))) 28 | (defmethod coerce :default [dest-cls obj] (cast dest-cls obj)) 29 | (defmethod coerce [Path String] [_ str] 30 | (Path. ant-project str)) 31 | 32 | (defn env [val] 33 | (System/getenv (name val))) 34 | 35 | (defn- build-sh-args [args] 36 | (concat (.split (first args) " +") (rest args))) 37 | 38 | (defn property-descriptor [inst prop-name] 39 | (first 40 | (filter #(= prop-name (.getName %)) 41 | (.getPropertyDescriptors 42 | (Introspector/getBeanInfo (class inst)))))) 43 | 44 | (defn get-property-class [write-method] 45 | (first (.getParameterTypes write-method))) 46 | 47 | (defn set-property! [inst prop value] 48 | (let [pd (property-descriptor inst prop)] 49 | (when-not pd 50 | (throw (Exception. (format "No such property %s." prop)))) 51 | (let [write-method (.getWriteMethod pd) 52 | dest-class (get-property-class write-method)] 53 | (.invoke write-method inst (into-array [(coerce dest-class value)]))))) 54 | 55 | (defn set-properties! [inst prop-map] 56 | (doseq [[k v] prop-map] (set-property! inst (name k) v))) 57 | 58 | (def ant-task-hierarchy 59 | (atom (-> (make-hierarchy) 60 | (derive ::exec ::has-args)))) 61 | 62 | (defmulti add-nested 63 | "Adds a nested element to ant task. 64 | Elements are added in a different way for each type. 65 | Task name keywords are connected into a hierarchy which can 66 | be used to extensively add other types to this method. 67 | The default behaviour is to add an element as a fileset." 68 | (fn [name task nested] [(keyword "lancet.core" name) (class nested)]) 69 | :hierarchy ant-task-hierarchy) 70 | 71 | (defmethod add-nested [::manifest Map] 72 | [_ task props] 73 | (doseq [[n v] props] (.addConfiguredAttribute task 74 | (Manifest$Attribute. n v)))) 75 | (defmethod add-nested [::has-args String] 76 | [_ task arg] 77 | (doto (.createArg task) (.setValue arg))) 78 | 79 | (defmethod add-nested :default [_ task nested] (.addFileset task nested)) 80 | 81 | (defn instantiate-task [project name props & nested] 82 | (let [task (.createTask project name)] 83 | (when-not task 84 | (throw (Exception. (format "No task named %s." name)))) 85 | (doto task 86 | (.init) 87 | (.setProject project) 88 | (set-properties! props)) 89 | (doseq [n nested] 90 | (add-nested name task n) 91 | ) 92 | task)) 93 | 94 | (defn runonce 95 | "Create a function that will only run once. All other invocations 96 | return the first calculated value. The function *can* have side effects 97 | and calls to runonce *can* be composed. Deadlock is possible 98 | if you have circular dependencies. 99 | Returns a [has-run-predicate, reset-fn, once-fn]" 100 | [function] 101 | (let [sentinel (Object.) 102 | result (atom sentinel) 103 | reset-fn (fn [] (reset! result sentinel)) 104 | has-run-fn (fn [] (not= @result sentinel))] 105 | [has-run-fn 106 | reset-fn 107 | (fn [& args] 108 | (locking sentinel 109 | (if (= @result sentinel) 110 | (reset! result (function)) 111 | @result)))])) 112 | 113 | (defmacro has-run? [f] 114 | `((:has-run (meta (var ~f))))) 115 | 116 | (defmacro reset [f] 117 | `((:reset-fn (meta (var ~f))))) 118 | 119 | (def targets (atom #{})) 120 | 121 | (defmacro deftarget [sym doc & forms] 122 | (swap! targets #(conj % sym)) 123 | (let [has-run (gensym "hr-") reset-fn (gensym "rf-")] 124 | `(let [[~has-run ~reset-fn once-fn#] (runonce (fn [] ~@forms))] 125 | (def ~(with-meta sym {:doc doc :has-run has-run :reset-fn reset-fn}) 126 | once-fn#)))) 127 | 128 | (defmacro define-ant-task [clj-name ant-name] 129 | `(defn ~clj-name [& props#] 130 | (let [task# (apply instantiate-task ant-project ~(name ant-name) props#)] 131 | (.execute task#) 132 | task#))) 133 | 134 | (defmacro define-ant-type [clj-name ant-name] 135 | `(defn ~clj-name [props#] 136 | (let [bean# (new ~ant-name)] 137 | (set-properties! bean# props#) 138 | (when (property-descriptor bean# "project") 139 | (set-property! bean# "project" ant-project)) 140 | bean#))) 141 | 142 | (defn task-names [] (map symbol (seq (.. ant-project getTaskDefinitions keySet)))) 143 | 144 | (defn safe-ant-name [n] 145 | (if (ns-resolve 'clojure.core n) (symbol (str "ant-" n)) n)) 146 | 147 | (defmacro define-all-ant-tasks [] 148 | `(do ~@(map (fn [n] `(define-ant-task ~n ~n)) (task-names)))) 149 | 150 | (defmacro define-all-ant-tasks [] 151 | `(do ~@(map (fn [n] `(define-ant-task ~(safe-ant-name n) ~n)) (task-names)))) 152 | 153 | (define-all-ant-tasks) 154 | 155 | ;; Newer versions of ant don't have this class: 156 | ;; (define-ant-type files org.apache.tools.ant.types.resources.Files) 157 | (define-ant-type fileset org.apache.tools.ant.types.FileSet) 158 | 159 | (defn -main [& targs] 160 | (load-file "build.clj") 161 | (if targs 162 | (doseq [targ (map symbol targs)] 163 | (eval (list targ))) 164 | (println "Available targets: " @targets))) 165 | --------------------------------------------------------------------------------