| #!/usr/bin/env python |
| # |
| # Copyright (c) 2012 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. |
| |
| # On Android we build unit test bundles as shared libraries. To run |
| # tests, we launch a special "test runner" apk which loads the library |
| # then jumps into it. Since java is required for many tests |
| # (e.g. PathUtils.java), a "pure native" test bundle is inadequate. |
| # |
| # This script, generate_native_test.py, is used to generate the source |
| # for an apk that wraps a unit test shared library bundle. That |
| # allows us to have a single boiler-plate application be used across |
| # all unit test bundles. |
| |
| import logging |
| import optparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| # cmd_helper.py is under ../../build/android/ |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', |
| '..', 'build', 'android'))) |
| from pylib import cmd_helper # pylint: disable=F0401 |
| |
| |
| class NativeTestApkGenerator(object): |
| """Generate a native test apk source tree. |
| |
| TODO(jrg): develop this more so the activity name is replaced as |
| well. That will allow multiple test runners to be installed at the |
| same time. (The complication is that it involves renaming a java |
| class, which implies regeneration of a jni header, and on and on...) |
| """ |
| |
| # Files or directories we need to copy to create a complete apk test shell. |
| _SOURCE_FILES = ['AndroidManifest.xml', |
| 'native_test_apk.xml', |
| 'res', # res/values/strings.xml |
| 'java', # .../ChromeNativeTestActivity.java |
| ] |
| |
| # Files in the destion directory that have a "replaceme" string |
| # which should be replaced by the basename of the shared library. |
| # Note we also update the filename if 'replaceme' is itself found in |
| # the filename. |
| _REPLACEME_FILES = ['AndroidManifest.xml', |
| 'native_test_apk.xml', |
| 'res/values/strings.xml'] |
| |
| def __init__(self, native_library, strip_binary, output_directory, |
| target_abi): |
| self._native_library = native_library |
| self._strip_binary = strip_binary |
| self._output_directory = os.path.abspath(output_directory) |
| self._target_abi = target_abi |
| self._root_name = None |
| if self._native_library: |
| self._root_name = self._LibraryRoot() |
| logging.warn('root name: %s', self._root_name) |
| |
| def _LibraryRoot(self): |
| """Return a root name for a shared library. |
| |
| The root name should be suitable for substitution in apk files |
| like the manifest. For example, blah/foo/libbase_unittests.so |
| becomes base_unittests. |
| """ |
| rootfinder = re.match('.?lib(.+).so', |
| os.path.basename(self._native_library)) |
| if rootfinder: |
| return rootfinder.group(1) |
| else: |
| return None |
| |
| def _CopyTemplateFilesAndClearDir(self): |
| """Copy files needed to build a new apk. |
| |
| Uses rsync to avoid unnecessary io. This call also clears outstanding |
| files in the directory. |
| """ |
| srcdir = os.path.abspath(os.path.dirname(__file__)) |
| destdir = self._output_directory |
| if not os.path.exists(destdir): |
| os.makedirs(destdir) |
| elif not '/out/' in destdir: |
| raise Exception('Unbelievable output directory; bailing for safety') |
| logging.warning('rsync %s --> %s', self._SOURCE_FILES, destdir) |
| logging.info(cmd_helper.GetCmdOutput( |
| ['rsync', '-aRv', '--delete', '--exclude', '.svn'] + |
| self._SOURCE_FILES + [destdir], cwd=srcdir)) |
| |
| def _ReplaceStrings(self): |
| """Replace 'replaceme' strings in generated files with a root libname. |
| |
| If we have no root libname (e.g. no shlib was specified), do nothing. |
| """ |
| if not self._root_name: |
| return |
| logging.warn('Replacing "replaceme" with ' + self._root_name) |
| for f in self._REPLACEME_FILES: |
| dest = os.path.join(self._output_directory, f) |
| contents = open(dest).read() |
| contents = contents.replace('replaceme', self._root_name) |
| dest = dest.replace('replaceme', self._root_name) # update the filename! |
| open(dest, 'w').write(contents) |
| |
| def _CopyLibrary(self): |
| """Copy the shlib into the apk source tree (if relevant).""" |
| if self._native_library: |
| destdir = os.path.join(self._output_directory, 'libs/' + self._target_abi) |
| if not os.path.exists(destdir): |
| os.makedirs(destdir) |
| dest = os.path.join(destdir, os.path.basename(self._native_library)) |
| logging.warn('strip %s --> %s', self._native_library, dest) |
| cmd_helper.RunCmd( |
| [self._strip_binary, '--strip-unneeded', self._native_library, '-o', |
| dest]) |
| |
| def CreateBundle(self): |
| """Create the apk bundle source and assemble components.""" |
| self._CopyTemplateFilesAndClearDir() |
| self._ReplaceStrings() |
| self._CopyLibrary() |
| |
| def Compile(self, ant_args): |
| """Build the generated apk with ant. |
| |
| Args: |
| ant_args: extra args to pass to ant |
| """ |
| cmd = ['ant'] |
| if ant_args: |
| cmd.extend(ant_args) |
| cmd.append("-DAPP_ABI=" + self._target_abi) |
| cmd.extend(['-buildfile', |
| os.path.join(self._output_directory, 'native_test_apk.xml')]) |
| logging.warn(cmd) |
| p = subprocess.Popen(cmd, stderr=subprocess.STDOUT) |
| (stdout, _) = p.communicate() |
| logging.warn(stdout) |
| if p.returncode != 0: |
| logging.error('Ant return code %d', p.returncode) |
| sys.exit(p.returncode) |
| |
| def main(argv): |
| parser = optparse.OptionParser() |
| parser.add_option('--verbose', |
| help='Be verbose') |
| parser.add_option('--native_library', |
| help='Full name of native shared library test bundle') |
| parser.add_option('--jars', |
| help='Space separated list of jars to be included') |
| parser.add_option('--output', |
| help='Output directory for generated files.') |
| parser.add_option('--app_abi', default='armeabi', |
| help='ABI for native shared library') |
| parser.add_option('--strip-binary', |
| help='Binary to use for stripping the native libraries.') |
| parser.add_option('--ant-args', action='append', |
| help='extra args for ant') |
| |
| options, _ = parser.parse_args(argv) |
| |
| # It is not an error to specify no native library; the apk should |
| # still be generated and build. It will, however, print |
| # NATIVE_LOADER_FAILED when run. |
| if not options.output: |
| raise Exception('No output directory specified for generated files') |
| |
| if options.verbose: |
| logging.basicConfig(level=logging.DEBUG, format=' %(message)s') |
| |
| if not options.strip_binary: |
| options.strip_binary = os.getenv('STRIP') |
| if not options.strip_binary: |
| raise Exception('No tool for stripping the libraries has been supplied') |
| |
| # Remove all quotes from the jars string and pass the list to ant as |
| # INPUT_JARS_PATHS. |
| # TODO(cjhopman): Remove this when all targets pass the list of jars as an |
| # ant-arg directly. |
| jar_list = [] |
| if options.jars: |
| jar_list = options.jars.replace('"', '').split() |
| options.ant_args.append('-DINPUT_JARS_PATHS=' + " ".join(jar_list)) |
| |
| |
| ntag = NativeTestApkGenerator(native_library=options.native_library, |
| strip_binary=options.strip_binary, |
| output_directory=options.output, |
| target_abi=options.app_abi) |
| ntag.CreateBundle() |
| ntag.Compile(options.ant_args) |
| logging.warn('COMPLETE.') |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv)) |