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