Add mojo/tools/mopy with paths and version utilities
This consolidates some common mojo python script utilities into
mojo/tools/mopy and updates scripts under mojo/tools to use them.
Currently this has some path manipulation, version querying and python
test running utilities but I expect this to grow over time. A skypy
could also resuse a lot of of this functionality as well.
R=eseidel@chromium.org
Review URL: https://codereview.chromium.org/707893005
diff --git a/mojo/PRESUBMIT.py b/mojo/PRESUBMIT.py
index 663c3de..557883e 100644
--- a/mojo/PRESUBMIT.py
+++ b/mojo/PRESUBMIT.py
@@ -28,6 +28,8 @@
# For the roll tools scripts:
mojo_roll_tools_path = os.path.join(
input_api.PresubmitLocalPath(), "tools", "roll")
+ # For all mojo/tools scripts
+ mopy_path = os.path.join(input_api.PresubmitLocalPath(), "tools")
# TODO(vtl): Don't lint these files until the (many) problems are fixed
# (possibly by deleting/rewriting some files).
temporary_black_list = input_api.DEFAULT_BLACK_LIST + \
@@ -43,6 +45,7 @@
mojo_python_bindings_path,
mojo_python_bindings_tests_path,
mojo_roll_tools_path,
+ mopy_path,
]
results += input_api.canned_checks.RunPylint(
input_api, output_api, extra_paths_list=pylint_extra_paths,
diff --git a/mojo/tools/check_mojom_golden_files.py b/mojo/tools/check_mojom_golden_files.py
index 9af7a86..ae80ceb 100755
--- a/mojo/tools/check_mojom_golden_files.py
+++ b/mojo/tools/check_mojom_golden_files.py
@@ -9,11 +9,11 @@
from filecmp import dircmp
from shutil import rmtree
from tempfile import mkdtemp
+from mopy.paths import Paths
-_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",
+paths = Paths()
+
+sys.path.insert(0, os.path.join(paths.mojo_dir, "public", "tools", "bindings",
"pylib"))
from mojom_tests.support.find_files import FindFiles
from mojom_tests.support.run_bindings_generator import RunBindingsGenerator
@@ -68,14 +68,14 @@
if args.verbose:
print "Generating files to %s ..." % out_dir
- mojom_files = FindFiles(_mojo_dir, "*.mojom")
+ mojom_files = FindFiles(paths.mojo_dir, "*.mojom")
for mojom_file in mojom_files:
if args.verbose:
- print " Processing %s ..." % os.path.relpath(mojom_file, _mojo_dir)
+ print " Processing %s ..." % os.path.relpath(mojom_file, paths.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)])
+ RunBindingsGenerator(out_dir, paths.mojo_dir, mojom_file,
+ ["-I", paths.src_root])
if args.generate_golden_files:
return 0
diff --git a/mojo/tools/mopy/__init__.py b/mojo/tools/mopy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mojo/tools/mopy/__init__.py
diff --git a/mojo/tools/mopy/mojo_python_tests_runner.py b/mojo/tools/mopy/mojo_python_tests_runner.py
new file mode 100644
index 0000000..ef9a6b2
--- /dev/null
+++ b/mojo/tools/mopy/mojo_python_tests_runner.py
@@ -0,0 +1,52 @@
+# 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
+import sys
+import unittest
+
+import mopy.paths
+
+
+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.add_argument('-v', '--verbose', action='count', default=0)
+ parser.add_argument('tests', nargs='*')
+
+ self.add_custom_commandline_options(parser)
+ args = parser.parse_args()
+ self.apply_customization(args)
+
+ loader = unittest.loader.TestLoader()
+ print "Running Python unit tests under %s..." % self._test_dir
+
+ src_root = mopy.paths.Paths().src_root
+ pylib_dir = os.path.abspath(os.path.join(src_root, 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)
+ 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
diff --git a/mojo/tools/mopy/paths.py b/mojo/tools/mopy/paths.py
new file mode 100644
index 0000000..2ad3726
--- /dev/null
+++ b/mojo/tools/mopy/paths.py
@@ -0,0 +1,19 @@
+# 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
+
+class Paths(object):
+ """Provides commonly used paths"""
+
+ def __init__(self, build_directory=None):
+ """Specify a build_directory to generate paths to binary artifacts"""
+ self.src_root = os.path.abspath(os.path.join(__file__,
+ os.pardir, os.pardir, os.pardir, os.pardir))
+ self.mojo_dir = os.path.join(self.src_root, "mojo")
+ if build_directory:
+ self.mojo_shell_path = os.path.join(self.src_root, build_directory,
+ "mojo_shell")
+ else:
+ self.mojo_shell_path = None
diff --git a/mojo/tools/mopy/transitive_hash.py b/mojo/tools/mopy/transitive_hash.py
new file mode 100644
index 0000000..6864ae3
--- /dev/null
+++ b/mojo/tools/mopy/transitive_hash.py
@@ -0,0 +1,93 @@
+# 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
+
+# pylint: disable=E0611
+from hashlib import sha256
+# pylint: enable=E0611
+from os.path import basename, realpath
+
+_logging = logging.getLogger()
+
+# pylint: disable=C0301
+# Based on/taken from
+# http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/
+# (with cosmetic changes).
+# pylint: enable=C0301
+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 subprocess.CalledProcessError:
+ print "ERROR", filename
+ rv = 1
+ return rv
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/mojo/tools/mopy/version.py b/mojo/tools/mopy/version.py
new file mode 100644
index 0000000..8f8e132
--- /dev/null
+++ b/mojo/tools/mopy/version.py
@@ -0,0 +1,14 @@
+# 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 subprocess
+
+import mopy.paths
+
+class Version(object):
+ """Computes the version (git committish) of the mojo repo"""
+
+ def __init__(self):
+ self.version = subprocess.check_output(["git", "rev-parse", "HEAD"],
+ cwd=mopy.paths.Paths().src_root).strip()
diff --git a/mojo/tools/run_mojo_python_bindings_tests.py b/mojo/tools/run_mojo_python_bindings_tests.py
index 916c68d..d64f49c 100755
--- a/mojo/tools/run_mojo_python_bindings_tests.py
+++ b/mojo/tools/run_mojo_python_bindings_tests.py
@@ -6,14 +6,10 @@
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
+from mopy.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')
diff --git a/mojo/tools/run_mojo_python_tests.py b/mojo/tools/run_mojo_python_tests.py
index d83ca54..127e3fc 100755
--- a/mojo/tools/run_mojo_python_tests.py
+++ b/mojo/tools/run_mojo_python_tests.py
@@ -6,10 +6,7 @@
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
+from mopy.mojo_python_tests_runner import MojoPythonTestRunner
def main():
diff --git a/mojo/tools/test_runner.py b/mojo/tools/test_runner.py
index 9174912..28eb3be 100755
--- a/mojo/tools/test_runner.py
+++ b/mojo/tools/test_runner.py
@@ -12,10 +12,7 @@
_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
+from mopy.transitive_hash import transitive_hash
def main(argv):
logging.basicConfig()
diff --git a/mojo/tools/upload_shell_binary.py b/mojo/tools/upload_shell_binary.py
index f9c1ff1..91c0ed9 100755
--- a/mojo/tools/upload_shell_binary.py
+++ b/mojo/tools/upload_shell_binary.py
@@ -11,30 +11,30 @@
import time
import zipfile
-root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
- "..", "..")
+from mopy.paths import Paths
+from mopy.version import Version
-sys.path.insert(0, os.path.join(root_path, "tools"))
+paths = Paths(os.path.join("out", "Release"))
+
+sys.path.insert(0, os.path.join(paths.src_root, "tools"))
# pylint: disable=F0401
import find_depot_tools
-binary_path = os.path.join(root_path, "out", "Release", "mojo_shell")
-
depot_tools_path = find_depot_tools.add_depot_tools_to_path()
gsutil_exe = os.path.join(depot_tools_path, "third_party", "gsutil", "gsutil")
-def upload(dry_run):
- version = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root_path)
- version = version.strip()
- dest = "gs://mojo/shell/" + version + "/linux-x64.zip"
+def upload(dry_run, verbose):
+ dest = "gs://mojo/shell/" + Version().version + "/linux-x64.zip"
with tempfile.NamedTemporaryFile() as zip_file:
with zipfile.ZipFile(zip_file, 'w') as z:
- with open(binary_path) as shell_binary:
+ with open(paths.mojo_shell_path) as shell_binary:
zipinfo = zipfile.ZipInfo("mojo_shell")
zipinfo.external_attr = 0777 << 16L
zipinfo.compress_type = zipfile.ZIP_DEFLATED
- zipinfo.date_time = time.gmtime(os.path.getmtime(binary_path))
+ zipinfo.date_time = time.gmtime(os.path.getmtime(paths.mojo_shell_path))
+ if verbose:
+ print "zipping %s" % paths.mojo_shell_path
z.writestr(zipinfo, shell_binary.read())
if dry_run:
print str([gsutil_exe, "cp", zip_file.name, dest])
@@ -46,8 +46,10 @@
"google storage")
parser.add_argument("-n", "--dry_run", help="Dry run, do not actually "+
"upload", action="store_true")
+ parser.add_argument("-v", "--verbose", help="Verbose mode",
+ action="store_true")
args = parser.parse_args()
- upload(args.dry_run)
+ upload(args.dry_run, args.verbose)
return 0
if __name__ == "__main__":