1#!/usr/bin/env python 2# 3# Copyright 2013 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 7import json 8import logging 9import optparse 10import os 11import sys 12import tempfile 13import zipfile 14 15from util import build_utils 16 17 18def _RemoveUnwantedFilesFromZip(dex_path): 19 iz = zipfile.ZipFile(dex_path, 'r') 20 tmp_dex_path = '%s.tmp.zip' % dex_path 21 oz = zipfile.ZipFile(tmp_dex_path, 'w', zipfile.ZIP_DEFLATED) 22 for i in iz.namelist(): 23 if i.endswith('.dex'): 24 oz.writestr(i, iz.read(i)) 25 os.remove(dex_path) 26 os.rename(tmp_dex_path, dex_path) 27 28 29def _ParseArgs(args): 30 args = build_utils.ExpandFileArgs(args) 31 32 parser = optparse.OptionParser() 33 build_utils.AddDepfileOption(parser) 34 35 parser.add_option('--android-sdk-tools', 36 help='Android sdk build tools directory.') 37 parser.add_option('--output-directory', 38 default=os.getcwd(), 39 help='Path to the output build directory.') 40 parser.add_option('--dex-path', help='Dex output path.') 41 parser.add_option('--configuration-name', 42 help='The build CONFIGURATION_NAME.') 43 parser.add_option('--proguard-enabled', 44 help='"true" if proguard is enabled.') 45 parser.add_option('--debug-build-proguard-enabled', 46 help='"true" if proguard is enabled for debug build.') 47 parser.add_option('--proguard-enabled-input-path', 48 help=('Path to dex in Release mode when proguard ' 49 'is enabled.')) 50 parser.add_option('--no-locals', default='0', 51 help='Exclude locals list from the dex file.') 52 parser.add_option('--incremental', 53 action='store_true', 54 help='Enable incremental builds when possible.') 55 parser.add_option('--inputs', help='A list of additional input paths.') 56 parser.add_option('--excluded-paths', 57 help='A list of paths to exclude from the dex file.') 58 parser.add_option('--main-dex-list-path', 59 help='A file containing a list of the classes to ' 60 'include in the main dex.') 61 parser.add_option('--multidex-configuration-path', 62 help='A JSON file containing multidex build configuration.') 63 parser.add_option('--multi-dex', default=False, action='store_true', 64 help='Generate multiple dex files.') 65 66 options, paths = parser.parse_args(args) 67 68 required_options = ('android_sdk_tools',) 69 build_utils.CheckOptions(options, parser, required=required_options) 70 71 if options.multidex_configuration_path: 72 with open(options.multidex_configuration_path) as multidex_config_file: 73 multidex_config = json.loads(multidex_config_file.read()) 74 options.multi_dex = multidex_config.get('enabled', False) 75 76 if options.multi_dex and not options.main_dex_list_path: 77 logging.warning('multidex cannot be enabled without --main-dex-list-path') 78 options.multi_dex = False 79 elif options.main_dex_list_path and not options.multi_dex: 80 logging.warning('--main-dex-list-path is unused if multidex is not enabled') 81 82 if options.inputs: 83 options.inputs = build_utils.ParseGypList(options.inputs) 84 if options.excluded_paths: 85 options.excluded_paths = build_utils.ParseGypList(options.excluded_paths) 86 87 return options, paths 88 89 90def _AllSubpathsAreClassFiles(paths, changes): 91 for path in paths: 92 if any(not p.endswith('.class') for p in changes.IterChangedSubpaths(path)): 93 return False 94 return True 95 96 97def _DexWasEmpty(paths, changes): 98 for path in paths: 99 if any(p.endswith('.class') 100 for p in changes.old_metadata.IterSubpaths(path)): 101 return False 102 return True 103 104 105def _RunDx(changes, options, dex_cmd, paths): 106 with build_utils.TempDir() as classes_temp_dir: 107 # --multi-dex is incompatible with --incremental. 108 if options.multi_dex: 109 dex_cmd.append('--main-dex-list=%s' % options.main_dex_list_path) 110 else: 111 # Use --incremental when .class files are added or modified (never when 112 # removed). 113 # --incremental tells dx to merge all newly dex'ed .class files with 114 # what that already exist in the output dex file (existing classes are 115 # replaced). 116 if options.incremental and changes.AddedOrModifiedOnly(): 117 changed_inputs = set(changes.IterChangedPaths()) 118 changed_paths = [p for p in paths if p in changed_inputs] 119 if not changed_paths: 120 return 121 # When merging in other dex files, there's no easy way to know if 122 # classes were removed from them. 123 if (_AllSubpathsAreClassFiles(changed_paths, changes) 124 and not _DexWasEmpty(changed_paths, changes)): 125 dex_cmd.append('--incremental') 126 for path in changed_paths: 127 changed_subpaths = set(changes.IterChangedSubpaths(path)) 128 # Not a fundamental restriction, but it's the case right now and it 129 # simplifies the logic to assume so. 130 assert changed_subpaths, 'All inputs should be zip files.' 131 build_utils.ExtractAll(path, path=classes_temp_dir, 132 predicate=lambda p: p in changed_subpaths) 133 paths = [classes_temp_dir] 134 135 dex_cmd += paths 136 build_utils.CheckOutput(dex_cmd, print_stderr=False) 137 138 if options.dex_path.endswith('.zip'): 139 _RemoveUnwantedFilesFromZip(options.dex_path) 140 141 142def _OnStaleMd5(changes, options, dex_cmd, paths): 143 _RunDx(changes, options, dex_cmd, paths) 144 build_utils.WriteJson( 145 [os.path.relpath(p, options.output_directory) for p in paths], 146 options.dex_path + '.inputs') 147 148 149def main(args): 150 options, paths = _ParseArgs(args) 151 if ((options.proguard_enabled == 'true' 152 and options.configuration_name == 'Release') 153 or (options.debug_build_proguard_enabled == 'true' 154 and options.configuration_name == 'Debug')): 155 paths = [options.proguard_enabled_input_path] 156 157 if options.inputs: 158 paths += options.inputs 159 160 if options.excluded_paths: 161 # Excluded paths are relative to the output directory. 162 exclude_paths = options.excluded_paths 163 paths = [p for p in paths if not 164 os.path.relpath(p, options.output_directory) in exclude_paths] 165 166 input_paths = list(paths) 167 168 dx_binary = os.path.join(options.android_sdk_tools, 'dx') 169 # See http://crbug.com/272064 for context on --force-jumbo. 170 # See https://github.com/android/platform_dalvik/commit/dd140a22d for 171 # --num-threads. 172 dex_cmd = [dx_binary, '--num-threads=8', '--dex', '--force-jumbo', 173 '--output', options.dex_path] 174 if options.no_locals != '0': 175 dex_cmd.append('--no-locals') 176 177 if options.multi_dex: 178 input_paths.append(options.main_dex_list_path) 179 dex_cmd += [ 180 '--multi-dex', 181 '--minimal-main-dex', 182 ] 183 184 output_paths = [ 185 options.dex_path, 186 options.dex_path + '.inputs', 187 ] 188 189 # An escape hatch to be able to check if incremental dexing is causing 190 # problems. 191 force = int(os.environ.get('DISABLE_INCREMENTAL_DX', 0)) 192 193 build_utils.CallAndWriteDepfileIfStale( 194 lambda changes: _OnStaleMd5(changes, options, dex_cmd, paths), 195 options, 196 input_paths=input_paths, 197 input_strings=dex_cmd, 198 output_paths=output_paths, 199 force=force, 200 pass_changes=True) 201 202 203if __name__ == '__main__': 204 sys.exit(main(sys.argv[1:])) 205