Clone of chromium aad1ce808763f59c7a3753e08f1500a104ecc6fd refs/remotes/origin/HEAD
diff --git a/mojo/tools/BUILD.gn b/mojo/tools/BUILD.gn
new file mode 100644
index 0000000..de38e6c
--- /dev/null
+++ b/mojo/tools/BUILD.gn
@@ -0,0 +1,23 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# GYP version: mojo/mojo_base.gyp:mojo_message_generator
+executable("message_generator") {
+  testonly = true
+  output_name = "mojo_message_generator"
+
+  sources = [
+    "message_generator.cc",
+  ]
+
+  deps = [
+    "//base",
+    "//mojo/common",
+    "//mojo/edk/system",
+    "//mojo/environment:chromium",
+    "//mojo/public/cpp/bindings",
+    "//testing/gtest",
+  ]
+}
+
diff --git a/mojo/tools/check_mojom_golden_files.py b/mojo/tools/check_mojom_golden_files.py
new file mode 100755
index 0000000..9af7a86
--- /dev/null
+++ b/mojo/tools/check_mojom_golden_files.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import os.path
+import sys
+from filecmp import dircmp
+from shutil import rmtree
+from tempfile import mkdtemp
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+_mojo_dir = os.path.join(_script_dir, os.pardir)
+_chromium_src_dir = os.path.join(_mojo_dir, os.pardir)
+sys.path.insert(0, os.path.join(_mojo_dir, "public", "tools", "bindings",
+                                "pylib"))
+from mojom_tests.support.find_files import FindFiles
+from mojom_tests.support.run_bindings_generator import RunBindingsGenerator
+
+
+def _ProcessDircmpResults(results, verbose=False):
+  """Prints results of directory comparison and returns true if they are
+  identical (note: the "left" directory should be the golden directory)."""
+  rv = not (bool(results.left_only) or bool(results.right_only) or \
+            bool(results.common_funny) or bool(results.funny_files) or \
+            bool(results.diff_files))
+  if verbose:
+    for f in results.left_only:
+      print "%s exists in golden directory but not in current output" % f
+    for f in results.right_only:
+      print "%s exists in current output but not in golden directory" % f
+    for f in results.common_funny + results.funny_files:
+      print "Unable to compare %s between golden directory and current output" \
+          % f
+    for f in results.diff_files:
+      print "%s differs between golden directory and current output" % f
+  for r in results.subdirs.values():
+    # If we're being verbose, check subdirectories even if we know that there
+    # are differences. Note that it's "... and rv" to avoid the short-circuit.
+    if rv or verbose:
+      rv = _ProcessDircmpResults(r, verbose=verbose) and rv
+  return rv
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument("--generate_golden_files", action="store_true",
+                      help=("generate golden files (does not obliterate "
+                            "directory"))
+  parser.add_argument("--keep_temp_dir", action="store_true",
+                      help="don't delete the temporary directory")
+  parser.add_argument("--verbose", action="store_true",
+                      help="spew excess verbiage")
+  parser.add_argument("golden_dir", metavar="GOLDEN_DIR",
+                      help="directory with the golden files")
+  args = parser.parse_args()
+
+  if args.generate_golden_files:
+    if os.path.exists(args.golden_dir):
+      print "WARNING: golden directory %s already exists" % args.golden_dir
+    out_dir = args.golden_dir
+  else:
+    if not os.path.exists(args.golden_dir):
+      print "ERROR: golden directory %s does not exist" % args.golden_dir
+      return 1
+    out_dir = mkdtemp()
+  if args.verbose:
+    print "Generating files to %s ..." % out_dir
+
+  mojom_files = FindFiles(_mojo_dir, "*.mojom")
+  for mojom_file in mojom_files:
+    if args.verbose:
+      print "  Processing %s ..." % os.path.relpath(mojom_file, _mojo_dir)
+    # TODO(vtl): This may wrong, since the path can be overridden in the .gyp
+    # file.
+    RunBindingsGenerator(out_dir, _mojo_dir, mojom_file,
+                         ["-I", os.path.abspath(_chromium_src_dir)])
+
+  if args.generate_golden_files:
+    return 0
+
+  identical = _ProcessDircmpResults(dircmp(args.golden_dir, out_dir, ignore=[]),
+                                    verbose=args.verbose)
+
+  if args.keep_temp_dir:
+    if args.verbose:
+      print "Not removing %s ..." % out_dir
+  else:
+    if args.verbose:
+      print "Removing %s ..." % out_dir
+    rmtree(out_dir)
+
+  if not identical:
+    print "FAILURE: current output differs from golden files"
+    return 1
+
+  print "SUCCESS: current output identical to golden files"
+  return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/mojo/tools/data/unittests b/mojo/tools/data/unittests
new file mode 100644
index 0000000..0bafba4
--- /dev/null
+++ b/mojo/tools/data/unittests
@@ -0,0 +1,27 @@
+# This file contains a list of Mojo gtest unit tests.
+# Prepend * to indicate that results shouldn't be cached (e.g., if the test has
+# other data dependencies).
+# TODO(vtl): Add a way of specifying data dependencies instead.
+
+# System tests:
+mojo_system_unittests
+
+# Public tests:
+*mojo_public_bindings_unittests
+mojo_public_environment_unittests
+mojo_public_system_unittests
+mojo_public_utility_unittests
+
+# Non-system, non-public tests:
+mojo_application_manager_unittests
+mojo_common_unittests
+mojo_view_manager_lib_unittests
+mojo_view_manager_unittests
+mojo_surfaces_lib_unittests
+
+# JavaScript tests:
+*mojo_apps_js_unittests
+*mojo_js_unittests
+
+# Shell integration tests:
+*mojo_shell_tests
diff --git a/mojo/tools/generate_java_callback_interfaces.py b/mojo/tools/generate_java_callback_interfaces.py
new file mode 100644
index 0000000..257a540
--- /dev/null
+++ b/mojo/tools/generate_java_callback_interfaces.py
@@ -0,0 +1,69 @@
+"""Generate the org.chromium.mojo.bindings.Callbacks interface"""
+
+import argparse
+import sys
+
+CALLBACK_TEMPLATE = ("""
+    /**
+     * A generic %d-argument callback.
+     *
+     * %s
+     */
+    interface Callback%d<%s> {
+        /**
+         * Call the callback.
+         */
+        public void call(%s);
+    }
+""")
+
+INTERFACE_TEMPLATE = (
+"""// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file was generated using
+//     mojo/tools/generate_java_callback_interfaces.py
+
+package org.chromium.mojo.bindings;
+
+/**
+ * Contains a generic interface for callbacks.
+ */
+public interface Callbacks {
+
+    /**
+     * A generic callback.
+     */
+    interface Callback0 {
+        /**
+         * Call the callback.
+         */
+        public void call();
+    }
+%s
+}""")
+
+def GenerateCallback(nb_args):
+  params = '\n      * '.join(
+      ['@param <T%d> the type of argument %d.' % (i+1, i+1)
+       for i in xrange(nb_args)])
+  template_parameters = ', '.join(['T%d' % (i+1) for i in xrange(nb_args)])
+  callback_parameters = ', '.join(['T%d arg%d' % ((i+1), (i+1))
+                                   for i in xrange(nb_args)])
+  return CALLBACK_TEMPLATE % (nb_args, params, nb_args, template_parameters,
+                              callback_parameters)
+
+def main():
+  parser = argparse.ArgumentParser(
+      description="Generate org.chromium.mojo.bindings.Callbacks")
+  parser.add_argument("max_args", nargs=1, type=int,
+      help="maximal number of arguments to generate callbacks for")
+  args = parser.parse_args()
+  max_args = args.max_args[0]
+  print INTERFACE_TEMPLATE % ''.join([GenerateCallback(i+1)
+                                      for i in xrange(max_args)])
+  return 0
+
+if __name__ == "__main__":
+  sys.exit(main())
diff --git a/mojo/tools/message_generator.cc b/mojo/tools/message_generator.cc
new file mode 100644
index 0000000..08b7dcc
--- /dev/null
+++ b/mojo/tools/message_generator.cc
@@ -0,0 +1,63 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "mojo/public/cpp/bindings/lib/message_builder.h"
+#include "mojo/public/cpp/bindings/lib/message_internal.h"
+#include "mojo/public/cpp/bindings/message.h"
+
+// This file is used to generate various files corresponding to mojo
+// messages. The various binding implementations can parse these to verify they
+// correctly decode messages.
+//
+// The output consists of each byte of the message encoded in a hex string with
+// a newline after it.
+namespace mojo {
+namespace {
+
+std::string BinaryToHex(const base::StringPiece& piece) {
+  std::string result("// File generated by mojo_message_generator.\n");;
+  result.reserve(result.size() + (piece.size() * 5));
+  for (size_t i = 0; i < piece.size(); ++i)
+    base::StringAppendF(&result, "0X%.2X\n", static_cast<int>(piece.data()[i]));
+  return result;
+}
+
+void WriteMessageToFile(const Message& message, const base::FilePath& path) {
+  const std::string hex_message(BinaryToHex(
+      base::StringPiece(reinterpret_cast<const char*>(message.data()),
+                        message.data_num_bytes())));
+  CHECK_EQ(static_cast<int>(hex_message.size()),
+           base::WriteFile(path, hex_message.data(),
+                           static_cast<int>(hex_message.size())));
+}
+
+// Generates a message of type MessageData. The message uses the name 21,
+// with 4 bytes of payload: 0x9, 0x8, 0x7, 0x6.
+void GenerateMessageDataMessage() {
+  internal::MessageBuilder builder(static_cast<uint32_t>(21),
+                                   static_cast<size_t>(4));
+  char* data = static_cast<char*>(builder.buffer()->Allocate(4));
+  DCHECK(data);
+  data[0] = 9;
+  data[1] = 8;
+  data[2] = 7;
+  data[3] = 6;
+
+  Message message;
+  builder.Finish(&message);
+  WriteMessageToFile(message,
+                     base::FilePath(FILE_PATH_LITERAL("message_data")));
+}
+
+}  // namespace
+}  // namespace mojo
+
+int main(int argc, char** argv) {
+  mojo::GenerateMessageDataMessage();
+  return 0;
+}
diff --git a/mojo/tools/mojob.sh b/mojo/tools/mojob.sh
new file mode 100755
index 0000000..e0804d1
--- /dev/null
+++ b/mojo/tools/mojob.sh
@@ -0,0 +1,238 @@
+#!/bin/bash
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This a simple script to make building/testing Mojo components easier (on
+# Linux).
+
+# TODO(vtl): Maybe make the test runner smart and not run unchanged test
+# binaries.
+# TODO(vtl) Maybe also provide a way to pass command-line arguments to the test
+# binaries.
+
+do_help() {
+  cat << EOF
+Usage: $(basename "$0") [command|option ...]
+
+command should be one of:
+  build - Build.
+  test - Run unit tests (does not build).
+  perftest - Run perf tests (does not build).
+  pytest - Run Python unit tests.
+  gyp - Run gyp for mojo (does not sync).
+  gypall - Run gyp for all of chromium (does not sync).
+  sync - Sync using gclient (does not run gyp).
+  show-bash-alias - Outputs an appropriate bash alias for mojob. In bash do:
+      \$ eval \`mojo/tools/mojob.sh show-bash-alias\`
+
+option (which will only apply to following commands) should be one of:
+  Build/test options (specified before build/test/perftest):
+    --debug - Build/test in Debug mode.
+    --release - Build/test in Release mode.
+    --debug-and-release - Build/test in both Debug and Release modes (default).
+  Compiler options (specified before gyp):
+    --clang - Use clang (default).
+    --gcc - Use gcc.
+  Component options:
+    --shared Build components as shared libraries (default).
+    --static Build components as static libraries.
+  Use goma:
+    --use-goma - Use goma if \$GOMA_DIR is set or \$HOME/goma exists (default).
+    --no-use-goma - Do not use goma.
+
+Note: It will abort on the first failure (if any).
+EOF
+}
+
+do_build() {
+  echo "Building in out/$1 ..."
+  if [ "$GOMA" = "auto" -a -v GOMA_DIR ]; then
+    ninja -j 1000 -l 100 -C "out/$1" mojo || exit 1
+  else
+    ninja -C "out/$1" mojo || exit 1
+  fi
+}
+
+do_unittests() {
+  echo "Running unit tests in out/$1 ..."
+  mojo/tools/test_runner.py mojo/tools/data/unittests "out/$1" \
+      mojob_test_successes || exit 1
+}
+
+do_perftests() {
+  echo "Running perf tests in out/$1 ..."
+  "out/$1/mojo_public_system_perftests" || exit 1
+}
+
+do_pytests() {
+  python mojo/tools/run_mojo_python_tests.py || exit 1
+}
+
+do_gyp() {
+  local gyp_defines="$(make_gyp_defines)"
+  echo "Running gyp for mojo with GYP_DEFINES=$gyp_defines ..."
+  GYP_DEFINES="$gyp_defines" build/gyp_chromium mojo/mojo.gyp || exit 1
+}
+
+do_gypall() {
+  local gyp_defines="$(make_gyp_defines)"
+  echo "Running gyp for everything with GYP_DEFINES=$gyp_defines ..."
+  GYP_DEFINES="$gyp_defines" build/gyp_chromium || exit 1
+}
+
+do_sync() {
+  # Note: sync only (with hooks, but no gyp-ing).
+  GYP_CHROMIUM_NO_ACTION=1 gclient sync || exit 1
+}
+
+# Valid values: Debug, Release, or Debug_and_Release.
+BUILD_TEST_TYPE=Debug_and_Release
+should_do_Debug() {
+  test "$BUILD_TEST_TYPE" = Debug -o "$BUILD_TEST_TYPE" = Debug_and_Release
+}
+should_do_Release() {
+  test "$BUILD_TEST_TYPE" = Release -o "$BUILD_TEST_TYPE" = Debug_and_Release
+}
+
+# Valid values: clang or gcc.
+COMPILER=clang
+# Valid values: shared or static.
+COMPONENT=shared
+# Valid values: auto or disabled.
+GOMA=auto
+make_gyp_defines() {
+  local options=()
+  # Always include these options.
+  options+=("use_aura=1")
+  case "$COMPILER" in
+    clang)
+      options+=("clang=1")
+      ;;
+    gcc)
+      options+=("clang=0")
+      ;;
+  esac
+  case "$COMPONENT" in
+    shared)
+      options+=("component=shared_library")
+      ;;
+    static)
+      options+=("component=static_library")
+      ;;
+  esac
+  case "$GOMA" in
+    auto)
+      if [ -v GOMA_DIR ]; then
+        options+=("use_goma=1" "gomadir=\"${GOMA_DIR}\"")
+      else
+        options+=("use_goma=0")
+      fi
+      ;;
+    disabled)
+      options+=("use_goma=0")
+      ;;
+  esac
+  echo "${options[*]}"
+}
+
+set_goma_dir_if_necessary() {
+  if [ "$GOMA" = "auto" -a ! -v GOMA_DIR ]; then
+    if [ -d "${HOME}/goma" ]; then
+      GOMA_DIR="${HOME}/goma"
+    fi
+  fi
+}
+
+start_goma_if_necessary() {
+  if [ "$GOMA" = "auto" -a -v GOMA_DIR ]; then
+    "${GOMA_DIR}/goma_ctl.py" ensure_start
+  fi
+}
+
+# We're in src/mojo/tools. We want to get to src.
+cd "$(realpath "$(dirname "$0")")/../.."
+
+if [ $# -eq 0 ]; then
+  do_help
+  exit 0
+fi
+
+for arg in "$@"; do
+  case "$arg" in
+    # Commands -----------------------------------------------------------------
+    help|--help)
+      do_help
+      exit 0
+      ;;
+    build)
+      set_goma_dir_if_necessary
+      start_goma_if_necessary
+      should_do_Debug && do_build Debug
+      should_do_Release && do_build Release
+      ;;
+    test)
+      should_do_Debug && do_unittests Debug
+      should_do_Release && do_unittests Release
+      ;;
+    perftest)
+      should_do_Debug && do_perftests Debug
+      should_do_Release && do_perftests Release
+      ;;
+    pytest)
+      do_pytests
+      ;;
+    gyp)
+      set_goma_dir_if_necessary
+      do_gyp
+      ;;
+    gypall)
+      set_goma_dir_if_necessary
+      do_gypall
+      ;;
+    sync)
+      do_sync
+      ;;
+    show-bash-alias)
+      # You want to type something like:
+      #   alias mojob=\
+      #       '"$(pwd | sed '"'"'s/\(.*\/src\).*/\1/'"'"')/mojo/tools/mojob.sh"'
+      # This is quoting hell, so we simply escape every non-alphanumeric
+      # character.
+      echo alias\ mojob\=\'\"\$\(pwd\ \|\ sed\ \'\"\'\"\'s\/\\\(\.\*\\\/src\\\)\
+\.\*\/\\1\/\'\"\'\"\'\)\/mojo\/tools\/mojob\.sh\"\'
+      ;;
+    # Options ------------------------------------------------------------------
+    --debug)
+      BUILD_TEST_TYPE=Debug
+      ;;
+    --release)
+      BUILD_TEST_TYPE=Release
+      ;;
+    --debug-and-release)
+      BUILD_TEST_TYPE=Debug_and_Release
+      ;;
+    --clang)
+      COMPILER=clang
+      ;;
+    --gcc)
+      COMPILER=gcc
+      ;;
+    --shared)
+      COMPONENT=shared
+      ;;
+    --static)
+      COMPONENT=static
+      ;;
+    --use-goma)
+      GOMA=auto
+      ;;
+    --no-use-goma)
+      GOMA=disabled
+      ;;
+    *)
+      echo "Unknown command \"${arg}\". Try \"$(basename "$0") help\"."
+      exit 1
+      ;;
+  esac
+done
diff --git a/mojo/tools/mojosh.sh b/mojo/tools/mojosh.sh
new file mode 100755
index 0000000..bb8a2cd
--- /dev/null
+++ b/mojo/tools/mojosh.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This a simple script to make running Mojo shell easier (on Linux).
+
+DIRECTORY="$(dirname "$0")"/../../out/Debug
+PORT=$(($RANDOM % 8192 + 2000))
+
+do_help() {
+  cat << EOF
+Usage: $(basename "$0") [-d DIRECTORY] [-|--] <mojo_shell arguments ...>
+
+DIRECTORY defaults to $DIRECTORY.
+
+Example:
+  $(basename "$0") mojo:mojo_sample_app
+EOF
+}
+
+kill_http_server() {
+  echo "Killing SimpleHTTPServer ..."
+  kill $HTTP_SERVER_PID
+  wait $HTTP_SERVER_PID
+}
+
+while [ $# -gt 0 ]; do
+  case "$1" in
+    -h|--help)
+      do_help
+      exit 0
+      ;;
+    -d)
+      shift
+      if [ $# -eq 0 ]; then
+        do_help
+        exit 1
+      fi
+      DIRECTORY="$1"
+      ;;
+    --show-bash-alias)
+      echo alias\ mojosh\=\'\"\$\(pwd\ \|\ sed\ \'\"\'\"\'s\/\\\(\.\*\\\/src\\\
+\)\.\*\/\\1\/\'\"\'\"\'\)\/mojo\/tools\/mojosh\.sh\"\'
+      exit 0
+      ;;
+    # Separate arguments to mojo_shell (e.g., in case you want to pass it -d).
+    -|--)
+      shift
+      break
+      ;;
+    *)
+      break
+      ;;
+  esac
+  shift
+done
+
+echo "Base directory: $DIRECTORY"
+
+echo "Running SimpleHTTPServer in directory $DIRECTORY/lib on port $PORT"
+cd $DIRECTORY/lib || exit 1
+python -m SimpleHTTPServer $PORT &
+# Kill the HTTP server on exit (even if the user kills everything using ^C).
+HTTP_SERVER_PID=$!
+trap kill_http_server EXIT
+cd ..
+
+echo "Running:"
+echo "./mojo_shell --origin=http://127.0.0.1:$PORT --disable-cache $*"
+./mojo_shell --origin=http://127.0.0.1:$PORT --disable-cache $*
diff --git a/mojo/tools/package_manager/BUILD.gn b/mojo/tools/package_manager/BUILD.gn
new file mode 100644
index 0000000..76b5771
--- /dev/null
+++ b/mojo/tools/package_manager/BUILD.gn
@@ -0,0 +1,29 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+shared_library("package_manager") {
+  output_name = "mojo_package_manager"
+
+  sources = [
+    "manifest.cc",
+    "manifest.h",
+    "package_fetcher.cc",
+    "package_fetcher.h",
+    "package_manager.cc",
+    "package_manager_application.cc",
+    "package_manager_application.h",
+    "unpacker.cc",
+    "unpacker.h",
+  ]
+
+  deps = [
+    "//base",
+    "//mojo/application",
+    "//mojo/public/c/system:for_shared_library",
+    "//mojo/public/cpp/utility",
+    "//mojo/services/public/interfaces/network",
+    "//third_party/zlib:zip",
+    "//url",
+  ]
+}
diff --git a/mojo/tools/package_manager/DEPS b/mojo/tools/package_manager/DEPS
new file mode 100644
index 0000000..784b7fb
--- /dev/null
+++ b/mojo/tools/package_manager/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+  '+third_party/zlib',
+]
diff --git a/mojo/tools/package_manager/manifest.cc b/mojo/tools/package_manager/manifest.cc
new file mode 100644
index 0000000..5a0e346
--- /dev/null
+++ b/mojo/tools/package_manager/manifest.cc
@@ -0,0 +1,83 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "mojo/tools/package_manager/manifest.h"
+
+#include "base/files/file_util.h"
+#include "base/json/json_reader.h"
+#include "base/values.h"
+#include "url/gurl.h"
+
+namespace mojo {
+
+Manifest::Manifest() {
+}
+
+Manifest::~Manifest() {
+}
+
+bool Manifest::Parse(const std::string& str, std::string* err_msg) {
+  int err_code = base::JSONReader::JSON_NO_ERROR;
+  scoped_ptr<base::Value> root(base::JSONReader::ReadAndReturnError(
+      str,
+      base::JSON_ALLOW_TRAILING_COMMAS,
+      &err_code, err_msg));
+  if (err_code != base::JSONReader::JSON_NO_ERROR)
+    return false;
+
+  const base::DictionaryValue* root_dict;
+  if (!root->GetAsDictionary(&root_dict)) {
+    *err_msg = "Manifest is not a dictionary.";
+    return false;
+  }
+
+  if (!PopulateDeps(root_dict, err_msg))
+    return false;
+
+  return true;
+}
+
+bool Manifest::ParseFromFile(const base::FilePath& file_name,
+                             std::string* err_msg) {
+  std::string data;
+  if (!base::ReadFileToString(file_name, &data)) {
+    *err_msg = "Couldn't read manifest file " + file_name.AsUTF8Unsafe();
+    return false;
+  }
+  return Parse(data, err_msg);
+}
+
+bool Manifest::PopulateDeps(const base::DictionaryValue* root,
+                            std::string* err_msg) {
+  const base::Value* deps_value;
+  if (!root->Get("deps", &deps_value))
+    return true;  // No deps, that's OK.
+
+  const base::ListValue* deps;
+  if (!deps_value->GetAsList(&deps)) {
+    *err_msg = "Deps is not a list. Should be \"deps\": [ \"...\", \"...\" ]";
+    return false;
+  }
+
+  deps_.reserve(deps->GetSize());
+  for (size_t i = 0; i < deps->GetSize(); i++) {
+    std::string cur_dep;
+    if (!deps->GetString(i, &cur_dep)) {
+      *err_msg = "Dependency list item wasn't a string.";
+      return false;
+    }
+
+    GURL cur_url(cur_dep);
+    if (!cur_url.is_valid()) {
+      *err_msg = "Dependency entry isn't a valid URL: " + cur_dep;
+      return false;
+    }
+
+    deps_.push_back(cur_url);
+  }
+
+  return true;
+}
+
+}  // namespace mojo
diff --git a/mojo/tools/package_manager/manifest.h b/mojo/tools/package_manager/manifest.h
new file mode 100644
index 0000000..f87040d
--- /dev/null
+++ b/mojo/tools/package_manager/manifest.h
@@ -0,0 +1,46 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MOJO_TOOLS_PACKAGE_MANAGER_MANIFEST_H_
+#define MOJO_TOOLS_PACKAGE_MANAGER_MANIFEST_H_
+
+#include <string>
+#include <vector>
+
+#include "mojo/public/cpp/system/macros.h"
+
+class GURL;
+
+namespace base {
+class DictionaryValue;
+class FilePath;
+}
+
+namespace mojo {
+
+class Manifest {
+ public:
+  Manifest();
+  ~Manifest();
+
+  // Parses the manifest from raw data. Returns true on success. On failure,
+  // populates the "err_msg" string with an error.
+  bool Parse(const std::string& str, std::string* err_msg);
+
+  // Like Parse but reads the data from a file.
+  bool ParseFromFile(const base::FilePath& file_name, std::string* err_msg);
+
+  const std::vector<GURL>& deps() const { return deps_; }
+
+ private:
+  bool PopulateDeps(const base::DictionaryValue* root, std::string* err_msg);
+
+  std::vector<GURL> deps_;
+
+  MOJO_DISALLOW_COPY_AND_ASSIGN(Manifest);
+};
+
+}  // namespace mojo
+
+#endif  // MOJO_TOOLS_PACKAGE_MANAGER_MANIFEST_H_
diff --git a/mojo/tools/package_manager/package_fetcher.cc b/mojo/tools/package_manager/package_fetcher.cc
new file mode 100644
index 0000000..b5a47e5
--- /dev/null
+++ b/mojo/tools/package_manager/package_fetcher.cc
@@ -0,0 +1,96 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "mojo/tools/package_manager/package_fetcher.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "mojo/services/public/interfaces/network/url_loader.mojom.h"
+
+namespace mojo {
+
+PackageFetcher::PackageFetcher(NetworkService* network_service,
+                               PackageFetcherDelegate* delegate,
+                               const GURL& url)
+    : delegate_(delegate),
+      url_(url) {
+  network_service->CreateURLLoader(Get(&loader_));
+
+  URLRequestPtr request(URLRequest::New());
+  request->url = url.spec();
+  request->auto_follow_redirects = true;
+
+  loader_->Start(request.Pass(),
+                 base::Bind(&PackageFetcher::OnReceivedResponse,
+                            base::Unretained(this)));
+}
+
+PackageFetcher::~PackageFetcher() {
+}
+
+void PackageFetcher::OnReceivedResponse(URLResponsePtr response) {
+  if (response->error) {
+    printf("Got error %d (%s) for %s\n",
+           response->error->code,
+           response->error->description.get().c_str(),
+           url_.spec().c_str());
+    delegate_->FetchFailed(url_);
+    return;
+  }
+
+  if (!base::CreateTemporaryFile(&output_file_name_)) {
+    delegate_->FetchFailed(url_);
+    return;
+  }
+  output_file_.Initialize(output_file_name_,
+      base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+  if (!output_file_.IsValid()) {
+    base::DeleteFile(output_file_name_, false);
+    delegate_->FetchFailed(url_);
+    // Danger: may be deleted now.
+    return;
+  }
+
+  body_ = response->body.Pass();
+  ReadData(MOJO_RESULT_OK);
+  // Danger: may be deleted now.
+}
+
+void PackageFetcher::ReadData(MojoResult) {
+  char buf[4096];
+  uint32_t num_bytes = sizeof(buf);
+  MojoResult result = ReadDataRaw(body_.get(), buf, &num_bytes,
+                                  MOJO_READ_DATA_FLAG_NONE);
+  if (result == MOJO_RESULT_SHOULD_WAIT) {
+    WaitToReadMore();
+  } else if (result == MOJO_RESULT_OK) {
+    if (output_file_.WriteAtCurrentPos(buf, num_bytes) !=
+        static_cast<int>(num_bytes)) {
+      // Clean up the output file.
+      output_file_.Close();
+      base::DeleteFile(output_file_name_, false);
+
+      delegate_->FetchFailed(url_);
+      // Danger: may be deleted now.
+      return;
+    }
+    WaitToReadMore();
+  } else if (result == MOJO_RESULT_FAILED_PRECONDITION) {
+    // Done.
+    output_file_.Close();
+    delegate_->FetchSucceeded(url_, output_file_name_);
+    // Danger: may be deleted now.
+  }
+}
+
+void PackageFetcher::WaitToReadMore() {
+  handle_watcher_.Start(
+      body_.get(),
+      MOJO_HANDLE_SIGNAL_READABLE,
+      MOJO_DEADLINE_INDEFINITE,
+      base::Bind(&PackageFetcher::ReadData, base::Unretained(this)));
+}
+
+}  // namespace mojo
diff --git a/mojo/tools/package_manager/package_fetcher.h b/mojo/tools/package_manager/package_fetcher.h
new file mode 100644
index 0000000..3cbd7a7
--- /dev/null
+++ b/mojo/tools/package_manager/package_fetcher.h
@@ -0,0 +1,63 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MOJO_TOOLS_PACKAGE_MANAGER_FETCHER_H_
+#define MOJO_TOOLS_PACKAGE_MANAGER_FETCHER_H_
+
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "mojo/common/handle_watcher.h"
+#include "mojo/public/cpp/system/macros.h"
+#include "mojo/services/public/interfaces/network/network_service.mojom.h"
+#include "mojo/services/public/interfaces/network/url_loader.mojom.h"
+#include "url/gurl.h"
+
+namespace base {
+class FilePath;
+}  // namespace base
+
+namespace mojo {
+
+class PackageFetcherDelegate;
+
+class PackageFetcher {
+ public:
+  PackageFetcher(NetworkService* network_service,
+                 PackageFetcherDelegate* delegate,
+                 const GURL& url);
+  virtual ~PackageFetcher();
+
+ private:
+  void OnReceivedResponse(URLResponsePtr response);
+
+  void ReadData(MojoResult);
+  void WaitToReadMore();
+
+  PackageFetcherDelegate* delegate_;
+
+  // URL of the package.
+  GURL url_;
+
+  URLLoaderPtr loader_;
+
+  // Valid once file has started streaming.
+  ScopedDataPipeConsumerHandle body_;
+  common::HandleWatcher handle_watcher_;
+
+  base::FilePath output_file_name_;
+  base::File output_file_;
+
+  MOJO_DISALLOW_COPY_AND_ASSIGN(PackageFetcher);
+};
+
+class PackageFetcherDelegate {
+ public:
+  virtual void FetchSucceeded(const GURL& url, const base::FilePath& name) = 0;
+
+  virtual void FetchFailed(const GURL& url) = 0;
+};
+
+}  // namespace mojo
+
+#endif  // MOJO_TOOLS_PACKAGE_MANAGER_FETCHER_H_
diff --git a/mojo/tools/package_manager/package_manager.cc b/mojo/tools/package_manager/package_manager.cc
new file mode 100644
index 0000000..b091f6d
--- /dev/null
+++ b/mojo/tools/package_manager/package_manager.cc
@@ -0,0 +1,12 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "mojo/application/application_runner_chromium.h"
+#include "mojo/public/c/system/main.h"
+#include "mojo/tools/package_manager/package_manager_application.h"
+
+MojoResult MojoMain(MojoHandle shell_handle) {
+  mojo::ApplicationRunnerChromium runner(new mojo::PackageManagerApplication);
+  return runner.Run(shell_handle);
+}
diff --git a/mojo/tools/package_manager/package_manager_application.cc b/mojo/tools/package_manager/package_manager_application.cc
new file mode 100644
index 0000000..ae4d4a4
--- /dev/null
+++ b/mojo/tools/package_manager/package_manager_application.cc
@@ -0,0 +1,113 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "mojo/tools/package_manager/package_manager_application.h"
+
+#include "base/files/file_util.h"
+#include "mojo/tools/package_manager/manifest.h"
+#include "mojo/tools/package_manager/unpacker.h"
+#include "base/message_loop/message_loop.h"
+#include "base/stl_util.h"
+#include "mojo/public/cpp/application/application_impl.h"
+
+namespace mojo {
+
+namespace {
+
+const base::FilePath::CharType kManifestFileName[] =
+    FILE_PATH_LITERAL("manifest.json");
+
+}  // namespace
+
+PackageManagerApplication::PendingLoad::PendingLoad() {
+}
+
+PackageManagerApplication::PendingLoad::~PendingLoad() {
+}
+
+PackageManagerApplication::PackageManagerApplication() {
+}
+
+PackageManagerApplication::~PackageManagerApplication() {
+  STLDeleteContainerPairSecondPointers(pending_.begin(), pending_.end());
+}
+
+void PackageManagerApplication::Initialize(ApplicationImpl* app) {
+  app->ConnectToService("mojo:mojo_network_service", &network_service_);
+
+  printf("Enter URL> ");
+  char buf[1024];
+  if (scanf("%1023s", buf) != 1) {
+    printf("No input, exiting\n");
+    base::MessageLoop::current()->Quit();
+    return;
+  }
+  GURL url(buf);
+  if (!url.is_valid()) {
+    printf("Invalid URL\n");
+    base::MessageLoop::current()->Quit();
+    return;
+  }
+
+  StartLoad(url);
+}
+
+void PackageManagerApplication::StartLoad(const GURL& url) {
+  if (completed_.find(url) != completed_.end() ||
+      pending_.find(url) != pending_.end())
+    return;  // Already loaded or are loading this one.
+
+  PendingLoad* load = new PendingLoad;
+  load->fetcher.reset(new mojo::PackageFetcher(
+      network_service_.get(), this, url));
+  pending_[url] = load;
+}
+
+void PackageManagerApplication::LoadDeps(const Manifest& manifest) {
+  for (size_t i = 0; i < manifest.deps().size(); i++)
+    StartLoad(manifest.deps()[i]);
+}
+
+void PackageManagerApplication::PendingLoadComplete(const GURL& url) {
+  pending_.erase(pending_.find(url));
+  completed_.insert(url);
+  if (pending_.empty())
+    base::MessageLoop::current()->Quit();
+}
+
+void PackageManagerApplication::FetchSucceeded(
+    const GURL& url,
+    const base::FilePath& name) {
+  Unpacker unpacker;
+  if (!unpacker.Unpack(name)) {
+    base::DeleteFile(name, false);
+    printf("Failed to unpack\n");
+    PendingLoadComplete(url);
+    return;
+  }
+  // The downloaded .zip file is no longer necessary.
+  base::DeleteFile(name, false);
+
+  // Load the manifest.
+  base::FilePath manifest_path = unpacker.dir().Append(kManifestFileName);
+  Manifest manifest;
+  std::string err_msg;
+  if (!manifest.ParseFromFile(manifest_path, &err_msg)) {
+    printf("%s\n", err_msg.c_str());
+    PendingLoadComplete(url);
+    return;
+  }
+
+  // Enqueue loads for any deps.
+  LoadDeps(manifest);
+
+  printf("Loaded %s\n", url.spec().c_str());
+  PendingLoadComplete(url);
+}
+
+void PackageManagerApplication::FetchFailed(const GURL& url) {
+  PendingLoadComplete(url);
+}
+
+}  // namespace mojo
diff --git a/mojo/tools/package_manager/package_manager_application.h b/mojo/tools/package_manager/package_manager_application.h
new file mode 100644
index 0000000..87d4fcd
--- /dev/null
+++ b/mojo/tools/package_manager/package_manager_application.h
@@ -0,0 +1,63 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MOJO_PACKAGE_MANAGER_PACKAGE_MANAGER_APPLICATION_H_
+#define MOJO_PACKAGE_MANAGER_PACKAGE_MANAGER_APPLICATION_H_
+
+#include <map>
+#include <set>
+
+#include "mojo/public/cpp/application/application_delegate.h"
+#include "mojo/public/cpp/application/interface_factory.h"
+#include "mojo/public/cpp/system/macros.h"
+#include "mojo/services/public/interfaces/network/network_service.mojom.h"
+#include "mojo/tools/package_manager/package_fetcher.h"
+
+namespace mojo {
+
+class Manifest;
+
+class PackageManagerApplication
+    : public ApplicationDelegate,
+      public PackageFetcherDelegate {
+ public:
+  PackageManagerApplication();
+  virtual ~PackageManagerApplication();
+
+ private:
+  struct PendingLoad {
+    PendingLoad();
+    ~PendingLoad();
+
+    scoped_ptr<PackageFetcher> fetcher;
+  };
+  typedef std::map<GURL, PendingLoad*> PendingLoadMap;
+
+  void StartLoad(const GURL& url);
+
+  void LoadDeps(const Manifest& manifest);
+
+  // Deletes the pending load entry for the given URL and possibly exits the
+  // message loop if all loads are done.
+  void PendingLoadComplete(const GURL& url);
+
+  // ApplicationDelegate implementation.
+  virtual void Initialize(ApplicationImpl* app) override;
+
+  // PackageFetcher.
+  virtual void FetchSucceeded(const GURL& url,
+                              const base::FilePath& name) override;
+  virtual void FetchFailed(const GURL& url) override;
+
+  mojo::NetworkServicePtr network_service_;
+
+  PendingLoadMap pending_;  // Owning pointers.
+  std::set<GURL> completed_;
+
+  MOJO_DISALLOW_COPY_AND_ASSIGN(PackageManagerApplication);
+};
+
+}  // namespace mojo
+
+#endif  // MOJO_PACKAGE_MANAGER_PACKAGE_MANAGER_APPLICATION_H
diff --git a/mojo/tools/package_manager/unpacker.cc b/mojo/tools/package_manager/unpacker.cc
new file mode 100644
index 0000000..8ed34d5
--- /dev/null
+++ b/mojo/tools/package_manager/unpacker.cc
@@ -0,0 +1,35 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "mojo/tools/package_manager/unpacker.h"
+
+#include "base/files/file_util.h"
+#include "base/logging.h"
+#include "third_party/zlib/google/zip.h"
+
+namespace mojo {
+
+Unpacker::Unpacker() {
+}
+
+Unpacker::~Unpacker() {
+  if (!dir_.empty())
+    DeleteFile(dir_, true);
+}
+
+bool Unpacker::Unpack(const base::FilePath& zip_file) {
+  DCHECK(zip_file_.empty());
+  zip_file_ = zip_file;
+
+  DCHECK(dir_.empty());
+  if (!CreateNewTempDirectory(base::FilePath::StringType(), &dir_))
+    return false;
+  if (!zip::Unzip(zip_file, dir_)) {
+    dir_ = base::FilePath();
+    return false;
+  }
+  return true;
+}
+
+}  // namespace mojo
diff --git a/mojo/tools/package_manager/unpacker.h b/mojo/tools/package_manager/unpacker.h
new file mode 100644
index 0000000..2f7acc5
--- /dev/null
+++ b/mojo/tools/package_manager/unpacker.h
@@ -0,0 +1,40 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MOJO_TOOLS_PACKAGE_MANAGER_UNPACKER_H_
+#define MOJO_TOOLS_PACKAGE_MANAGER_UNPACKER_H_
+
+#include "base/files/file_path.h"
+#include "base/memory/ref_counted.h"
+#include "mojo/public/cpp/system/macros.h"
+
+namespace mojo {
+
+// Unzips a package into a temporary folder. The temporary folder will be
+// deleted when the object is destroyed.
+//
+// In the future, this class would probably manage doing the unzip operation on
+// a background thread.
+class Unpacker {
+ public:
+  Unpacker();
+  ~Unpacker();
+
+  // Actually does the unpacking, returns true on success.
+  bool Unpack(const base::FilePath& zip_file);
+
+  // The root directory where the package has been unpacked.
+  const base::FilePath& dir() const { return dir_; }
+
+ private:
+  base::FilePath zip_file_;
+
+  base::FilePath dir_;
+
+  MOJO_DISALLOW_COPY_AND_ASSIGN(Unpacker);
+};
+
+}  // namespace mojo
+
+#endif  // MOJO_TOOLS_PACKAGE_MANAGER_UNPACKER_H_
diff --git a/mojo/tools/pylib/mojo_python_tests_runner.py b/mojo/tools/pylib/mojo_python_tests_runner.py
new file mode 100644
index 0000000..e63a2a4
--- /dev/null
+++ b/mojo/tools/pylib/mojo_python_tests_runner.py
@@ -0,0 +1,147 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import json
+import os
+import sys
+import time
+import unittest
+
+
+class MojoPythonTestRunner(object):
+  """Helper class to run python tests on the bots."""
+
+  def __init__(self, test_dir):
+    self._test_dir = test_dir
+
+  def run(self):
+    parser = argparse.ArgumentParser()
+    parser.usage = 'run_mojo_python_tests.py [options] [tests...]'
+    parser.add_argument('-v', '--verbose', action='count', default=0)
+    parser.add_argument('--metadata', action='append', default=[],
+                        help=('optional key=value metadata that will be stored '
+                              'in the results files (can be used for revision '
+                              'numbers, etc.)'))
+    parser.add_argument('--write-full-results-to', metavar='FILENAME',
+                        action='store',
+                        help='path to write the list of full results to.')
+    parser.add_argument('tests', nargs='*')
+
+    self.add_custom_commandline_options(parser)
+    args = parser.parse_args()
+    self.apply_customization(args)
+
+    bad_metadata = False
+    for val in args.metadata:
+      if '=' not in val:
+        print >> sys.stderr, ('Error: malformed metadata "%s"' % val)
+        bad_metadata = True
+    if bad_metadata:
+      print >> sys.stderr
+      parser.print_help()
+      return 2
+
+    chromium_src_dir = os.path.join(os.path.dirname(__file__),
+                                    os.pardir,
+                                    os.pardir,
+                                    os.pardir)
+
+    loader = unittest.loader.TestLoader()
+    print "Running Python unit tests under %s..." % self._test_dir
+
+    pylib_dir = os.path.abspath(os.path.join(chromium_src_dir, self._test_dir))
+    if args.tests:
+      if pylib_dir not in sys.path:
+        sys.path.append(pylib_dir)
+      suite = unittest.TestSuite()
+      for test_name in args.tests:
+        suite.addTests(loader.loadTestsFromName(test_name))
+    else:
+      suite = loader.discover(pylib_dir, pattern='*_unittest.py')
+
+    runner = unittest.runner.TextTestRunner(verbosity=(args.verbose + 1))
+    result = runner.run(suite)
+
+    full_results = _FullResults(suite, result, args.metadata)
+    if args.write_full_results_to:
+      with open(args.write_full_results_to, 'w') as fp:
+        json.dump(full_results, fp, indent=2)
+        fp.write("\n")
+
+    return 0 if result.wasSuccessful() else 1
+
+  def add_custom_commandline_options(self, parser):
+    """Allow to add custom option to the runner script."""
+    pass
+
+  def apply_customization(self, args):
+    """Allow to apply any customization to the runner."""
+    pass
+
+
+TEST_SEPARATOR = '.'
+
+
+def _FullResults(suite, result, metadata):
+  """Convert the unittest results to the Chromium JSON test result format.
+
+  This matches run-webkit-tests (the layout tests) and the flakiness dashboard.
+  """
+
+  full_results = {}
+  full_results['interrupted'] = False
+  full_results['path_delimiter'] = TEST_SEPARATOR
+  full_results['version'] = 3
+  full_results['seconds_since_epoch'] = time.time()
+  for md in metadata:
+    key, val = md.split('=', 1)
+    full_results[key] = val
+
+  all_test_names = _AllTestNames(suite)
+  failed_test_names = _FailedTestNames(result)
+
+  full_results['num_failures_by_type'] = {
+      'FAIL': len(failed_test_names),
+      'PASS': len(all_test_names) - len(failed_test_names),
+  }
+
+  full_results['tests'] = {}
+
+  for test_name in all_test_names:
+    value = {}
+    value['expected'] = 'PASS'
+    if test_name in failed_test_names:
+      value['actual'] = 'FAIL'
+      value['is_unexpected'] = True
+    else:
+      value['actual'] = 'PASS'
+    _AddPathToTrie(full_results['tests'], test_name, value)
+
+  return full_results
+
+
+def _AllTestNames(suite):
+  test_names = []
+  # _tests is protected  pylint: disable=W0212
+  for test in suite._tests:
+    if isinstance(test, unittest.suite.TestSuite):
+      test_names.extend(_AllTestNames(test))
+    else:
+      test_names.append(test.id())
+  return test_names
+
+
+def _FailedTestNames(result):
+  return set(test.id() for test, _ in result.failures + result.errors)
+
+
+def _AddPathToTrie(trie, path, value):
+  if TEST_SEPARATOR not in path:
+    trie[path] = value
+    return
+  directory, rest = path.split(TEST_SEPARATOR, 1)
+  if directory not in trie:
+    trie[directory] = {}
+  _AddPathToTrie(trie[directory], rest, value)
diff --git a/mojo/tools/pylib/transitive_hash.py b/mojo/tools/pylib/transitive_hash.py
new file mode 100644
index 0000000..93e8dc4
--- /dev/null
+++ b/mojo/tools/pylib/transitive_hash.py
@@ -0,0 +1,89 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import subprocess
+import sys
+
+from hashlib import sha256
+from os.path import basename, realpath
+
+_logging = logging.getLogger()
+
+# Based on/taken from
+#   http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/
+# (with cosmetic changes).
+def _memoize(f):
+  """Memoization decorator for a function taking a single argument."""
+  class Memoize(dict):
+    def __missing__(self, key):
+      rv = self[key] = f(key)
+      return rv
+  return Memoize().__getitem__
+
+@_memoize
+def _file_hash(filename):
+  """Returns a string representing the hash of the given file."""
+  _logging.debug("Hashing %s ...", filename)
+  rv = subprocess.check_output(['sha256sum', '-b', filename]).split(None, 1)[0]
+  _logging.debug("  => %s", rv)
+  return rv
+
+@_memoize
+def _get_dependencies(filename):
+  """Returns a list of filenames for files that the given file depends on."""
+  _logging.debug("Getting dependencies for %s ...", filename)
+  lines = subprocess.check_output(['ldd', filename]).splitlines()
+  rv = []
+  for line in lines:
+    i = line.find('/')
+    if i < 0:
+      _logging.debug("  => no file found in line: %s", line)
+      continue
+    rv.append(line[i:].split(None, 1)[0])
+  _logging.debug("  => %s", rv)
+  return rv
+
+def transitive_hash(filename):
+  """Returns a string that represents the "transitive" hash of the given
+  file. The transitive hash is a hash of the file and all the shared libraries
+  on which it depends (done in an order-independent way)."""
+  hashes = set()
+  to_hash = [filename]
+  while to_hash:
+    current_filename = realpath(to_hash.pop())
+    current_hash = _file_hash(current_filename)
+    if current_hash in hashes:
+      _logging.debug("Already seen %s (%s) ...", current_filename, current_hash)
+      continue
+    _logging.debug("Haven't seen %s (%s) ...", current_filename, current_hash)
+    hashes.add(current_hash)
+    to_hash.extend(_get_dependencies(current_filename))
+  return sha256('|'.join(sorted(hashes))).hexdigest()
+
+def main(argv):
+  logging.basicConfig()
+  # Uncomment to debug:
+  # _logging.setLevel(logging.DEBUG)
+
+  if len(argv) < 2:
+    print """\
+Usage: %s [file] ...
+
+Prints the \"transitive\" hash of each (executable) file. The transitive
+hash is a hash of the file and all the shared libraries on which it
+depends (done in an order-independent way).""" % basename(argv[0])
+    return 0
+
+  rv = 0
+  for filename in argv[1:]:
+    try:
+      print transitive_hash(filename), filename
+    except:
+      print "ERROR", filename
+      rv = 1
+  return rv
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv))
diff --git a/mojo/tools/run_mojo_python_bindings_tests.py b/mojo/tools/run_mojo_python_bindings_tests.py
new file mode 100755
index 0000000..916c68d
--- /dev/null
+++ b/mojo/tools/run_mojo_python_bindings_tests.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_script_dir, "pylib"))
+
+from mojo_python_tests_runner import MojoPythonTestRunner
+
+
+class PythonBindingsTestRunner(MojoPythonTestRunner):
+
+  def add_custom_commandline_options(self, parser):
+    parser.add_argument('--build-dir', action='store',
+                        help='path to the build output directory')
+
+  def apply_customization(self, args):
+    if args.build_dir:
+      python_build_dir = os.path.join(args.build_dir, 'python')
+      if python_build_dir not in sys.path:
+        sys.path.append(python_build_dir)
+      python_gen_dir = os.path.join(
+          args.build_dir,
+          'gen', 'mojo', 'public', 'interfaces', 'bindings', 'tests')
+      if python_gen_dir not in sys.path:
+        sys.path.append(python_gen_dir)
+
+
+def main():
+  runner = PythonBindingsTestRunner(os.path.join('mojo', 'python', 'tests'))
+  sys.exit(runner.run())
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/mojo/tools/run_mojo_python_tests.py b/mojo/tools/run_mojo_python_tests.py
new file mode 100755
index 0000000..d83ca54
--- /dev/null
+++ b/mojo/tools/run_mojo_python_tests.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_script_dir, "pylib"))
+
+from mojo_python_tests_runner import MojoPythonTestRunner
+
+
+def main():
+  runner = MojoPythonTestRunner(os.path.join('mojo', 'public', 'tools',
+                                             'bindings', 'pylib'))
+  sys.exit(runner.run())
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/mojo/tools/test_runner.py b/mojo/tools/test_runner.py
new file mode 100755
index 0000000..b0bb4d9
--- /dev/null
+++ b/mojo/tools/test_runner.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A "smart" test runner for gtest unit tests (that caches successes)."""
+
+import logging
+import os
+import subprocess
+import sys
+
+_logging = logging.getLogger()
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_script_dir, "pylib"))
+
+from transitive_hash import transitive_hash
+
+def main(argv):
+  logging.basicConfig()
+  # Uncomment to debug:
+  # _logging.setLevel(logging.DEBUG)
+
+  if len(argv) < 3 or len(argv) > 4:
+    print "Usage: %s gtest_list_file root_dir [successes_cache_file]" % \
+        os.path.basename(argv[0])
+    return 0 if len(argv) < 2 else 1
+
+  _logging.debug("Test list file: %s", argv[1])
+  with open(argv[1], 'rb') as f:
+    gtest_list = [y for y in [x.strip() for x in f.readlines()] \
+                      if y and y[0] != '#']
+  _logging.debug("Test list: %s" % gtest_list)
+
+  print "Running tests in directory: %s" % argv[2]
+  os.chdir(argv[2])
+
+  if len(argv) == 4 and argv[3]:
+    successes_cache_filename = argv[3]
+    print "Successes cache file: %s" % successes_cache_filename
+  else:
+    successes_cache_filename = None
+    print "No successes cache file (will run all tests unconditionally)"
+
+  if successes_cache_filename:
+    # This file simply contains a list of transitive hashes of tests that
+    # succeeded.
+    try:
+      _logging.debug("Trying to read successes cache file: %s",
+                     successes_cache_filename)
+      with open(argv[3], 'rb') as f:
+        successes = set([x.strip() for x in f.readlines()])
+      _logging.debug("Successes: %s", successes)
+    except:
+      # Just assume that it didn't exist, or whatever.
+      print "Failed to read successes cache file %s (will create)" % argv[3]
+      successes = set()
+
+  # Run gtests with color if we're on a TTY (and we're not being told explicitly
+  # what to do).
+  if sys.stdout.isatty() and 'GTEST_COLOR' not in os.environ:
+    _logging.debug("Setting GTEST_COLOR=yes")
+    os.environ['GTEST_COLOR'] = 'yes'
+
+  # TODO(vtl): We may not close this file on failure.
+  successes_cache_file = open(successes_cache_filename, 'ab') \
+      if successes_cache_filename else None
+  for gtest in gtest_list:
+    if gtest[0] == '*':
+      gtest = gtest[1:]
+      _logging.debug("%s is marked as non-cacheable" % gtest)
+      cacheable = False
+    else:
+      cacheable = True
+
+    if successes_cache_file and cacheable:
+      _logging.debug("Getting transitive hash for %s ... " % gtest)
+      try:
+        gtest_hash = transitive_hash(gtest)
+      except:
+        print "Failed to get transitive hash for %s" % gtest
+        return 1
+      _logging.debug("  Transitive hash: %s" % gtest_hash)
+
+      if gtest_hash in successes:
+        print "Skipping %s (previously succeeded)" % gtest
+        continue
+
+    print "Running %s...." % gtest,
+    sys.stdout.flush()
+    try:
+      subprocess.check_output(["./" + gtest], stderr=subprocess.STDOUT)
+      print "Succeeded"
+      # Record success.
+      if successes_cache_filename and cacheable:
+        successes.add(gtest_hash)
+        successes_cache_file.write(gtest_hash + '\n')
+        successes_cache_file.flush()
+    except subprocess.CalledProcessError as e:
+      print "Failed with exit code %d and output:" % e.returncode
+      print 72 * '-'
+      print e.output
+      print 72 * '-'
+      return 1
+    except OSError as e:
+      print "  Failed to start test"
+      return 1
+  print "All tests succeeded"
+  if successes_cache_file:
+    successes_cache_file.close()
+
+  return 0
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv))