GN: Side-load dex files as well as native code in incremental installs
This also re-organizes a bit by moving all incremental install related files into //build/android/incremental_install. BUG=520082 Review URL: https://codereview.chromium.org/1338813003 Cr-Original-Commit-Position: refs/heads/master@{#349200} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 933f2593199c4f95678ddda884aa476ff1fe0c23
This commit is contained in:
Родитель
be7248bb97
Коммит
6457f677cf
|
@ -16,16 +16,22 @@ def CommonChecks(input_api, output_api):
|
|||
"""Returns a path relative to presubmit directory."""
|
||||
return input_api.os_path.join(input_api.PresubmitLocalPath(), *dirs)
|
||||
|
||||
build_pys = [
|
||||
r'gyp/.*\.py$',
|
||||
r'gn/.*\.py',
|
||||
r'incremental_install/.*\.py',
|
||||
]
|
||||
output.extend(input_api.canned_checks.RunPylint(
|
||||
input_api,
|
||||
output_api,
|
||||
pylintrc='pylintrc',
|
||||
black_list=[r'pylib/symbols/.*\.py$', r'gyp/.*\.py$', r'gn/.*\.py'],
|
||||
# symbols has its own PRESUBMIT.py
|
||||
black_list=build_pys + [r'pylib/symbols/.*\.py$'],
|
||||
extra_paths_list=[J(), J('buildbot')]))
|
||||
output.extend(input_api.canned_checks.RunPylint(
|
||||
input_api,
|
||||
output_api,
|
||||
white_list=[r'gyp/.*\.py$', r'gn/.*\.py'],
|
||||
white_list=build_pys,
|
||||
extra_paths_list=[J('gyp'), J('gn')]))
|
||||
|
||||
# Disabled due to http://crbug.com/410936
|
||||
|
|
|
@ -13,7 +13,7 @@ from pylib import constants
|
|||
|
||||
_AAPT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'aapt')
|
||||
_MANIFEST_ATTRIBUTE_RE = re.compile(
|
||||
r'\s*A: ([^\(\)= ]*)\([^\(\)= ]*\)="(.*)" \(Raw: .*\)$')
|
||||
r'\s*A: ([^\(\)= ]*)\([^\(\)= ]*\)=(?:"(.*)" \(Raw: .*\)|\(type.*?\)(.*))$')
|
||||
_MANIFEST_ELEMENT_RE = re.compile(r'\s*(?:E|N): (\S*) .*$')
|
||||
_PACKAGE_NAME_RE = re.compile(r'package: .*name=\'(\S*)\'')
|
||||
_SPLIT_NAME_RE = re.compile(r'package: .*split=\'(\S*)\'')
|
||||
|
@ -60,7 +60,7 @@ def _ParseManifestFromApk(apk_path):
|
|||
if m:
|
||||
if not m.group(1) in node:
|
||||
node[m.group(1)] = []
|
||||
node[m.group(1)].append(m.group(2))
|
||||
node[m.group(1)].append(m.group(2) or m.group(3))
|
||||
continue
|
||||
|
||||
return parsed_manifest
|
||||
|
@ -72,6 +72,7 @@ class ApkHelper(object):
|
|||
self._manifest = None
|
||||
self._package_name = None
|
||||
self._split_name = None
|
||||
self._has_isolated_processes = None
|
||||
|
||||
def GetActivityName(self):
|
||||
"""Returns the name of the Activity in the apk."""
|
||||
|
@ -130,6 +131,18 @@ class ApkHelper(object):
|
|||
return self._split_name
|
||||
return None
|
||||
|
||||
def HasIsolatedProcesses(self):
|
||||
"""Returns whether any services exist that use isolatedProcess=true."""
|
||||
if self._has_isolated_processes is None:
|
||||
manifest_info = self._GetManifest()
|
||||
try:
|
||||
services = manifest_info['manifest']['application']['service']
|
||||
self._has_isolated_processes = (
|
||||
any(int(v, 0) for v in services['android:isolatedProcess']))
|
||||
except KeyError:
|
||||
self._has_isolated_processes = False
|
||||
return self._has_isolated_processes
|
||||
|
||||
def _GetManifest(self):
|
||||
if not self._manifest:
|
||||
self._manifest = _ParseManifestFromApk(self._apk_path)
|
||||
|
|
|
@ -627,6 +627,8 @@ class DeviceUtils(object):
|
|||
retries=None):
|
||||
"""Remove the app |package_name| from the device.
|
||||
|
||||
This is a no-op if the app is not already installed.
|
||||
|
||||
Args:
|
||||
package_name: The package to uninstall.
|
||||
keep_data: (optional) Whether to keep the data and cache directories.
|
||||
|
@ -638,6 +640,9 @@ class DeviceUtils(object):
|
|||
CommandTimeoutError if the uninstallation times out.
|
||||
DeviceUnreachableError on missing device.
|
||||
"""
|
||||
installed = self._GetApplicationPathsInternal(package_name)
|
||||
if not installed:
|
||||
return
|
||||
try:
|
||||
self.adb.Uninstall(package_name, keep_data)
|
||||
self._cache['package_apk_paths'][package_name] = []
|
||||
|
|
|
@ -605,7 +605,7 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
|
|||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'),
|
||||
['/fake/data/app/test.package.apk']),
|
||||
self.call.adb.Uninstall('test.package', False),
|
||||
self.call.device.Uninstall('test.package'),
|
||||
self.call.adb.Install('/fake/test/app.apk', reinstall=False)):
|
||||
self.device.Install('/fake/test/app.apk', retries=0, permissions=[])
|
||||
|
||||
|
@ -690,12 +690,18 @@ class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
|
|||
|
||||
|
||||
class DeviceUtilsUninstallTest(DeviceUtilsTest):
|
||||
|
||||
def testUninstall_callsThrough(self):
|
||||
with self.assertCalls(
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'),
|
||||
['/path.apk']),
|
||||
self.call.adb.Uninstall('test.package', True)):
|
||||
self.device.Uninstall('test.package', True)
|
||||
|
||||
def testUninstall_noop(self):
|
||||
with self.assertCalls(
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'), [])):
|
||||
self.device.Uninstall('test.package', True)
|
||||
|
||||
|
||||
class DeviceUtilsSuTest(DeviceUtilsTest):
|
||||
def testSu_preM(self):
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2015 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("//build/config/android/rules.gni")
|
||||
|
||||
android_library("bootstrap_java") {
|
||||
# Use .dex rather than .dex.jar to be usable by package_apk().
|
||||
dex_path = "$target_gen_dir/bootstrap.dex"
|
||||
java_files = [
|
||||
"java/org/chromium/incrementalinstall/BootstrapApplication.java",
|
||||
"java/org/chromium/incrementalinstall/ClassLoaderPatcher.java",
|
||||
"java/org/chromium/incrementalinstall/LockFile.java",
|
||||
"java/org/chromium/incrementalinstall/Reflect.java",
|
||||
]
|
||||
}
|
|
@ -4,23 +4,25 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Creates a script to run a "_incremental" .apk."""
|
||||
"""Creates a script to run an "_incremental" .apk."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
|
||||
from util import build_utils
|
||||
|
||||
from pylib import constants
|
||||
from util import build_utils
|
||||
|
||||
|
||||
SCRIPT_TEMPLATE = """\
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This file was generated by:
|
||||
# //build/android/gyp/create_incremental_install_script.py
|
||||
# //build/android/incremental_install/create_install_script.py
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
@ -47,7 +49,7 @@ if __name__ == '__main__':
|
|||
"""
|
||||
|
||||
|
||||
def main(args):
|
||||
def _ParseArgs(args):
|
||||
args = build_utils.ExpandFileArgs(args)
|
||||
parser = argparse.ArgumentParser()
|
||||
build_utils.AddDepfileOption(parser)
|
||||
|
@ -68,31 +70,49 @@ def main(args):
|
|||
'Can be specified multiple times.')
|
||||
parser.add_argument('--lib-dir',
|
||||
help='Path to native libraries directory.')
|
||||
parser.add_argument('--dex-file',
|
||||
action='append',
|
||||
default=[],
|
||||
dest='dex_files',
|
||||
help='List of dex files to include.')
|
||||
parser.add_argument('--dex-file-list',
|
||||
help='GYP-list of dex files.')
|
||||
|
||||
options = parser.parse_args(args)
|
||||
options.dex_files += build_utils.ParseGypList(options.dex_file_list)
|
||||
return options
|
||||
|
||||
|
||||
def main(args):
|
||||
options = _ParseArgs(args)
|
||||
|
||||
def relativize(path):
|
||||
return os.path.relpath(path, os.path.dirname(options.script_output_path))
|
||||
|
||||
incremental_install_path = os.path.join(constants.DIR_SOURCE_ROOT, 'build',
|
||||
'android', 'incremental_install.py')
|
||||
incremental_install_path = relativize(incremental_install_path)
|
||||
installer_path = os.path.join(constants.DIR_SOURCE_ROOT, 'build', 'android',
|
||||
'incremental_install', 'installer.py')
|
||||
installer_path = relativize(installer_path)
|
||||
|
||||
incremental_install_path_args = [
|
||||
path_args = [
|
||||
('--output-directory', relativize(options.output_directory)),
|
||||
(None, relativize(options.apk_path)),
|
||||
]
|
||||
|
||||
if options.lib_dir:
|
||||
incremental_install_path_args.append(
|
||||
('--lib-dir', relativize(options.lib_dir)))
|
||||
path_args.append(('--lib-dir', relativize(options.lib_dir)))
|
||||
|
||||
if options.dex_files:
|
||||
for dex_file in options.dex_files:
|
||||
path_args.append(('--dex-file', relativize(dex_file)))
|
||||
|
||||
for split_arg in options.splits:
|
||||
incremental_install_path_args.append(('--split', relativize(split_arg)))
|
||||
path_args.append(('--split', relativize(split_arg)))
|
||||
|
||||
with open(options.script_output_path, 'w') as script:
|
||||
script.write(SCRIPT_TEMPLATE.format(
|
||||
cmd_path=repr(incremental_install_path),
|
||||
cmd_path=pprint.pformat(installer_path),
|
||||
cmd_args='[]',
|
||||
cmd_path_args=repr(incremental_install_path_args)))
|
||||
cmd_path_args=pprint.pformat(path_args)))
|
||||
|
||||
os.chmod(options.script_output_path, 0750)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2015 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.
|
||||
"""Creates an AndroidManifest.xml for an incremental APK.
|
||||
|
||||
Given the manifest file for the real APK, generates an AndroidManifest.xml with
|
||||
the application class changed to IncrementalApplication.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from xml.etree import ElementTree
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir, 'gyp'))
|
||||
from util import build_utils
|
||||
|
||||
_ANDROID_NAMESPACE = 'http://schemas.android.com/apk/res/android'
|
||||
ElementTree.register_namespace('android', _ANDROID_NAMESPACE)
|
||||
|
||||
_INCREMENTAL_APP_NAME = 'org.chromium.incrementalinstall.BootstrapApplication'
|
||||
_META_DATA_NAME = 'incremental-install-real-app'
|
||||
|
||||
|
||||
def _AddNamespace(name):
|
||||
"""Adds the android namespace prefix to the given identifier."""
|
||||
return '{%s}%s' % (_ANDROID_NAMESPACE, name)
|
||||
|
||||
def _ParseArgs():
|
||||
parser = argparse.ArgumentParser()
|
||||
build_utils.AddDepfileOption(parser)
|
||||
parser.add_argument('--src-manifest',
|
||||
help='The main manifest of the app',
|
||||
required=True)
|
||||
parser.add_argument('--out-manifest',
|
||||
help='The output manifest',
|
||||
required=True)
|
||||
parser.add_argument('--disable-isolated-processes',
|
||||
help='Changes all android:isolatedProcess to false. '
|
||||
'This is required on Android M+',
|
||||
action='store_true')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _ProcessManifest(main_manifest, main_manifest_path,
|
||||
disable_isolated_processes):
|
||||
"""Returns a transformed AndroidManifest.xml for use with _incremental apks.
|
||||
|
||||
Args:
|
||||
main_manifest: Manifest contents to transform.
|
||||
main_manifest_path: Path to main_manifest (used for error messages).
|
||||
disable_isolated_processes: Whether to set all isolatedProcess attributes to
|
||||
false
|
||||
|
||||
Returns:
|
||||
The transformed AndroidManifest.xml.
|
||||
"""
|
||||
if disable_isolated_processes:
|
||||
main_manifest = main_manifest.replace('isolatedProcess="true"',
|
||||
'isolatedProcess="false"')
|
||||
|
||||
doc = ElementTree.fromstring(main_manifest)
|
||||
app_node = doc.find('application')
|
||||
if app_node is None:
|
||||
raise Exception('Could not find <application> in %s' % main_manifest_path)
|
||||
real_app_class = app_node.get(_AddNamespace('name'))
|
||||
if real_app_class is None:
|
||||
raise Exception('Could not find android:name in <application> in %s' %
|
||||
main_manifest_path)
|
||||
app_node.set(_AddNamespace('name'), _INCREMENTAL_APP_NAME)
|
||||
|
||||
meta_data_node = ElementTree.SubElement(app_node, 'meta-data')
|
||||
meta_data_node.set(_AddNamespace('name'), _META_DATA_NAME)
|
||||
meta_data_node.set(_AddNamespace('value'), real_app_class)
|
||||
return ElementTree.tostring(doc, encoding='UTF-8')
|
||||
|
||||
|
||||
def main():
|
||||
options = _ParseArgs()
|
||||
with open(options.src_manifest) as f:
|
||||
main_manifest_data = f.read()
|
||||
new_manifest_data = _ProcessManifest(main_manifest_data, options.src_manifest,
|
||||
options.disable_isolated_processes)
|
||||
with open(options.out_manifest, 'w') as f:
|
||||
f.write(new_manifest_data)
|
||||
|
||||
if options.depfile:
|
||||
build_utils.WriteDepfile(
|
||||
options.depfile,
|
||||
[options.src_manifest] + build_utils.GetPythonDependencies())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -9,20 +9,32 @@
|
|||
import argparse
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
from devil.android import apk_helper
|
||||
from devil.android import device_utils
|
||||
from devil.android import device_errors
|
||||
from devil.android.sdk import version_codes
|
||||
from devil.utils import reraiser_thread
|
||||
from pylib import constants
|
||||
from pylib.utils import run_tests_helper
|
||||
from pylib.utils import time_profile
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
|
||||
from util import build_utils
|
||||
|
||||
|
||||
def _TransformDexPaths(paths):
|
||||
"""Given paths like ["/a/b/c", "/a/c/d"], returns ["b.c", "c.d"]."""
|
||||
prefix_len = len(os.path.commonprefix(paths))
|
||||
return [p[prefix_len:].replace(os.sep, '.') for p in paths]
|
||||
|
||||
|
||||
def main():
|
||||
start_time = time.time()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('apk_path',
|
||||
help='The path to the APK to install.')
|
||||
|
@ -33,6 +45,10 @@ def main():
|
|||
'Can be specified multiple times.')
|
||||
parser.add_argument('--lib-dir',
|
||||
help='Path to native libraries directory.')
|
||||
parser.add_argument('--dex-files',
|
||||
help='List of dex files to push.',
|
||||
action='append',
|
||||
default=[])
|
||||
parser.add_argument('-d', '--device', dest='device',
|
||||
help='Target device for apk to install on.')
|
||||
parser.add_argument('--uninstall',
|
||||
|
@ -54,12 +70,16 @@ def main():
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(format='%(asctime)s (%(thread)d) %(message)s')
|
||||
run_tests_helper.SetLogLevel(args.verbose_count)
|
||||
constants.SetBuildType('Debug')
|
||||
if args.output_directory:
|
||||
constants.SetOutputDirectory(args.output_directory)
|
||||
|
||||
main_timer = time_profile.TimeProfile()
|
||||
install_timer = time_profile.TimeProfile()
|
||||
push_native_timer = time_profile.TimeProfile()
|
||||
push_dex_timer = time_profile.TimeProfile()
|
||||
|
||||
if args.device:
|
||||
# Retries are annoying when commands fail for legitimate reasons. Might want
|
||||
# to enable them if this is ever used on bots though.
|
||||
|
@ -80,19 +100,26 @@ def main():
|
|||
msg += ' %s (%s)\n' % (d, desc)
|
||||
raise Exception(msg)
|
||||
|
||||
apk_package = apk_helper.ApkHelper(args.apk_path).GetPackageName()
|
||||
apk_help = apk_helper.ApkHelper(args.apk_path)
|
||||
apk_package = apk_help.GetPackageName()
|
||||
device_incremental_dir = '/data/local/tmp/incremental-app-%s' % apk_package
|
||||
|
||||
if args.uninstall:
|
||||
logging.info('Uninstalling .apk')
|
||||
device.Uninstall(apk_package)
|
||||
logging.info('Removing side-loaded files')
|
||||
device.RunShellCommand(['rm', '-rf', device_incremental_dir],
|
||||
check_return=True)
|
||||
logging.info('Uninstall took %s seconds.', main_timer.GetDelta())
|
||||
return
|
||||
|
||||
if device.build_version_sdk >= version_codes.MARSHMALLOW:
|
||||
if apk_help.HasIsolatedProcesses():
|
||||
raise Exception('Cannot use perform incremental installs on Android M+ '
|
||||
'without first disabling isolated processes. Use GN arg: '
|
||||
'disable_incremental_isolated_processes=true to do so.')
|
||||
|
||||
# Install .apk(s) if any of them have changed.
|
||||
def do_install():
|
||||
install_timer.Start()
|
||||
if args.splits:
|
||||
splits = []
|
||||
for split_glob in args.splits:
|
||||
|
@ -101,25 +128,60 @@ def main():
|
|||
allow_cached_props=True)
|
||||
else:
|
||||
device.Install(args.apk_path, reinstall=True)
|
||||
logging.info('Finished installing .apk')
|
||||
install_timer.Stop(log=False)
|
||||
|
||||
# Push .so files to the device (if they have changed).
|
||||
def do_push_libs():
|
||||
# Push .so and .dex files to the device (if they have changed).
|
||||
def do_push_files():
|
||||
if args.lib_dir:
|
||||
push_native_timer.Start()
|
||||
device_lib_dir = posixpath.join(device_incremental_dir, 'lib')
|
||||
device.PushChangedFiles([(args.lib_dir, device_lib_dir)],
|
||||
delete_device_stale=True)
|
||||
logging.info('Finished pushing native libs')
|
||||
push_native_timer.Stop(log=False)
|
||||
|
||||
if args.dex_files:
|
||||
push_dex_timer.Start()
|
||||
# Put all .dex files to be pushed into a temporary directory so that we
|
||||
# can use delete_device_stale=True.
|
||||
with build_utils.TempDir() as temp_dir:
|
||||
device_dex_dir = posixpath.join(device_incremental_dir, 'dex')
|
||||
# Ensure no two files have the same name.
|
||||
transformed_names = _TransformDexPaths(args.dex_files)
|
||||
for src_path, dest_name in zip(args.dex_files, transformed_names):
|
||||
shutil.copyfile(src_path, os.path.join(temp_dir, dest_name))
|
||||
device.PushChangedFiles([(temp_dir, device_dex_dir)],
|
||||
delete_device_stale=True)
|
||||
push_dex_timer.Stop(log=False)
|
||||
|
||||
# Create 2 lock files:
|
||||
# * install.lock tells the app to pause on start-up (until we release it).
|
||||
# * firstrun.lock is used by the app to pause all secondary processes until
|
||||
# the primary process finishes loading the .dex / .so files.
|
||||
def create_lock_files():
|
||||
# Creates or zeros out lock files.
|
||||
cmd = ('D="%s";'
|
||||
'mkdir -p $D &&'
|
||||
'echo -n >$D/install.lock 2>$D/firstrun.lock')
|
||||
device.RunShellCommand(cmd % device_incremental_dir, check_return=True)
|
||||
|
||||
# The firstrun.lock is released by the app itself.
|
||||
def release_installer_lock():
|
||||
device.RunShellCommand('echo > %s/install.lock' % device_incremental_dir,
|
||||
check_return=True)
|
||||
|
||||
create_lock_files()
|
||||
# Concurrency here speeds things up quite a bit, but DeviceUtils hasn't
|
||||
# been designed for multi-threading. Enabling only because this is a
|
||||
# developer-only tool.
|
||||
if args.no_threading:
|
||||
do_install()
|
||||
do_push_libs()
|
||||
do_push_files()
|
||||
else:
|
||||
reraiser_thread.RunAsync((do_install, do_push_libs))
|
||||
logging.info('Took %s seconds', round(time.time() - start_time, 1))
|
||||
reraiser_thread.RunAsync((do_install, do_push_files))
|
||||
release_installer_lock()
|
||||
logging.info('Took %s seconds (install=%s, libs=%s, dex=%s)',
|
||||
main_timer.GetDelta(), install_timer.GetDelta(),
|
||||
push_native_timer.GetDelta(), push_dex_timer.GetDelta())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
package org.chromium.incrementalinstall;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An Application that replaces itself with another Application (as defined in
|
||||
* the "incremental-install-real-app" meta-data tag within the
|
||||
* AndroidManifest.xml). It loads the other application only after side-loading
|
||||
* its .so and .dex files from /data/local/tmp.
|
||||
*/
|
||||
public final class BootstrapApplication extends Application {
|
||||
private static final String TAG = "cr.incrementalinstall";
|
||||
private static final String MANAGED_DIR_PREFIX = "/data/local/tmp/incremental-app-";
|
||||
private static final String REAL_APP_META_DATA_NAME = "incremental-install-real-app";
|
||||
|
||||
private ClassLoaderPatcher mClassLoaderPatcher;
|
||||
private Application mRealApplication;
|
||||
private Object mStashedProviderList;
|
||||
private Object mActivityThread;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context context) {
|
||||
super.attachBaseContext(context);
|
||||
File incrementalRootDir = new File(MANAGED_DIR_PREFIX + context.getPackageName());
|
||||
File libDir = new File(incrementalRootDir, "lib");
|
||||
File dexDir = new File(incrementalRootDir, "dex");
|
||||
File installLockFile = new File(incrementalRootDir, "install.lock");
|
||||
File firstRunLockFile = new File(incrementalRootDir, "firstrun.lock");
|
||||
|
||||
try {
|
||||
mActivityThread = Reflect.invokeMethod(Class.forName("android.app.ActivityThread"),
|
||||
"currentActivityThread");
|
||||
mClassLoaderPatcher = new ClassLoaderPatcher(context);
|
||||
|
||||
boolean isFirstRun = LockFile.installerLockExists(firstRunLockFile);
|
||||
if (isFirstRun) {
|
||||
if (mClassLoaderPatcher.mIsPrimaryProcess) {
|
||||
// Wait for incremental_install.py to finish.
|
||||
LockFile.waitForInstallerLock(installLockFile, 20 * 1000);
|
||||
} else {
|
||||
// Wait for the browser process to create the optimized dex files
|
||||
// (and for M+, copy the library files).
|
||||
LockFile.waitForInstallerLock(firstRunLockFile, 30 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
mClassLoaderPatcher.importNativeLibs(libDir);
|
||||
mClassLoaderPatcher.loadDexFiles(dexDir);
|
||||
|
||||
if (isFirstRun && mClassLoaderPatcher.mIsPrimaryProcess) {
|
||||
LockFile.clearInstallerLock(firstRunLockFile);
|
||||
}
|
||||
|
||||
// attachBaseContext() is called from ActivityThread#handleBindApplication() and
|
||||
// Application#mApplication is changed right after we return. Thus, we cannot swap
|
||||
// the Application instances until onCreate() is called.
|
||||
String realApplicationName = getRealApplicationName();
|
||||
Log.i(TAG, "Instantiating " + realApplicationName);
|
||||
mRealApplication =
|
||||
(Application) Reflect.newInstance(Class.forName(realApplicationName));
|
||||
Reflect.invokeMethod(mRealApplication, "attachBaseContext", context);
|
||||
|
||||
// Between attachBaseContext() and onCreate(), ActivityThread tries to instantiate
|
||||
// all ContentProviders. The ContentProviders break without the correct Application
|
||||
// class being installed, so temporarily pretend there are no providers, and then
|
||||
// instantiate them explicitly within onCreate().
|
||||
disableContentProviders();
|
||||
Log.i(TAG, "Waiting for onCreate");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Incremental install failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
try {
|
||||
Log.i(TAG, "onCreate() called. Swapping Application references");
|
||||
swapApplicationReferences();
|
||||
enableContentProviders();
|
||||
Log.i(TAG, "Calling onCreate");
|
||||
mRealApplication.onCreate();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Incremental install failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class name of the real Application class (recorded in the
|
||||
* AndroidManifest.xml)
|
||||
*/
|
||||
private String getRealApplicationName() throws NameNotFoundException {
|
||||
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(),
|
||||
PackageManager.GET_META_DATA);
|
||||
return appInfo.metaData.getString(REAL_APP_META_DATA_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nulls out ActivityThread.mBoundApplication.providers.
|
||||
*/
|
||||
private void disableContentProviders() throws ReflectiveOperationException {
|
||||
Object data = Reflect.getField(mActivityThread, "mBoundApplication");
|
||||
mStashedProviderList = Reflect.getField(data, "providers");
|
||||
Reflect.setField(data, "providers", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the value of ActivityThread.mBoundApplication.providers, and invokes
|
||||
* ActivityThread#installContentProviders().
|
||||
*/
|
||||
private void enableContentProviders() throws ReflectiveOperationException {
|
||||
Object data = Reflect.getField(mActivityThread, "mBoundApplication");
|
||||
Reflect.setField(data, "providers", mStashedProviderList);
|
||||
if (mStashedProviderList != null && mClassLoaderPatcher.mIsPrimaryProcess) {
|
||||
Log.i(TAG, "Instantiating content providers");
|
||||
Reflect.invokeMethod(mActivityThread, "installContentProviders", mRealApplication,
|
||||
mStashedProviderList);
|
||||
}
|
||||
mStashedProviderList = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes all fields within framework classes that have stored an reference to this
|
||||
* BootstrapApplication to instead store references to mRealApplication.
|
||||
* @throws NoSuchFieldException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void swapApplicationReferences() throws ReflectiveOperationException {
|
||||
if (Reflect.getField(mActivityThread, "mInitialApplication") == this) {
|
||||
Reflect.setField(mActivityThread, "mInitialApplication", mRealApplication);
|
||||
}
|
||||
|
||||
List<Application> allApplications =
|
||||
(List<Application>) Reflect.getField(mActivityThread, "mAllApplications");
|
||||
for (int i = 0; i < allApplications.size(); i++) {
|
||||
if (allApplications.get(i) == this) {
|
||||
allApplications.set(i, mRealApplication);
|
||||
}
|
||||
}
|
||||
|
||||
for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) {
|
||||
Map<String, WeakReference<?>> packageMap =
|
||||
(Map<String, WeakReference<?>>) Reflect.getField(mActivityThread, fieldName);
|
||||
for (Map.Entry<String, WeakReference<?>> entry : packageMap.entrySet()) {
|
||||
Object loadedApk = entry.getValue().get();
|
||||
if (loadedApk != null && Reflect.getField(loadedApk, "mApplication") == this) {
|
||||
Reflect.setField(loadedApk, "mApplication", mRealApplication);
|
||||
Reflect.setField(mRealApplication, "mLoadedApk", loadedApk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
package org.chromium.incrementalinstall;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides the ability to add native libraries and .dex files to an existing class loader.
|
||||
* Tested with Jellybean MR2 - Marshmellow.
|
||||
*/
|
||||
final class ClassLoaderPatcher {
|
||||
private static final String TAG = "cr.incrementalinstall";
|
||||
private final File mAppFilesSubDir;
|
||||
private final ClassLoader mClassLoader;
|
||||
private final Object mLibcoreOs;
|
||||
private final int mProcessUid;
|
||||
final boolean mIsPrimaryProcess;
|
||||
|
||||
ClassLoaderPatcher(Context context) throws ReflectiveOperationException {
|
||||
mAppFilesSubDir =
|
||||
new File(context.getApplicationInfo().dataDir, "incremental-install-files");
|
||||
mClassLoader = context.getClassLoader();
|
||||
mLibcoreOs = Reflect.getField(Class.forName("libcore.io.Libcore"), "os");
|
||||
mProcessUid = (Integer) Reflect.invokeMethod(mLibcoreOs, "getuid");
|
||||
mIsPrimaryProcess = context.getApplicationInfo().uid == mProcessUid;
|
||||
Log.i(TAG, "uid=" + mProcessUid + " (isPrimary=" + mIsPrimaryProcess + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all dex files within |dexDir| into the app's ClassLoader.
|
||||
*/
|
||||
void loadDexFiles(File dexDir) throws ReflectiveOperationException, FileNotFoundException {
|
||||
Log.i(TAG, "Installing dex files from: " + dexDir);
|
||||
File[] dexFilesArr = dexDir.listFiles();
|
||||
if (dexFilesArr == null) {
|
||||
throw new FileNotFoundException("Dex dir does not exist: " + dexDir);
|
||||
}
|
||||
// The optimized dex files will be owned by this process' user.
|
||||
// Store them within the app's data dir rather than on /data/local/tmp
|
||||
// so that they are still deleted (by the OS) when we uninstall
|
||||
// (even on a non-rooted device).
|
||||
File incrementalDexesDir = new File(mAppFilesSubDir, "optimized-dexes");
|
||||
File isolatedDexesDir = new File(mAppFilesSubDir, "isolated-dexes");
|
||||
File optimizedDir;
|
||||
|
||||
if (mIsPrimaryProcess) {
|
||||
ensureAppFilesSubDirExists();
|
||||
// Allows isolated processes to access the same files.
|
||||
incrementalDexesDir.mkdir();
|
||||
incrementalDexesDir.setReadable(true, false);
|
||||
incrementalDexesDir.setExecutable(true, false);
|
||||
// Create a directory for isolated processes to create directories in.
|
||||
isolatedDexesDir.mkdir();
|
||||
isolatedDexesDir.setWritable(true, false);
|
||||
isolatedDexesDir.setExecutable(true, false);
|
||||
|
||||
optimizedDir = incrementalDexesDir;
|
||||
} else {
|
||||
// There is a UID check of the directory in dalvik.system.DexFile():
|
||||
// https://android.googlesource.com/platform/libcore/+/45e0260/dalvik/src/main/java/dalvik/system/DexFile.java#101
|
||||
// Rather than have each isolated process run DexOpt though, we use
|
||||
// symlinks within the directory to point at the browser process'
|
||||
// optimized dex files.
|
||||
optimizedDir = new File(isolatedDexesDir, "isolated-" + mProcessUid);
|
||||
optimizedDir.mkdir();
|
||||
// Always wipe it out and re-create for simplicity.
|
||||
Log.i(TAG, "Creating dex file symlinks for isolated process");
|
||||
for (File f : optimizedDir.listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
for (File f : incrementalDexesDir.listFiles()) {
|
||||
String to = "../../" + incrementalDexesDir.getName() + "/" + f.getName();
|
||||
File from = new File(optimizedDir, f.getName());
|
||||
createSymlink(to, from);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Code cache dir: " + optimizedDir);
|
||||
// TODO(agrieve): Might need to record classpath ordering if we ever have duplicate
|
||||
// class names (since then order will matter here).
|
||||
Log.i(TAG, "Loading " + dexFilesArr.length + " dex files");
|
||||
|
||||
Object dexPathList = Reflect.getField(mClassLoader, "pathList");
|
||||
Object[] dexElements = (Object[]) Reflect.getField(dexPathList, "dexElements");
|
||||
Object[] additionalElements = makeDexElements(dexFilesArr, optimizedDir);
|
||||
Reflect.setField(
|
||||
dexPathList, "dexElements", Reflect.concatArrays(dexElements, additionalElements));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up all libraries within |libDir| to be loadable by System.loadLibrary().
|
||||
*/
|
||||
void importNativeLibs(File libDir) throws ReflectiveOperationException, IOException {
|
||||
Log.i(TAG, "Importing native libraries from: " + libDir);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
libDir = prepareNativeLibsAndroidM(libDir);
|
||||
}
|
||||
addNativeLibrarySearchPath(libDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary process: Copies native libraries into the app's data directory
|
||||
* Other processes: Waits for primary process to finish copying.
|
||||
*/
|
||||
private File prepareNativeLibsAndroidM(File libDir) throws IOException {
|
||||
File localLibsDir = new File(mAppFilesSubDir, "lib");
|
||||
File copyLibsLockFile = new File(mAppFilesSubDir, "libcopy.lock");
|
||||
// Due to a new SELinux policy, all libs must be copied into the app's
|
||||
// data directory first.
|
||||
// https://code.google.com/p/android/issues/detail?id=79480
|
||||
if (mIsPrimaryProcess) {
|
||||
LockFile lockFile = LockFile.acquireRuntimeLock(copyLibsLockFile);
|
||||
if (lockFile == null) {
|
||||
LockFile.waitForRuntimeLock(copyLibsLockFile, 10 * 1000);
|
||||
} else {
|
||||
try {
|
||||
ensureAppFilesSubDirExists();
|
||||
localLibsDir.mkdir();
|
||||
localLibsDir.setReadable(true, false);
|
||||
localLibsDir.setExecutable(true, false);
|
||||
copyChangedFiles(libDir, localLibsDir);
|
||||
} finally {
|
||||
lockFile.release();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: Work around this issue by using APK splits to install each dex / lib.
|
||||
throw new RuntimeException("Incremental install does not work on Android M+ "
|
||||
+ "with isolated processes. Use the gn arg:\n"
|
||||
+ " disable_incremental_isolated_processes=true\n"
|
||||
+ "and try again.");
|
||||
}
|
||||
return localLibsDir;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void addNativeLibrarySearchPath(File nativeLibDir) throws ReflectiveOperationException {
|
||||
Object dexPathList = Reflect.getField(mClassLoader, "pathList");
|
||||
Object currentDirs = Reflect.getField(dexPathList, "nativeLibraryDirectories");
|
||||
File[] newDirs = new File[] { nativeLibDir };
|
||||
// Switched from an array to an ArrayList in Lollipop.
|
||||
if (currentDirs instanceof List) {
|
||||
List<File> dirsAsList = (List<File>) currentDirs;
|
||||
dirsAsList.add(nativeLibDir);
|
||||
} else {
|
||||
File[] dirsAsArray = (File[]) currentDirs;
|
||||
Reflect.setField(dexPathList, "nativeLibraryDirectories",
|
||||
Reflect.concatArrays(dirsAsArray, newDirs));
|
||||
}
|
||||
|
||||
Object[] nativeLibraryPathElements;
|
||||
try {
|
||||
nativeLibraryPathElements =
|
||||
(Object[]) Reflect.getField(dexPathList, "nativeLibraryPathElements");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// This field doesn't exist pre-M.
|
||||
return;
|
||||
}
|
||||
Object[] additionalElements = makeNativePathElements(newDirs);
|
||||
Reflect.setField(
|
||||
dexPathList, "nativeLibraryPathElements",
|
||||
Reflect.concatArrays(nativeLibraryPathElements, additionalElements));
|
||||
}
|
||||
|
||||
private static void copyChangedFiles(File srcDir, File dstDir) throws IOException {
|
||||
// No need to delete stale libs since libraries are loaded explicitly.
|
||||
for (File f : srcDir.listFiles()) {
|
||||
// Note: Tried using hardlinks, but resulted in EACCES exceptions.
|
||||
File dest = new File(dstDir, f.getName());
|
||||
copyIfModified(f, dest);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyIfModified(File src, File dest) throws IOException {
|
||||
long lastModified = src.lastModified();
|
||||
if (!dest.exists() || dest.lastModified() != lastModified) {
|
||||
Log.i(TAG, "Copying " + src + " -> " + dest);
|
||||
FileInputStream istream = new FileInputStream(src);
|
||||
FileOutputStream ostream = new FileOutputStream(dest);
|
||||
ostream.getChannel().transferFrom(istream.getChannel(), 0, istream.getChannel().size());
|
||||
istream.close();
|
||||
ostream.close();
|
||||
dest.setReadable(true, false);
|
||||
dest.setExecutable(true, false);
|
||||
dest.setLastModified(lastModified);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAppFilesSubDirExists() {
|
||||
mAppFilesSubDir.mkdir();
|
||||
mAppFilesSubDir.setExecutable(true, false);
|
||||
}
|
||||
|
||||
private void createSymlink(String to, File from) throws ReflectiveOperationException {
|
||||
Reflect.invokeMethod(mLibcoreOs, "symlink", to, from.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static Object[] makeNativePathElements(File[] paths)
|
||||
throws ReflectiveOperationException {
|
||||
Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element");
|
||||
Object[] entries = new Object[paths.length];
|
||||
for (int i = 0; i < paths.length; ++i) {
|
||||
entries[i] = Reflect.newInstance(entryClazz, paths[i], true, null, null);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static Object[] makeDexElements(File[] files, File optimizedDirectory)
|
||||
throws ReflectiveOperationException {
|
||||
Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element");
|
||||
Class<?> clazz = Class.forName("dalvik.system.DexPathList");
|
||||
Object[] entries = new Object[files.length];
|
||||
File emptyDir = new File("");
|
||||
for (int i = 0; i < files.length; ++i) {
|
||||
File file = files[i];
|
||||
Object dexFile = Reflect.invokeMethod(clazz, "loadDexFile", file, optimizedDirectory);
|
||||
entries[i] = Reflect.newInstance(entryClazz, emptyDir, false, file, dexFile);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
package org.chromium.incrementalinstall;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Helpers for dealing with .lock files used during install / first run.
|
||||
*/
|
||||
final class LockFile {
|
||||
private static final String TAG = "cr.incrementalinstall";
|
||||
|
||||
private final File mFile;
|
||||
private final FileOutputStream mOutputStream;
|
||||
private final FileLock mFileLock;
|
||||
|
||||
private LockFile(File file, FileOutputStream outputStream, FileLock fileLock) {
|
||||
mFile = file;
|
||||
mOutputStream = outputStream;
|
||||
mFileLock = fileLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the lock file by writing to it (making it non-zero in length);
|
||||
*/
|
||||
static void clearInstallerLock(File lockFile) throws IOException {
|
||||
Log.i(TAG, "Clearing " + lockFile);
|
||||
// On Android M+, we can't delete files in /data/local/tmp, so we write to it instead.
|
||||
FileOutputStream os = new FileOutputStream(lockFile);
|
||||
os.write(1);
|
||||
os.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given file to be non-zero in length.
|
||||
*/
|
||||
static void waitForInstallerLock(final File file, long timeoutMs) {
|
||||
pollingWait(new Callable<Boolean>() {
|
||||
@Override public Boolean call() {
|
||||
return !installerLockExists(file);
|
||||
}
|
||||
}, file, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given file to be non-zero in length.
|
||||
*/
|
||||
private static void pollingWait(Callable<Boolean> func, File file, long timeoutMs) {
|
||||
long pollIntervalMs = 200;
|
||||
for (int i = 0; i < timeoutMs / pollIntervalMs; i++) {
|
||||
try {
|
||||
if (func.call()) {
|
||||
if (i > 0) {
|
||||
Log.i(TAG, "Finished waiting on lock file: " + file);
|
||||
}
|
||||
return;
|
||||
} else if (i == 0) {
|
||||
Log.i(TAG, "Waiting on lock file: " + file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(pollIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
// Should never happen.
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Timed out waiting for lock file: " + file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given lock file is missing or is in the locked state.
|
||||
*/
|
||||
static boolean installerLockExists(File file) {
|
||||
return !file.exists() || file.length() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to acquire a lock for the given file.
|
||||
* @return Returns the FileLock if it was acquired, or null otherwise.
|
||||
*/
|
||||
static LockFile acquireRuntimeLock(File file) {
|
||||
try {
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
FileLock lock = outputStream.getChannel().tryLock();
|
||||
if (lock != null) {
|
||||
Log.i(TAG, "Created lock file: " + file);
|
||||
return new LockFile(file, outputStream, lock);
|
||||
}
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
// Do nothing. We didn't get the lock.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given file to not exist.
|
||||
*/
|
||||
static void waitForRuntimeLock(final File file, long timeoutMs) {
|
||||
pollingWait(new Callable<Boolean>() {
|
||||
@Override public Boolean call() {
|
||||
return !file.exists();
|
||||
}
|
||||
}, file, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases and deletes the lock file.
|
||||
*/
|
||||
void release() throws IOException {
|
||||
Log.i(TAG, "Deleting lock file: " + mFile);
|
||||
mFileLock.release();
|
||||
mOutputStream.close();
|
||||
if (!mFile.delete()) {
|
||||
throw new IOException("Failed to delete lock file: " + mFile);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
package org.chromium.incrementalinstall;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Reflection helper methods.
|
||||
*/
|
||||
final class Reflect {
|
||||
/**
|
||||
* Sets the value of an object's field (even if it's not visible).
|
||||
*
|
||||
* @param instance The object containing the field to set.
|
||||
* @param name The name of the field to set.
|
||||
* @param value The new value for the field.
|
||||
*/
|
||||
static void setField(Object instance, String name, Object value)
|
||||
throws ReflectiveOperationException {
|
||||
Field field = findField(instance, name);
|
||||
field.setAccessible(true);
|
||||
field.set(instance, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of an object's field (even if it's not visible).
|
||||
*
|
||||
* @param instance The object containing the field to set.
|
||||
* @param name The name of the field to set.
|
||||
* @return The field's value. Primitive values are returned as their boxed
|
||||
* type.
|
||||
*/
|
||||
static Object getField(Object instance, String name) throws ReflectiveOperationException {
|
||||
Field field = findField(instance, name);
|
||||
field.setAccessible(true);
|
||||
return field.get(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates two arrays into a new array. The arrays must be of the same
|
||||
* type.
|
||||
*/
|
||||
static Object[] concatArrays(Object[] left, Object[] right) {
|
||||
Object[] result = (Object[]) Array.newInstance(
|
||||
left.getClass().getComponentType(), left.length + right.length);
|
||||
System.arraycopy(left, 0, result, 0, left.length);
|
||||
System.arraycopy(right, 0, result, left.length, right.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a method with zero or more parameters. For static methods, use the Class as the
|
||||
* instance.
|
||||
*/
|
||||
static Object invokeMethod(Object instance, String name, Object... params)
|
||||
throws ReflectiveOperationException {
|
||||
boolean isStatic = instance instanceof Class;
|
||||
Class<?> clazz = isStatic ? (Class<?>) instance : instance.getClass();
|
||||
Method method = findMethod(clazz, name, params);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(instance, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a constructor with zero or more parameters.
|
||||
*/
|
||||
static Object newInstance(Class<?> clazz, Object... params)
|
||||
throws ReflectiveOperationException {
|
||||
Constructor<?> constructor = findConstructor(clazz, params);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(params);
|
||||
}
|
||||
|
||||
private static Field findField(Object instance, String name) throws NoSuchFieldException {
|
||||
boolean isStatic = instance instanceof Class;
|
||||
Class<?> clazz = isStatic ? (Class<?>) instance : instance.getClass();
|
||||
for (; clazz != null; clazz = clazz.getSuperclass()) {
|
||||
try {
|
||||
return clazz.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// Need to look in the super class.
|
||||
}
|
||||
}
|
||||
throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?> clazz, String name, Object... params)
|
||||
throws NoSuchMethodException {
|
||||
for (; clazz != null; clazz = clazz.getSuperclass()) {
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (method.getName().equals(name)
|
||||
&& areParametersCompatible(method.getParameterTypes(), params)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new NoSuchMethodException("Method " + name + " with parameters "
|
||||
+ Arrays.asList(params) + " not found in " + clazz);
|
||||
}
|
||||
|
||||
private static Constructor<?> findConstructor(Class<?> clazz, Object... params)
|
||||
throws NoSuchMethodException {
|
||||
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
|
||||
if (areParametersCompatible(constructor.getParameterTypes(), params)) {
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
throw new NoSuchMethodException("Constructor with parameters " + Arrays.asList(params)
|
||||
+ " not found in " + clazz);
|
||||
}
|
||||
|
||||
private static boolean areParametersCompatible(Class<?>[] paramTypes, Object... params) {
|
||||
if (params.length != paramTypes.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
if (!isAssignableFrom(paramTypes[i], params[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isAssignableFrom(Class<?> left, Object right) {
|
||||
if (right == null) {
|
||||
return !left.isPrimitive();
|
||||
}
|
||||
Class<?> rightClazz = right.getClass();
|
||||
if (left.isPrimitive()) {
|
||||
// TODO(agrieve): Fill in the rest as needed.
|
||||
return left == boolean.class && rightClazz == Boolean.class
|
||||
|| left == int.class && rightClazz == Integer.class;
|
||||
}
|
||||
return left.isAssignableFrom(rightClazz);
|
||||
}
|
||||
}
|
|
@ -9,18 +9,37 @@ import time
|
|||
class TimeProfile(object):
|
||||
"""Class for simple profiling of action, with logging of cost."""
|
||||
|
||||
def __init__(self, description):
|
||||
def __init__(self, description='operation'):
|
||||
self._starttime = None
|
||||
self._endtime = None
|
||||
self._description = description
|
||||
self.Start()
|
||||
|
||||
def Start(self):
|
||||
self._starttime = time.time()
|
||||
self._endtime = None
|
||||
|
||||
def Stop(self):
|
||||
"""Stop profiling and dump a log."""
|
||||
if self._starttime:
|
||||
stoptime = time.time()
|
||||
logging.info('%fsec to perform %s',
|
||||
stoptime - self._starttime, self._description)
|
||||
self._starttime = None
|
||||
def GetDelta(self):
|
||||
"""Returns the rounded delta.
|
||||
|
||||
Also stops the timer if Stop() has not already been called.
|
||||
"""
|
||||
if self._endtime is None:
|
||||
self.Stop(log=False)
|
||||
delta = self._endtime - self._starttime
|
||||
delta = round(delta, 2) if delta < 10 else round(delta, 1)
|
||||
return delta
|
||||
|
||||
def LogResult(self):
|
||||
"""Logs the result."""
|
||||
logging.info('%s seconds to perform %s', self.GetDelta(), self._description)
|
||||
|
||||
def Stop(self, log=True):
|
||||
"""Stop profiling.
|
||||
|
||||
Args:
|
||||
log: Log the delta (defaults to true).
|
||||
"""
|
||||
self._endtime = time.time()
|
||||
if log:
|
||||
self.LogResult()
|
||||
|
|
|
@ -65,6 +65,10 @@ if (is_android) {
|
|||
|
||||
# Set to true to enable the Errorprone compiler
|
||||
use_errorprone_java_compiler = false
|
||||
|
||||
# Disables process isolation when building _incremental targets.
|
||||
# Required for Android M+ due to SELinux policies (stronger sandboxing).
|
||||
disable_incremental_isolated_processes = false
|
||||
}
|
||||
|
||||
# Host stuff -----------------------------------------------------------------
|
||||
|
|
|
@ -480,7 +480,6 @@ template("package_apk") {
|
|||
forward_variables_from(invoker,
|
||||
[
|
||||
"deps",
|
||||
"data_deps",
|
||||
"public_deps",
|
||||
"testonly",
|
||||
])
|
||||
|
@ -488,6 +487,7 @@ template("package_apk") {
|
|||
_ant_script = "//build/android/ant/apk-package.xml"
|
||||
|
||||
depfile = "$target_gen_dir/$target_name.d"
|
||||
data_deps = [ "//tools/android/md5sum" ] # Used when deploying APKs
|
||||
|
||||
inputs = [
|
||||
invoker.resource_packaged_apk_path,
|
||||
|
@ -616,15 +616,16 @@ template("create_apk") {
|
|||
|
||||
if (defined(invoker.resources_zip)) {
|
||||
_resources_zip = invoker.resources_zip
|
||||
assert(_resources_zip != "") # Mark as used.
|
||||
}
|
||||
if (defined(invoker.dex_path)) {
|
||||
_dex_path = invoker.dex_path
|
||||
}
|
||||
_load_library_from_apk = invoker.load_library_from_apk
|
||||
|
||||
_package_deps = []
|
||||
_deps = []
|
||||
if (defined(invoker.deps)) {
|
||||
_package_deps = invoker.deps
|
||||
_deps = invoker.deps
|
||||
}
|
||||
|
||||
_native_libs_dir = "//build/android/empty/res"
|
||||
|
@ -634,19 +635,24 @@ template("create_apk") {
|
|||
|
||||
if (defined(invoker.asset_location)) {
|
||||
_asset_location = invoker.asset_location
|
||||
assert(_asset_location != "") # Mark as used.
|
||||
}
|
||||
|
||||
_version_code = invoker.version_code
|
||||
_version_name = invoker.version_name
|
||||
assert(_version_code != -1) # Mark as used.
|
||||
assert(_version_name != "") # Mark as used.
|
||||
|
||||
_base_apk_path = _base_path + ".apk_intermediates"
|
||||
|
||||
_resource_packaged_apk_path = _base_apk_path + ".ap_"
|
||||
_incremental_resource_packaged_apk_path = _base_apk_path + "_incremental.ap_"
|
||||
_packaged_apk_path = _base_apk_path + ".unfinished.apk"
|
||||
_incremental_packaged_apk_path =
|
||||
_base_apk_path + "_incremental.unfinished.apk"
|
||||
_shared_resources =
|
||||
defined(invoker.shared_resources) && invoker.shared_resources
|
||||
assert(_shared_resources || true) # Mark as used.
|
||||
|
||||
_keystore_path = invoker.keystore_path
|
||||
_keystore_name = invoker.keystore_name
|
||||
|
@ -668,83 +674,129 @@ template("create_apk") {
|
|||
_split_languages = invoker.language_splits
|
||||
}
|
||||
|
||||
_package_resources_target_name = "${target_name}__package_resources"
|
||||
action(_package_resources_target_name) {
|
||||
deps = _package_deps
|
||||
template("package_resources_helper") {
|
||||
action(target_name) {
|
||||
deps = invoker.deps
|
||||
|
||||
script = "//build/android/gyp/package_resources.py"
|
||||
script = "//build/android/gyp/package_resources.py"
|
||||
depfile = "${target_gen_dir}/${target_name}.d"
|
||||
inputs = [
|
||||
invoker.android_manifest,
|
||||
]
|
||||
if (defined(_resources_zip)) {
|
||||
inputs += [ _resources_zip ]
|
||||
}
|
||||
outputs = [
|
||||
depfile,
|
||||
invoker.resource_packaged_apk_path,
|
||||
]
|
||||
|
||||
args = [
|
||||
"--depfile",
|
||||
rebase_path(depfile, root_build_dir),
|
||||
"--android-sdk",
|
||||
rebased_android_sdk,
|
||||
"--aapt-path",
|
||||
android_aapt_path,
|
||||
"--configuration-name=$android_configuration_name",
|
||||
"--android-manifest",
|
||||
rebase_path(invoker.android_manifest, root_build_dir),
|
||||
"--version-code",
|
||||
_version_code,
|
||||
"--version-name",
|
||||
_version_name,
|
||||
"--apk-path",
|
||||
rebase_path(invoker.resource_packaged_apk_path, root_build_dir),
|
||||
]
|
||||
|
||||
if (defined(_asset_location)) {
|
||||
args += [
|
||||
"--asset-dir",
|
||||
rebase_path(_asset_location, root_build_dir),
|
||||
]
|
||||
}
|
||||
if (defined(_resources_zip)) {
|
||||
args += [
|
||||
"--resource-zips",
|
||||
rebase_path(_resources_zip, root_build_dir),
|
||||
]
|
||||
}
|
||||
if (_shared_resources) {
|
||||
args += [ "--shared-resources" ]
|
||||
}
|
||||
if (_split_densities != []) {
|
||||
args += [ "--create-density-splits" ]
|
||||
foreach(_density, _split_densities) {
|
||||
outputs += [ "${invoker.resource_packaged_apk_path}_${_density}" ]
|
||||
}
|
||||
}
|
||||
if (_split_languages != []) {
|
||||
args += [ "--language-splits=$_split_languages" ]
|
||||
foreach(_language, _split_languages) {
|
||||
outputs += [ "${invoker.resource_packaged_apk_path}_${_language}" ]
|
||||
}
|
||||
}
|
||||
if (defined(invoker.extensions_to_not_compress)) {
|
||||
args += [
|
||||
"--no-compress",
|
||||
invoker.extensions_to_not_compress,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_package_resources_target_name = "${target_name}__package_resources"
|
||||
package_resources_helper(_package_resources_target_name) {
|
||||
forward_variables_from(invoker, [ "extensions_to_not_compress" ])
|
||||
deps = _deps
|
||||
android_manifest = _android_manifest
|
||||
resource_packaged_apk_path = _resource_packaged_apk_path
|
||||
}
|
||||
|
||||
_generate_incremental_manifest_target_name =
|
||||
"${target_name}_incremental_generate_manifest"
|
||||
_incremental_android_manifest =
|
||||
get_label_info(_generate_incremental_manifest_target_name,
|
||||
"target_gen_dir") + "/AndroidManifest.xml"
|
||||
action(_generate_incremental_manifest_target_name) {
|
||||
deps = _deps
|
||||
script = "//build/android/incremental_install/generate_android_manifest.py"
|
||||
depfile = "${target_gen_dir}/${target_name}.d"
|
||||
inputs = [
|
||||
_android_manifest,
|
||||
]
|
||||
if (defined(_resources_zip)) {
|
||||
inputs += [ _resources_zip ]
|
||||
}
|
||||
outputs = [
|
||||
depfile,
|
||||
_resource_packaged_apk_path,
|
||||
_incremental_android_manifest,
|
||||
]
|
||||
|
||||
_rebased_src_manifest = rebase_path(_android_manifest, root_build_dir)
|
||||
_rebased_incremental_manifest =
|
||||
rebase_path(_incremental_android_manifest, root_build_dir)
|
||||
args = [
|
||||
"--depfile",
|
||||
rebase_path(depfile, root_build_dir),
|
||||
"--android-sdk",
|
||||
rebased_android_sdk,
|
||||
"--aapt-path",
|
||||
android_aapt_path,
|
||||
"--configuration-name=$android_configuration_name",
|
||||
"--android-manifest",
|
||||
rebase_path(_android_manifest, root_build_dir),
|
||||
"--version-code",
|
||||
_version_code,
|
||||
"--version-name",
|
||||
_version_name,
|
||||
"--apk-path",
|
||||
rebase_path(_resource_packaged_apk_path, root_build_dir),
|
||||
"--src-manifest=$_rebased_src_manifest",
|
||||
"--out-manifest=$_rebased_incremental_manifest",
|
||||
]
|
||||
if (disable_incremental_isolated_processes) {
|
||||
args += [ "--disable-isolated-processes" ]
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(_asset_location)) {
|
||||
args += [
|
||||
"--asset-dir",
|
||||
rebase_path(_asset_location, root_build_dir),
|
||||
]
|
||||
}
|
||||
if (defined(_resources_zip)) {
|
||||
args += [
|
||||
"--resource-zips",
|
||||
rebase_path(_resources_zip, root_build_dir),
|
||||
]
|
||||
}
|
||||
if (_shared_resources) {
|
||||
args += [ "--shared-resources" ]
|
||||
}
|
||||
if (_split_densities != []) {
|
||||
args += [ "--create-density-splits" ]
|
||||
foreach(_density, _split_densities) {
|
||||
outputs += [ "${_resource_packaged_apk_path}_${_density}" ]
|
||||
}
|
||||
}
|
||||
if (_split_languages != []) {
|
||||
args += [ "--language-splits=$_split_languages" ]
|
||||
foreach(_language, _split_languages) {
|
||||
outputs += [ "${_resource_packaged_apk_path}_${_language}" ]
|
||||
}
|
||||
}
|
||||
if (defined(invoker.extensions_to_not_compress)) {
|
||||
args += [
|
||||
"--no-compress",
|
||||
invoker.extensions_to_not_compress,
|
||||
]
|
||||
}
|
||||
_incremental_package_resources_target_name =
|
||||
"${target_name}_incremental__package_resources"
|
||||
|
||||
# TODO(agrieve): See if we can speed up this step by swapping the manifest
|
||||
# from the result of the main package_resources step.
|
||||
package_resources_helper(_incremental_package_resources_target_name) {
|
||||
forward_variables_from(invoker, [ "extensions_to_not_compress" ])
|
||||
deps = _deps + [ ":$_generate_incremental_manifest_target_name" ]
|
||||
android_manifest = _incremental_android_manifest
|
||||
resource_packaged_apk_path = _incremental_resource_packaged_apk_path
|
||||
}
|
||||
|
||||
package_target = "${target_name}__package"
|
||||
package_apk(package_target) {
|
||||
deps = []
|
||||
forward_variables_from(invoker, [ "deps" ])
|
||||
deps += [ ":${_package_resources_target_name}" ]
|
||||
|
||||
data_deps = [ "//tools/android/md5sum" ] # Used when deploying APKs
|
||||
deps = _deps + [ ":${_package_resources_target_name}" ]
|
||||
|
||||
if (defined(_dex_path)) {
|
||||
dex_path = _dex_path
|
||||
|
@ -756,19 +808,21 @@ template("create_apk") {
|
|||
|
||||
_incremental_package_target = "${target_name}_incremental__package"
|
||||
package_apk(_incremental_package_target) {
|
||||
deps = []
|
||||
forward_variables_from(invoker, [ "deps" ])
|
||||
deps += [ ":${_package_resources_target_name}" ]
|
||||
_dex_target = "//build/android/incremental_install:bootstrap_java__dex"
|
||||
deps = _deps + [
|
||||
":${_incremental_package_resources_target_name}",
|
||||
_dex_target,
|
||||
]
|
||||
|
||||
# TODO(agrieve): Multidex
|
||||
if (defined(_dex_path)) {
|
||||
dex_path = _dex_path
|
||||
dex_path =
|
||||
get_label_info(_dex_target, "target_gen_dir") + "/bootstrap.dex"
|
||||
}
|
||||
|
||||
# TODO(agrieve): Add a placeholder .so for http://crbug.com/384638
|
||||
native_libs_dir = "//build/android/empty/res"
|
||||
output_apk_path = _incremental_packaged_apk_path
|
||||
resource_packaged_apk_path = _resource_packaged_apk_path
|
||||
resource_packaged_apk_path = _incremental_resource_packaged_apk_path
|
||||
}
|
||||
|
||||
_finalize_apk_rule_name = "${target_name}__finalize"
|
||||
|
|
|
@ -1161,7 +1161,9 @@ template("android_apk") {
|
|||
_build_config = "$target_gen_dir/$target_name.build_config"
|
||||
resources_zip_path = "$base_path.resources.zip"
|
||||
_all_resources_zip_path = "$base_path.resources.all.zip"
|
||||
jar_path = "$base_path.jar"
|
||||
_jar_path = "$base_path.jar"
|
||||
_lib_dex_path = "$base_path.dex.jar"
|
||||
_rebased_lib_dex_path = rebase_path(_lib_dex_path, root_build_dir)
|
||||
_template_name = target_name
|
||||
|
||||
final_dex_path = "$gen_dir/classes.dex"
|
||||
|
@ -1297,6 +1299,7 @@ template("android_apk") {
|
|||
write_build_config(build_config_target) {
|
||||
forward_variables_from(invoker, [ "apk_under_test" ])
|
||||
type = "android_apk"
|
||||
jar_path = _jar_path
|
||||
dex_path = final_dex_path
|
||||
resources_zip = resources_zip_path
|
||||
build_config = _build_config
|
||||
|
@ -1389,7 +1392,8 @@ template("android_apk") {
|
|||
java_files = []
|
||||
}
|
||||
srcjar_deps = _srcjar_deps
|
||||
dex_path = base_path + ".dex.jar"
|
||||
jar_path = _jar_path
|
||||
dex_path = _lib_dex_path
|
||||
|
||||
if (defined(invoker.deps)) {
|
||||
deps += invoker.deps
|
||||
|
@ -1421,8 +1425,8 @@ template("android_apk") {
|
|||
rebase_path(_dist_jar_path, root_build_dir),
|
||||
"--inputs=@FileArg($_rebased_build_config:dist_jar:dependency_jars)",
|
||||
]
|
||||
inputs += [ jar_path ]
|
||||
_rebased_jar_path = rebase_path([ jar_path ], root_build_dir)
|
||||
inputs += [ _jar_path ]
|
||||
_rebased_jar_path = rebase_path([ _jar_path ], root_build_dir)
|
||||
args += [ "--inputs=$_rebased_jar_path" ]
|
||||
deps = [
|
||||
":$build_config_target", # Generates the build config file.
|
||||
|
@ -1436,15 +1440,15 @@ template("android_apk") {
|
|||
":$build_config_target",
|
||||
":$java_target",
|
||||
]
|
||||
sources = [
|
||||
jar_path,
|
||||
]
|
||||
inputs = [
|
||||
_build_config,
|
||||
]
|
||||
output = final_dex_path
|
||||
dex_arg_key = "${_rebased_build_config}:final_dex:dependency_dex_files"
|
||||
args = [ "--inputs=@FileArg($dex_arg_key)" ]
|
||||
_dex_arg_key = "${_rebased_build_config}:final_dex:dependency_dex_files"
|
||||
args = [
|
||||
"--inputs=@FileArg($_dex_arg_key)",
|
||||
_rebased_lib_dex_path,
|
||||
]
|
||||
}
|
||||
|
||||
if (_native_libs != []) {
|
||||
|
@ -1598,7 +1602,7 @@ template("android_apk") {
|
|||
_create_incremental_script_rule_name = "${_template_name}__incremental_script"
|
||||
_incremental_final_deps += [ ":${_create_incremental_script_rule_name}" ]
|
||||
action(_create_incremental_script_rule_name) {
|
||||
script = "//build/android/gn/create_incremental_install_script.py"
|
||||
script = "//build/android/incremental_install/create_install_script.py"
|
||||
depfile = "$target_gen_dir/$target_name.d"
|
||||
|
||||
_generated_script_path =
|
||||
|
@ -1613,9 +1617,12 @@ template("android_apk") {
|
|||
_rebased_generated_script_path =
|
||||
rebase_path(_generated_script_path, root_build_dir)
|
||||
_rebased_depfile = rebase_path(depfile, root_build_dir)
|
||||
_dex_arg_key = "${_rebased_build_config}:final_dex:dependency_dex_files"
|
||||
args = [
|
||||
"--apk-path=${_rebased_apk_path_no_ext}_incremental.apk",
|
||||
"--script-output-path=$_rebased_generated_script_path",
|
||||
"--dex-file=$_rebased_lib_dex_path",
|
||||
"--dex-file-list=@FileArg($_dex_arg_key)",
|
||||
"--depfile=$_rebased_depfile",
|
||||
]
|
||||
if (defined(_native_libs_dir)) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче