OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 # On Android we build unit test bundles as shared libraries. To run |
| 7 # tests, we launch a special "test runner" apk which loads the library |
| 8 # then jumps into it. Since java is required for many tests |
| 9 # (e.g. PathUtils.java), a "pure native" test bundle is inadequate. |
| 10 # |
| 11 # This script, generate_native_test.py, is used to generate the source |
| 12 # for an apk that wraps a unit test shared library bundle. That |
| 13 # allows us to have a single boiler-plate application be used across |
| 14 # all unit test bundles. |
| 15 |
| 16 import logging |
| 17 import optparse |
| 18 import os |
| 19 import re |
| 20 import shutil |
| 21 import subprocess |
| 22 import sys |
| 23 |
| 24 |
| 25 class NativeTestApkGenerator(object): |
| 26 """Generate a native test apk source tree. |
| 27 |
| 28 TODO(jrg): develop this more so the activity name is replaced as |
| 29 well. That will allow multiple test runners to be installed at the |
| 30 same time. (The complication is that it involves renaming a java |
| 31 class, which implies regeneration of a jni header, and on and on...) |
| 32 """ |
| 33 |
| 34 # Files or directories we need to copy to create a complete apk test shell. |
| 35 _SOURCE_FILES = ['AndroidManifest.xml', |
| 36 'native_test_apk.xml', |
| 37 'res', # res/values/strings.xml |
| 38 'java', # .../ChromeNativeTestActivity.java |
| 39 ] |
| 40 |
| 41 # Files in the destion directory that have a "replaceme" string |
| 42 # which should be replaced by the basename of the shared library. |
| 43 # Note we also update the filename if 'replaceme' is itself found in |
| 44 # the filename. |
| 45 _REPLACEME_FILES = ['AndroidManifest.xml', |
| 46 'native_test_apk.xml', |
| 47 'res/values/strings.xml'] |
| 48 |
| 49 def __init__(self, native_library, jars, output_directory): |
| 50 self._native_library = native_library |
| 51 self._jars = jars |
| 52 self._output_directory = output_directory |
| 53 self._root_name = None |
| 54 if self._native_library: |
| 55 self._root_name = self._LibraryRoot() |
| 56 logging.warn('root name: %s' % self._root_name) |
| 57 |
| 58 |
| 59 def _LibraryRoot(self): |
| 60 """Return a root name for a shared library. |
| 61 |
| 62 The root name should be suitable for substitution in apk files |
| 63 like the manifest. For example, blah/foo/libbase_unittests.so |
| 64 becomes base_unittests. |
| 65 """ |
| 66 rootfinder = re.match('.?lib(.+).so', |
| 67 os.path.basename(self._native_library)) |
| 68 if rootfinder: |
| 69 return rootfinder.group(1) |
| 70 else: |
| 71 return None |
| 72 |
| 73 def _CopyTemplateFiles(self): |
| 74 """Copy files needed to build a new apk. |
| 75 |
| 76 TODO(jrg): add more smarts so we don't change file timestamps if |
| 77 the files don't change? |
| 78 """ |
| 79 srcdir = os.path.dirname(os.path.realpath( __file__)) |
| 80 if not os.path.exists(self._output_directory): |
| 81 os.makedirs(self._output_directory) |
| 82 for f in self._SOURCE_FILES: |
| 83 src = os.path.join(srcdir, f) |
| 84 dest = os.path.join(self._output_directory, f) |
| 85 if os.path.isfile(src): |
| 86 if os.path.exists(dest): |
| 87 os.remove(dest) |
| 88 logging.warn('%s --> %s' % (src, dest)) |
| 89 shutil.copyfile(src, dest) |
| 90 else: # directory |
| 91 if os.path.exists(dest): |
| 92 # One more sanity check since we're deleting a directory... |
| 93 if not '/out/' in dest: |
| 94 raise Exception('Unbelievable output directory; bailing for safety') |
| 95 shutil.rmtree(dest) |
| 96 logging.warn('%s --> %s' % (src, dest)) |
| 97 shutil.copytree(src, dest) |
| 98 |
| 99 def _ReplaceStrings(self): |
| 100 """Replace 'replaceme' strings in generated files with a root libname. |
| 101 |
| 102 If we have no root libname (e.g. no shlib was specified), do nothing. |
| 103 """ |
| 104 if not self._root_name: |
| 105 return |
| 106 logging.warn('Replacing "replaceme" with ' + self._root_name) |
| 107 for f in self._REPLACEME_FILES: |
| 108 dest = os.path.join(self._output_directory, f) |
| 109 contents = open(dest).read() |
| 110 contents = contents.replace('replaceme', self._root_name) |
| 111 dest = dest.replace('replaceme', self._root_name) # update the filename! |
| 112 open(dest, "w").write(contents) |
| 113 |
| 114 def _CopyLibraryAndJars(self): |
| 115 """Copy the shlib and jars into the apk source tree (if relevant)""" |
| 116 if self._native_library: |
| 117 destdir = os.path.join(self._output_directory, 'libs/armeabi') |
| 118 if not os.path.exists(destdir): |
| 119 os.makedirs(destdir) |
| 120 dest = os.path.join(destdir, os.path.basename(self._native_library)) |
| 121 logging.warn('%s --> %s' % (self._native_library, dest)) |
| 122 shutil.copyfile(self._native_library, dest) |
| 123 if self._jars: |
| 124 destdir = os.path.join(self._output_directory, 'libs') |
| 125 if not os.path.exists(destdir): |
| 126 os.makedirs(destdir) |
| 127 for jar in self._jars: |
| 128 dest = os.path.join(destdir, os.path.basename(jar)) |
| 129 logging.warn('%s --> %s' % (jar, dest)) |
| 130 shutil.copyfile(jar, dest) |
| 131 |
| 132 def CreateBundle(self): |
| 133 """Create the apk bundle source and assemble components.""" |
| 134 self._CopyTemplateFiles() |
| 135 self._ReplaceStrings() |
| 136 self._CopyLibraryAndJars() |
| 137 |
| 138 def Compile(self, ant_args): |
| 139 """Build the generated apk with ant. |
| 140 |
| 141 Args: |
| 142 ant_args: extra args to pass to ant |
| 143 """ |
| 144 cmd = ['ant'] |
| 145 if ant_args: |
| 146 cmd.append(ant_args) |
| 147 cmd.extend(['-buildfile', |
| 148 os.path.join(self._output_directory, 'native_test_apk.xml')]) |
| 149 logging.warn(cmd) |
| 150 p = subprocess.Popen(cmd, stderr=subprocess.STDOUT) |
| 151 (stdout, _) = p.communicate() |
| 152 logging.warn(stdout) |
| 153 if p.returncode != 0: |
| 154 logging.error('Ant return code %d' % p.returncode) |
| 155 sys.exit(p.returncode) |
| 156 |
| 157 |
| 158 def main(argv): |
| 159 parser = optparse.OptionParser() |
| 160 parser.add_option('--verbose', |
| 161 help='Be verbose') |
| 162 parser.add_option('--native_library', |
| 163 help='Full name of native shared library test bundle') |
| 164 parser.add_option('--jar', action='append', |
| 165 help='Include this jar; can be specified multiple times') |
| 166 parser.add_option('--output', |
| 167 help='Output directory for generated files.') |
| 168 parser.add_option('--ant-compile', action='store_true', |
| 169 help='If specified, build the generated apk with ant') |
| 170 parser.add_option('--ant-args', |
| 171 help='extra args for ant') |
| 172 |
| 173 options, _ = parser.parse_args(argv) |
| 174 |
| 175 # It is not an error to specify no native library; the apk should |
| 176 # still be generated and build. It will, however, print |
| 177 # NATIVE_LOADER_FAILED when run. |
| 178 if not options.output: |
| 179 raise Exception('No output directory specified for generated files') |
| 180 |
| 181 if options.verbose: |
| 182 logging.basicConfig(level=logging.DEBUG, format=' %(message)s') |
| 183 |
| 184 ntag = NativeTestApkGenerator(native_library=options.native_library, |
| 185 jars=options.jar, |
| 186 output_directory=options.output) |
| 187 ntag.CreateBundle() |
| 188 if options.ant_compile: |
| 189 ntag.Compile(options.ant_args) |
| 190 |
| 191 logging.warn('COMPLETE.') |
| 192 |
| 193 if __name__ == '__main__': |
| 194 sys.exit(main(sys.argv)) |
OLD | NEW |