PageRenderTime 60ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/leiningen-core/src/leiningen/core/eval.clj

https://github.com/cemerick/leiningen
Clojure | 243 lines | 194 code | 34 blank | 15 comment | 27 complexity | 962aec83496bb6ae1953111c57bbcb6d MD5 | raw file
  1. (ns leiningen.core.eval
  2. "Evaluate code inside the context of a project."
  3. (:require [classlojure.core :as cl]
  4. [clojure.java.io :as io]
  5. [clojure.string :as string]
  6. [cemerick.pomegranate :as pomegranate]
  7. [leiningen.core.user :as user]
  8. [leiningen.core.project :as project]
  9. [leiningen.core.main :as main]
  10. [leiningen.core.classpath :as classpath]))
  11. ;; # OS detection
  12. (defn- get-by-pattern
  13. "Gets a value from map m, but uses the keys as regex patterns, trying
  14. to match against k instead of doing an exact match."
  15. [m k]
  16. (m (first (drop-while #(nil? (re-find (re-pattern %) k))
  17. (keys m)))))
  18. (def ^:private native-names
  19. {"Mac OS X" :macosx "Windows" :windows "Linux" :linux
  20. "FreeBSD" :freebsd "OpenBSD" :openbsd
  21. "amd64" :x86_64 "x86_64" :x86_64 "x86" :x86 "i386" :x86
  22. "arm" :arm "SunOS" :solaris "sparc" :sparc "Darwin" :macosx})
  23. (def ^:private arch-options
  24. {:x86 ["-d32"] :x86_64 ["-d64"]})
  25. (defn get-os
  26. "Returns a keyword naming the host OS."
  27. []
  28. (get-by-pattern native-names (System/getProperty "os.name")))
  29. (defn get-arch
  30. "Returns a keyword naming the host architecture"
  31. []
  32. (get-by-pattern native-names (System/getProperty "os.arch")))
  33. (defn platform-nullsink []
  34. (io/file (if (= :windows (get-os))
  35. "NUL"
  36. "/dev/null")))
  37. ;; # Preparing for eval-in-project
  38. (defn run-prep-tasks
  39. "Execute all the prep-tasks. A task can either be a string, or a
  40. vector if it takes arguments. see :prep-tasks in sample.project.clj
  41. for examples"
  42. [{:keys [prep-tasks] :as project}]
  43. (doseq [task prep-tasks]
  44. (let [[task-name & task-args] (if (vector? task) task [task])
  45. task-name (main/lookup-alias task-name project)]
  46. (main/apply-task task-name (dissoc project :prep-tasks) task-args))))
  47. ;; Some tasks need to wait till the project is fully prepped before continuing.
  48. (defonce prep-blocker (atom (promise)))
  49. (defn prep [project]
  50. ;; This must exist before the project is launched.
  51. (.mkdirs (io/file (:compile-path project "/tmp")))
  52. (classpath/resolve-dependencies :dependencies project)
  53. (run-prep-tasks project)
  54. (.mkdirs (io/file (:compile-path project "/tmp")))
  55. (deliver @prep-blocker true)
  56. (reset! prep-blocker (promise)))
  57. ;; # Subprocess stuff
  58. (defn native-arch-path
  59. "Path to the os/arch-specific directory containing native libs."
  60. [project]
  61. (let [os (:os project (get-os))
  62. arch (:arch project (get-arch))]
  63. (if (and os arch)
  64. (io/file (:native-path project) (name os) (name arch)))))
  65. (defn- as-str [x]
  66. (if (instance? clojure.lang.Named x)
  67. (name x)
  68. (str x)))
  69. (defn- d-property [[k v]]
  70. (format "-D%s=%s" (as-str k) v))
  71. ;; TODO: this would still screw up with something like this:
  72. ;; export JAVA_OPTS="-Dmain.greeting=\"hello -main\" -Xmx512m"
  73. (defn- join-broken-arg [args x]
  74. (if (= \- (first x))
  75. (conj args x)
  76. (conj (vec (butlast args))
  77. (str (last args) " " x))))
  78. (defn ^{:internal true} get-jvm-opts-from-env [env-opts]
  79. (and (seq env-opts)
  80. (reduce join-broken-arg [] (.split env-opts " "))))
  81. (defn- get-jvm-args
  82. "Calculate command-line arguments for launching java subprocess."
  83. [project]
  84. (let [native-arch-path (native-arch-path project)]
  85. `(~@(get-jvm-opts-from-env (System/getenv "JVM_OPTS"))
  86. ~@(:jvm-opts project)
  87. ~@(get arch-options (:arch project))
  88. ~@(map d-property {:clojure.compile.path (:compile-path project)
  89. (str (:name project) ".version") (:version project)
  90. :file.encoding (or (System/getProperty "file.encoding") "UTF-8")
  91. :clojure.debug (boolean (or (System/getenv "DEBUG")
  92. (:debug project)))})
  93. ~@(if (and native-arch-path (.exists native-arch-path))
  94. [(d-property [:java.library.path native-arch-path])])
  95. ~@(if-let [{:keys [host port non-proxy-hosts]} (classpath/get-proxy-settings)]
  96. [(d-property [:http.proxyHost host])
  97. (d-property [:http.proxyPort port])
  98. (d-property [:http.nonProxyHosts non-proxy-hosts])]))))
  99. (defn- pump [reader out]
  100. (let [buffer (make-array Character/TYPE 1000)]
  101. (loop [len (.read reader buffer)]
  102. (when-not (neg? len)
  103. (.write out buffer 0 len)
  104. (.flush out)
  105. (Thread/sleep 100)
  106. (recur (.read reader buffer))))))
  107. (def ^:dynamic *dir* (System/getProperty "user.dir"))
  108. (def ^:dynamic *env* nil)
  109. (defn- overridden-env
  110. "Returns an overridden version of the current environment as an Array of
  111. Strings of the form name=val, suitable for passing to Runtime#exec."
  112. [env]
  113. (->> (merge {} (System/getenv) env)
  114. (filter val)
  115. (map #(str (name (key %)) "=" (val %)))
  116. (into-array String)))
  117. (defn sh
  118. "A version of clojure.java.shell/sh that streams out/err."
  119. [& cmd]
  120. (let [env (overridden-env *env*)
  121. proc (.exec (Runtime/getRuntime) (into-array cmd) env (io/file *dir*))]
  122. (.addShutdownHook (Runtime/getRuntime)
  123. (Thread. (fn [] (.destroy proc))))
  124. (with-open [out (io/reader (.getInputStream proc))
  125. err (io/reader (.getErrorStream proc))]
  126. (let [pump-out (doto (Thread. (bound-fn [] (pump out *out*))) .start)
  127. pump-err (doto (Thread. (bound-fn [] (pump err *err*))) .start)]
  128. (.join pump-out)
  129. (.join pump-err))
  130. (.waitFor proc))))
  131. ;; work around java's command line handling on windows
  132. ;; http://bit.ly/9c6biv This isn't perfect, but works for what's
  133. ;; currently being passed; see http://www.perlmonks.org/?node_id=300286
  134. ;; for some of the landmines involved in doing it properly
  135. (defn- form-string [form eval-in]
  136. (if (and (= (get-os) :windows) (not= :trampoline eval-in))
  137. (pr-str (pr-str form))
  138. (pr-str form)))
  139. (defn- classpath-arg [project]
  140. (if (:bootclasspath project)
  141. [(apply str "-Xbootclasspath/a:"
  142. (interpose java.io.File/pathSeparatorChar
  143. (classpath/get-classpath project)))]
  144. ["-classpath" (string/join java.io.File/pathSeparatorChar
  145. (classpath/get-classpath project))]))
  146. (defn shell-command [project form]
  147. `(~(or (:java-cmd project) (System/getenv "JAVA_CMD") "java")
  148. ~@(classpath-arg project)
  149. ~@(get-jvm-args project)
  150. "clojure.main" "-e" ~(form-string form (:eval-in project))))
  151. ;; # eval-in multimethod
  152. (defmulti eval-in
  153. "Evaluate the given from in either a subprocess or the leiningen process."
  154. ;; Force it to be a keyword so that we can accept symbols too. That
  155. ;; way ^:replace and ^:displace metadata can be applied.
  156. (fn [project _] (keyword (name (:eval-in project :subprocess)))))
  157. (defmethod eval-in :subprocess [project form]
  158. (binding [*dir* (:root project)]
  159. (let [exit-code (apply sh (shell-command project form))]
  160. (when (pos? exit-code)
  161. (throw (ex-info "Subprocess failed" {:exit-code exit-code}))))))
  162. (defonce trampoline-forms (atom []))
  163. (defonce trampoline-deps (atom []))
  164. (defmethod eval-in :trampoline [project form]
  165. (swap! trampoline-forms conj form)
  166. (swap! trampoline-deps conj (:dependencies project)))
  167. (defmethod eval-in :classloader [project form]
  168. (when-let [classpath (map io/file (classpath/ext-classpath project))]
  169. (cl/wrap-ext-classloader classpath))
  170. (let [classpath (map io/file (classpath/get-classpath project))
  171. classloader (cl/classlojure classpath)]
  172. (doseq [opt (get-jvm-args project)
  173. :when (.startsWith opt "-D")
  174. :let [[_ k v] (re-find #"^-D(.*?)=(.*)$" opt)]]
  175. (if (= k "java.library.path")
  176. (cl/alter-java-library-path!
  177. (constantly (string/split v (re-pattern java.io.File/pathSeparator))))
  178. (System/setProperty k v)))
  179. (try (cl/eval-in classloader form)
  180. (catch Exception e
  181. (println (str "Error evaluating in classloader: "
  182. (class e) ":" (.getMessage e)))
  183. (.printStackTrace e)
  184. (throw (ex-info "Classloader eval failed" {:exit-code 1}))))))
  185. (defmethod eval-in :leiningen [project form]
  186. (when (:debug project)
  187. (System/setProperty "clojure.debug" "true"))
  188. ;; :dependencies are loaded the same way as plugins in eval-in-leiningen
  189. (project/load-plugins project :dependencies)
  190. (doseq [path (classpath/get-classpath project)]
  191. (pomegranate/add-classpath path))
  192. (doseq [opt (get-jvm-args project)
  193. :when (.startsWith opt "-D")
  194. :let [[_ k v] (re-find #"^-D(.*?)=(.*)$" opt)]]
  195. (System/setProperty k v))
  196. (eval form))
  197. (defn eval-in-project
  198. "Executes form in isolation with the classpath and compile path set correctly
  199. for the project. If the form depends on any requires, put them in the init arg
  200. to avoid the Gilardi Scenario: http://technomancy.us/143"
  201. ([project form init]
  202. (prep project)
  203. (eval-in project
  204. `(do ~init
  205. ~@(:injections project)
  206. (set! ~'*warn-on-reflection*
  207. ~(:warn-on-reflection project))
  208. ~form)))
  209. ([project form] (eval-in-project project form nil)))