Add a simple MD5-based cache to DalvikRunner.

This makes the new simpler scheme pretty much free.

I've also made a start on simplifying the make rules, though there's more we
can do, and I'll look at that next.
diff --git a/libcore/tools/runner/Android.mk b/libcore/tools/runner/Android.mk
index bc7c5b7..2fb3591 100644
--- a/libcore/tools/runner/Android.mk
+++ b/libcore/tools/runner/Android.mk
@@ -2,15 +2,7 @@
 
 include $(CLEAR_VARS)
 
-ext_dirs := \
-        ../../../../external/jsr305/ri/src/main/java \
-        ../../../../external/guava/src \
-        ../../../../external/caliper/src
-
-ext_src_files := $(call all-java-files-under,$(ext_dirs))
-
 LOCAL_SRC_FILES := \
-        $(ext_src_files) \
         java/dalvik/runner/Aapt.java \
         java/dalvik/runner/Adb.java \
         java/dalvik/runner/ActivityMode.java \
@@ -36,6 +28,7 @@
         java/dalvik/runner/JtregRunner.java \
         java/dalvik/runner/MainFinder.java \
         java/dalvik/runner/MainRunner.java \
+        java/dalvik/runner/Md5Cache.java \
         java/dalvik/runner/Mkdir.java \
         java/dalvik/runner/Mode.java \
         java/dalvik/runner/NamingPatternCodeFinder.java \
@@ -53,7 +46,7 @@
         java/dalvik/runner/XmlReportPrinter.java \
 
 LOCAL_MODULE:= dalvik_runner
-LOCAL_STATIC_JAVA_LIBRARIES := javatest jh jtreg kxml2-2.3.0
+LOCAL_STATIC_JAVA_LIBRARIES := jsr305 guava caliper javatest jh jtreg kxml2-2.3.0
 
 # TODO this only works when junit is already built...
 LOCAL_JAVA_LIBRARIES := junit
@@ -64,6 +57,23 @@
 
 include $(call all-subdir-makefiles)
 
+# prebuilt jsr305.jar
+# TODO: do we need this any more? caliper.jar has a jarjar'ed copy.
+include $(CLEAR_VARS)
+LOCAL_PREBUILT_JAVA_LIBRARIES := jsr305:lib/jsr305.jar
+include $(BUILD_HOST_PREBUILT)
+
+# prebuilt guava.jar
+# TODO: do we need this any more? caliper.jar has a jarjar'ed copy.
+include $(CLEAR_VARS)
+LOCAL_PREBUILT_JAVA_LIBRARIES := guava:lib/guava.jar
+include $(BUILD_HOST_PREBUILT)
+
+# prebuilt caliper.jar
+include $(CLEAR_VARS)
+LOCAL_PREBUILT_JAVA_LIBRARIES := caliper:lib/caliper.jar
+include $(BUILD_HOST_PREBUILT)
+
 # prebuilt javatest.jar
 include $(CLEAR_VARS)
 LOCAL_PREBUILT_JAVA_LIBRARIES := javatest:lib/javatest.jar
diff --git a/libcore/tools/runner/java/dalvik/runner/Dx.java b/libcore/tools/runner/java/dalvik/runner/Dx.java
index 1190bce..393b70d 100644
--- a/libcore/tools/runner/java/dalvik/runner/Dx.java
+++ b/libcore/tools/runner/java/dalvik/runner/Dx.java
@@ -17,13 +17,25 @@
 package dalvik.runner;
 
 import java.io.File;
