Permalink
Browse files

Run compile and test tasks in isolated classloader

This means that we can:
- Safely compile against alternative versions of Clojure.
- Move classpath logic from the shell script to lein itself.
- Run lein with: java -jar leiningen-standalone.jar

This patch also makes the source, test and library paths configurable
which is useful for people with special requirements like mixed-language
projects.
  • Loading branch information...
1 parent b9a0187 commit fb95c5ef57a554081c012df139b2751344cc970a @ato ato committed Nov 30, 2009
Showing with 118 additions and 62 deletions.
  1. +8 −15 bin/lein
  2. +2 −1 project.clj
  3. +62 −14 src/leiningen/compile.clj
  4. +11 −5 src/leiningen/core.clj
  5. +2 −2 src/leiningen/deps.clj
  6. +2 −2 src/leiningen/jar.clj
  7. +29 −21 src/leiningen/test.clj
  8. +1 −1 src/leiningen/uberjar.clj
  9. +1 −1 todo.org
View
@@ -1,8 +1,6 @@
#!/bin/bash
VERSION="1.0.0-SNAPSHOT"
-LIBS="$(find -H lib/ -mindepth 2> /dev/null 1 -maxdepth 1 -print0 | tr \\0 \:)"
-CLASSPATH="src/:classes/:$LIBS"
LEIN_JAR=$HOME/.m2/repository/leiningen/leiningen/$VERSION/leiningen-$VERSION-standalone.jar
# normalize $0 on certain BSDs
@@ -29,7 +27,7 @@ if [ -r "$BIN_DIR/../src/leiningen/core.clj" ]; then
# Running from source checkout
LEIN_DIR="$(dirname "$BIN_DIR")"
LEIN_LIBS="$(find -H $LEIN_DIR/lib/ -mindepth 2> /dev/null 1 -maxdepth 1 -print0 | tr \\0 \:)"
- CLASSPATH="$LEIN_DIR/src:$LEIN_LIBS:$CLASSPATH"
+ CLASSPATH="$LEIN_DIR/src:$LEIN_LIBS"
if [ "$LEIN_LIBS" = "" -a "$1" != "self-install" ]; then
echo "Your Leiningen development checkout is missing its dependencies."
@@ -39,28 +37,23 @@ if [ -r "$BIN_DIR/../src/leiningen/core.clj" ]; then
fi
else
# Not running from a checkout
- CLASSPATH="$CLASSPATH:$LEIN_JAR"
+ CLASSPATH="$LEIN_JAR"
if [ ! -r "$LEIN_JAR" -a "$1" != "self-install" ]; then
echo "Leiningen is not installed. Please run \"lein self-install\"."
exit 1
fi
fi
-if [ "$1" = "test" ]; then
- CLASSPATH=test/:$CLASSPATH
-fi
-
if [ $DEBUG ]; then
echo $CLASSPATH
fi
-# Deps need to run before the JVM launches for tasks that need them
-if [ "$1" = "compile" -o "$1" = "jar" -o "$1" = "uberjar" ]; then
- if [ ! "$(ls -A lib/*jar 2> /dev/null)" ]; then
- $0 deps skip-dev
- fi
-fi
+# escape command-line arguments so they can be evaled as strings
+ESCAPED_ARGS=""
+for ARG in "$@"; do
+ ESCAPED_ARGS="$ESCAPED_ARGS"' "'$(echo $ARG | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')'"'
+done
if [ "$1" = "repl" ]; then
# TODO: use rlwrap if present
@@ -77,5 +70,5 @@ elif [ "$1" = "self-install" ]; then
elif [ -z "$1" ]; then
echo "Usage: `basename $0` taskname. Run `basename $0` help for a list of tasks."
else
- exec java -client -cp "$CLASSPATH" clojure.main -e "(use 'leiningen.core)(main \"$*\")"
+ exec java -client -cp "$CLASSPATH" clojure.main -e "(use 'leiningen.core)(-main $ESCAPED_ARGS)"
fi
View
@@ -8,4 +8,5 @@
[org.clojure/clojure-contrib "1.0-SNAPSHOT"]
[ant/ant-launcher "1.6.2"]
[org.apache.maven/maven-ant-tasks "2.0.10"]]
- :dev-dependencies [[leiningen/lein-swank "1.0.0-SNAPSHOT"]])
+ :dev-dependencies [[leiningen/lein-swank "1.0.0-SNAPSHOT"]]
+ :main leiningen.core)
@@ -1,21 +1,69 @@
(ns leiningen.compile
"Compile the namespaces listed in project.clj or all namespaces in src."
+ (:require lancet)
(:use [clojure.contrib.java-utils :only [file]]
- [clojure.contrib.find-namespaces :only [find-namespaces-in-dir]])
- (:refer-clojure :exclude [compile]))
+ [clojure.contrib.find-namespaces :only [find-namespaces-in-dir]]
+ [leiningen.deps :only [deps]])
+ (:refer-clojure :exclude [compile])
+ (:import org.apache.tools.ant.taskdefs.Java
+ (org.apache.tools.ant.types Environment$Variable Path)))
+
+(defn namespaces-to-compile
+ "Returns a seq of the namespaces which need compiling."
+ [project]
+ (for [n (or (:namespaces project)
+ (find-namespaces-in-dir (file (:source-path project))))
+ :let [ns-file (str (-> (name n)
+ (.replaceAll "\\." "/")
+ (.replaceAll "-" "_")))]
+ :when (> (.lastModified (file (:source-path project)
+ (str ns-file ".clj")))
+ (.lastModified (file (:compile-path project)
+ (str ns-file "__init.class"))))]
+ n))
+
+(defn find-lib-jars
+ "Returns a seq of Files for all the jars in the project's library directory."
+ [project]
+ (filter #(.endsWith (.getName %) ".jar")
+ (file-seq (file (:library-path project)))))
+
+(defn make-path
+ "Constructs an ant Path object from Files and strings."
+ [& paths]
+ (let [ant-path (Path. nil)]
+ (doseq [path paths]
+ (.addExisting ant-path (Path. nil (str path))))
+ ant-path))
+
+(defn eval-in-project
+ "Executes form in an isolated classloader with the classpath and compile path
+ set correctly for the project."
+ [project form]
+ (let [java (Java.)]
+ (.setProject java lancet/ant-project)
+ (.addSysproperty java (doto (Environment$Variable.)
+ (.setKey "clojure.compile.path")
+ (.setValue (:compile-path project))))
+ (.setClasspath java (apply make-path
+ (:source-path project)
+ (:test-path project)
+ (:compile-path project)
+ (find-lib-jars project)))
+ (.setClassname java "clojure.main")
+ (.setValue (.createArg java) "-e")
+ (.setValue (.createArg java) (prn-str form))
+ (.execute java)))
(defn compile
"Ahead-of-time compile the project. Looks for all namespaces under src/
-unless a list of :namespaces is provided in project.clj."
+ unless a list of :namespaces is provided in project.clj."
[project]
- ;; TODO: use a java subprocess in case a different clojure version is needed
- (doseq [n (or (:namespaces project)
- (find-namespaces-in-dir (file (:root project) "src")))]
- (let [ns-file (str (-> (name n)
- (.replaceAll "\\." "/")
- (.replaceAll "-" "_")))]
- (when (> (.lastModified (file (:root project) "src" (str ns-file ".clj")))
- (.lastModified (file (:root project) "classes"
- (str ns-file "__init.class"))))
- (println "Compiling" n)
- (clojure.core/compile n)))))
+ (deps project)
+ (.mkdir (file (:compile-path project)))
+ (let [namespaces (namespaces-to-compile project)]
+ (when (seq namespaces)
+ (eval-in-project project
+ `(doseq [namespace# '~namespaces]
+ (println "Compiling" namespace#)
+ (clojure.core/compile namespace#))))))
@@ -1,6 +1,7 @@
(ns leiningen.core
(:use [clojure.contrib.with-ns])
- (:import [java.io File]))
+ (:import [java.io File])
+ (:gen-class))
(def project nil)
@@ -18,7 +19,13 @@
(name project-name))
:version ~version
:compile-path (or (:compile-path m#)
- (str root# "/classes/"))
+ (str root# "/classes"))
+ :source-path (or (:source-path m#)
+ (str root# "/src"))
+ :library-path (or (:library-path m#)
+ (str root# "/lib"))
+ :test-path (or (:test-path m#)
+ (str root# "/test"))
:root root#))))
(def ~(symbol (name project-name)) project)))
@@ -48,9 +55,8 @@
(catch java.io.FileNotFoundException e
(partial task-not-found task)))))
-(defn main [args-string]
- (let [[task & args] (.split args-string " ")
- task (or (aliases task) task)
+(defn -main [& [task & args]]
+ (let [task (or (aliases task) task "help")
project (if (no-project-needed task)
(first args)
(read-project))
@@ -41,7 +41,7 @@ following:
(doseq [dep (:dev-dependencies project)]
(.addDependency deps-task (make-dependency dep))))
(.execute deps-task)
- (.mkdirs (file (:root project) "lib"))
- (lancet/copy {:todir (str (:root project) "/lib/") :flatten "on"}
+ (.mkdirs (file (:library-path project)))
+ (lancet/copy {:todir (:library-path project) :flatten "on"}
(.getReference lancet/ant-project
(.getFilesetId deps-task)))))
@@ -63,8 +63,8 @@ as the main-class for an executable jar."
(:group project)
(:name project))
:bytes (make-pom-properties project)}
- {:type :path :path *compile-path*}
- {:type :path :path (str (:root project) "/src")}
+ {:type :path :path (:compile-path project)}
+ {:type :path :path (:source-path project)}
{:type :path :path (str (:root project) "/project.clj")}]]
;; TODO: support slim, etc
(write-jar project jar-file filespecs)
@@ -3,32 +3,40 @@
(:refer-clojure :exclude [test])
(:use [clojure.test]
[clojure.contrib.java-utils :only [file]]
- [clojure.contrib.find-namespaces :only [find-namespaces-in-dir]]))
+ [clojure.contrib.find-namespaces :only [find-namespaces-in-dir]]
+ [leiningen.compile :only [eval-in-project]]))
-(let [orig-report report
- aggregates (ref [])]
- (defn lein-report [event]
- (when (= (:type event) :summary)
- (dosync (commute aggregates conj event)))
- (orig-report event))
+(def report-fns
+ '(let [orig-report report
+ aggregates (ref [])]
+ (defn lein-report [event]
+ (when (= (:type event) :summary)
+ (dosync (commute aggregates conj event)))
+ (orig-report event))
- (defn super-summary []
- (with-test-out
- (println "\n\n--------------------\nTotal:"))
- (orig-report (apply merge-with (fn [a b]
- (if (number? a)
- (+ a b)
- a))
- @aggregates))))
+ (defn super-summary []
+ (with-test-out
+ (println "\n\n--------------------\nTotal:"))
+ (orig-report (apply merge-with (fn [a b]
+ (if (number? a)
+ (+ a b)
+ a))
+ @aggregates)))))
(defn test
"Run the project's tests. Accept a list of namespaces for which to run all
tests for. If none are given, runs them all."
[project & namespaces]
;; TODO: System/exit appropriately (depends on Clojure ticket #193)
- (doseq [n (if (empty? namespaces)
- (find-namespaces-in-dir (file (:root project) "test"))
- (map symbol namespaces))]
- (require n)
- (binding [report lein-report]
- (run-tests n))))
+ (let [namespaces (if (empty? namespaces)
+ (find-namespaces-in-dir (file (:root project) "test"))
+ (map symbol namespaces))]
+ (eval-in-project
+ project
+ `(do
+ (use ~''clojure.test)
+ ~report-fns
+ (doseq [ns# '~namespaces]
+ (require ns#)
+ (binding [report ~'lein-report]
+ (run-tests ns#)))))))
@@ -48,7 +48,7 @@
(str (:name project) "-standalone.jar"))
(FileOutputStream.) (ZipOutputStream.))]
;; TODO: any way to make sure we skip dev dependencies?
- (let [deps (->> (file-seq (file (:root project) "lib"))
+ (let [deps (->> (file-seq (file (:library-path project)))
(filter #(.endsWith (.getName %) ".jar"))
(cons (file (:root project) (str (:name project) ".jar"))))
[_ components] (reduce (partial include-dep out)
View
@@ -10,7 +10,7 @@ Leiningen TODOs
** TODO Use -Xbootclasspath where possible :Dan:
** DONE Don't write manifest, pom, etc. to disk when jarring :Dan:
** DONE Don't put uberjar in ~/.m2 :Phil:
-** TODO Perform compilation in either a subprocess or with a separate classloader
+** DONE Perform compilation in either a subprocess or with a separate classloader
** DONE Allow test task to take namespaces as an argument
** TODO System/exit appropriately when testing based on pass/fail
* Post 1.0

3 comments on commit fb95c5e

@wilkes
Contributor
wilkes commented on fb95c5e Dec 2, 2009

Doesn't this commit break all custom tasks since they are no longer on the classpath at launch?

@ato
Collaborator
ato commented on fb95c5e Dec 2, 2009

You are of course completely correct. Thanks for pointing this out.

@technomancy
Owner

Wups! A fix is on the way.

Please sign in to comment.