• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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