+import java.util.logging.Logger;
 
 /**
  * A dx command.
  */
 final class Dx {
+    private static final Logger logger = Logger.getLogger(Dx.class.getName());
+    private static final Md5Cache DEX_CACHE = new Md5Cache("dex");
 
+    /**
+     * Converts all the .class files on 'classpath' into a dex file written to 'output'.
+     */
     public void dex(File output, Classpath classpath) {
+        File key = DEX_CACHE.makeKey(classpath);
+        if (key != null && key.exists()) {
+            logger.fine("dex cache hit for " + classpath);
+            new Command.Builder().args("cp", key, output).execute();
+            return;
+        }
         /*
          * We pass --core-library so that we can write tests in the
          * same package they're testing, even when that's a core
@@ -44,5 +56,6 @@
                 .args("--core-library")
                 .args(Strings.objectsToStrings(classpath.getElements()))
                 .execute();
+        DEX_CACHE.insert(key, output);
     }
 }
diff --git a/libcore/tools/runner/java/dalvik/runner/Md5Cache.java b/libcore/tools/runner/java/dalvik/runner/Md5Cache.java
new file mode 100644
index 0000000..f6ba85d
--- /dev/null
+++ b/libcore/tools/runner/java/dalvik/runner/Md5Cache.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.runner;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.security.MessageDigest;
+import java.util.logging.Logger;
+
+/**
+ * Caches content by MD5.
+ */
+public final class Md5Cache {
+    private static final Logger logger = Logger.getLogger(Md5Cache.class.getName());
+    private static final File CACHE_ROOT = new File("/tmp/vogar-md5-cache/");
+
+    private final String keyPrefix;
+
+    /**
+     * Creates a new cache accessor. There's only one directory on disk, so 'keyPrefix' is really
+     * just a convenience for humans inspecting the cache.
+     */
+    public Md5Cache(String keyPrefix) {
+        this.keyPrefix = keyPrefix;
+    }
+
+    /**
+     * Returns an ASCII hex representation of the MD5 of the content of 'file'.
+     */
+    private static String md5(File file) {
+        byte[] digest = null;
+        try {
+            MessageDigest digester = MessageDigest.getInstance("MD5");
+            byte[] bytes = new byte[8192];
+            FileInputStream in = new FileInputStream(file);
+            try {
+                int byteCount;
+                while ((byteCount = in.read(bytes)) > 0) {
+                    digester.update(bytes, 0, byteCount);
+                }
+                digest = digester.digest();
+            } finally {
+                in.close();
+            }
+        } catch (Exception cause) {
+            throw new RuntimeException("Unable to compute MD5 of \"" + file + "\"", cause);
+        }
+        return (digest == null) ? null : byteArrayToHexString(digest);
+    }
+
+    private static String byteArrayToHexString(byte[] bytes) {
+        StringBuilder result = new StringBuilder();
+        for (byte b : bytes) {
+            result.append(Integer.toHexString((b >> 4) & 0xf));
+            result.append(Integer.toHexString(b & 0xf));
+        }
+        return result.toString();
+    }
+
+    /**
+     * Returns the appropriate key for a dex file corresponding to the contents of 'classpath'.
+     * Returns null if we don't think it's possible to cache the given classpath.
+     */
+    public File makeKey(Classpath classpath) {
+        // Do we have it in cache?
+        String key = keyPrefix;
+        for (File element : classpath.getElements()) {
+            // We only cache dexed .jar files, not directories.
+            if (!element.toString().endsWith(".jar")) {
+                return null;
+            }
+            key += "-" + md5(element);
+        }
+        return new File(CACHE_ROOT, key);
+    }
+
+    /**
+     * Copy the file 'content' into the cache with the given 'key'.
+     * This method assumes you're using the appropriate key for the content (and has no way to
+     * check because the key is a function of the inputs that made the content, not the content
+     * itself).
+     * We accept a null so the caller doesn't have to pay attention to whether we think we can
+     * cache the content or not.
+     */
+    public void insert(File key, File content) {
+        if (key == null) {
+            return;
+        }
+        logger.fine("inserting " + key);
+        if (!key.toString().startsWith(CACHE_ROOT.toString())) {
+            throw new IllegalArgumentException("key '" + key + "' not a valid cache key");
+        }
+        // Make sure the cache exists first.
+        new Mkdir().mkdirs(CACHE_ROOT);
+        // Copy it onto the same file system first, then atomically move it into place.
+        // That way, if we fail, we don't leave anything dangerous lying around.
+        File temporary = new File(key + ".tmp");
+        new Command.Builder().args("cp", content, temporary).execute();
+        new Command.Builder().args("mv", temporary, key).execute();
+    }
+}