Skip to content

Commit

Permalink
Run compile and test tasks in isolated classloader
Browse files Browse the repository at this point in the history
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
ato committed Nov 30, 2009
1 parent b9a0187 commit fb95c5e
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 62 deletions.
23 changes: 8 additions & 15 deletions bin/lein
@@ -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
Expand All @@ -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."
Expand All @@ -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
Expand All @@ -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
3 changes: 2 additions & 1 deletion project.clj
Expand Up @@ -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)
76 changes: 62 additions & 14 deletions src/leiningen/compile.clj
@@ -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#))))))
16 changes: 11 additions & 5 deletions src/leiningen/core.clj
@@ -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)

Expand All @@ -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)))

Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions src/leiningen/deps.clj
Expand Up @@ -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)))))
4 changes: 2 additions & 2 deletions src/leiningen/jar.clj
Expand Up @@ -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)
Expand Down
50 changes: 29 additions & 21 deletions src/leiningen/test.clj
Expand Up @@ -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#)))))))
2 changes: 1 addition & 1 deletion src/leiningen/uberjar.clj
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion todo.org
Expand Up @@ -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
Expand Down

0 comments on commit fb95c5e

Please sign in to comment.