• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2014 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"""Writes a build_config file.
8
9The build_config file for a target is a json file containing information about
10how to build that target based on the target's dependencies. This includes
11things like: the javac classpath, the list of android resources dependencies,
12etc. It also includes the information needed to create the build_config for
13other targets that depend on that one.
14
15Android build scripts should not refer to the build_config directly, and the
16build specification should instead pass information in using the special
17file-arg syntax (see build_utils.py:ExpandFileArgs). That syntax allows passing
18of values in a json dict in a file and looks like this:
19  --python-arg=@FileArg(build_config_path:javac:classpath)
20
21Note: If paths to input files are passed in this way, it is important that:
22  1. inputs/deps of the action ensure that the files are available the first
23  time the action runs.
24  2. Either (a) or (b)
25    a. inputs/deps ensure that the action runs whenever one of the files changes
26    b. the files are added to the action's depfile
27"""
28
29import itertools
30import optparse
31import os
32import sys
33import xml.dom.minidom
34
35from util import build_utils
36from util import md5_check
37
38import write_ordered_libraries
39
40
41# Types that should never be used as a dependency of another build config.
42_ROOT_TYPES = ('android_apk', 'deps_dex', 'java_binary', 'resource_rewriter')
43# Types that should not allow code deps to pass through.
44_RESOURCE_TYPES = ('android_assets', 'android_resources')
45
46
47class AndroidManifest(object):
48  def __init__(self, path):
49    self.path = path
50    dom = xml.dom.minidom.parse(path)
51    manifests = dom.getElementsByTagName('manifest')
52    assert len(manifests) == 1
53    self.manifest = manifests[0]
54
55  def GetInstrumentation(self):
56    instrumentation_els = self.manifest.getElementsByTagName('instrumentation')
57    if len(instrumentation_els) == 0:
58      return None
59    if len(instrumentation_els) != 1:
60      raise Exception(
61          'More than one <instrumentation> element found in %s' % self.path)
62    return instrumentation_els[0]
63
64  def CheckInstrumentation(self, expected_package):
65    instr = self.GetInstrumentation()
66    if not instr:
67      raise Exception('No <instrumentation> elements found in %s' % self.path)
68    instrumented_package = instr.getAttributeNS(
69        'http://schemas.android.com/apk/res/android', 'targetPackage')
70    if instrumented_package != expected_package:
71      raise Exception(
72          'Wrong instrumented package. Expected %s, got %s'
73          % (expected_package, instrumented_package))
74
75  def GetPackageName(self):
76    return self.manifest.getAttribute('package')
77
78
79dep_config_cache = {}
80def GetDepConfig(path):
81  if not path in dep_config_cache:
82    dep_config_cache[path] = build_utils.ReadJson(path)['deps_info']
83  return dep_config_cache[path]
84
85
86def DepsOfType(wanted_type, configs):
87  return [c for c in configs if c['type'] == wanted_type]
88
89
90def GetAllDepsConfigsInOrder(deps_config_paths):
91  def GetDeps(path):
92    return set(GetDepConfig(path)['deps_configs'])
93  return build_utils.GetSortedTransitiveDependencies(deps_config_paths, GetDeps)
94
95
96def ResolveGroups(configs):
97  while True:
98    groups = DepsOfType('group', configs)
99    if not groups:
100      return configs
101    for config in groups:
102      index = configs.index(config)
103      expanded_configs = [GetDepConfig(p) for p in config['deps_configs']]
104      configs[index:index + 1] = expanded_configs
105
106
107class Deps(object):
108  def __init__(self, direct_deps_config_paths):
109    self.all_deps_config_paths = GetAllDepsConfigsInOrder(
110        direct_deps_config_paths)
111    self.direct_deps_configs = ResolveGroups(
112        [GetDepConfig(p) for p in direct_deps_config_paths])
113    self.all_deps_configs = [
114        GetDepConfig(p) for p in self.all_deps_config_paths]
115    self.direct_deps_config_paths = direct_deps_config_paths
116
117  def All(self, wanted_type=None):
118    if type is None:
119      return self.all_deps_configs
120    return DepsOfType(wanted_type, self.all_deps_configs)
121
122  def Direct(self, wanted_type=None):
123    if wanted_type is None:
124      return self.direct_deps_configs
125    return DepsOfType(wanted_type, self.direct_deps_configs)
126
127  def AllConfigPaths(self):
128    return self.all_deps_config_paths
129
130  def RemoveNonDirectDep(self, path):
131    if path in self.direct_deps_config_paths:
132      raise Exception('Cannot remove direct dep.')
133    self.all_deps_config_paths.remove(path)
134    self.all_deps_configs.remove(GetDepConfig(path))
135
136def _MergeAssets(all_assets):
137  """Merges all assets from the given deps.
138
139  Returns:
140    A tuple of lists: (compressed, uncompressed)
141    Each tuple entry is a list of "srcPath:zipPath". srcPath is the path of the
142    asset to add, and zipPath is the location within the zip (excluding assets/
143    prefix)
144  """
145  compressed = {}
146  uncompressed = {}
147  for asset_dep in all_assets:
148    entry = asset_dep['assets']
149    disable_compression = entry.get('disable_compression', False)
150    dest_map = uncompressed if disable_compression else compressed
151    other_map = compressed if disable_compression else uncompressed
152    outputs = entry.get('outputs', [])
153    for src, dest in itertools.izip_longest(entry['sources'], outputs):
154      if not dest:
155        dest = os.path.basename(src)
156      # Merge so that each path shows up in only one of the lists, and that
157      # deps of the same target override previous ones.
158      other_map.pop(dest, 0)
159      dest_map[dest] = src
160
161  def create_list(asset_map):
162    ret = ['%s:%s' % (src, dest) for dest, src in asset_map.iteritems()]
163    # Sort to ensure deterministic ordering.
164    ret.sort()
165    return ret
166
167  return create_list(compressed), create_list(uncompressed)
168
169
170def _FilterUnwantedDepsPaths(dep_paths, target_type):
171  # Don't allow root targets to be considered as a dep.
172  ret = [p for p in dep_paths if GetDepConfig(p)['type'] not in _ROOT_TYPES]
173
174  # Don't allow java libraries to cross through assets/resources.
175  if target_type in _RESOURCE_TYPES:
176    ret = [p for p in ret if GetDepConfig(p)['type'] in _RESOURCE_TYPES]
177  return ret
178
179
180def _AsInterfaceJar(jar_path):
181  return jar_path[:-3] + 'interface.jar'
182
183
184def main(argv):
185  parser = optparse.OptionParser()
186  build_utils.AddDepfileOption(parser)
187  parser.add_option('--build-config', help='Path to build_config output.')
188  parser.add_option(
189      '--type',
190      help='Type of this target (e.g. android_library).')
191  parser.add_option(
192      '--possible-deps-configs',
193      help='List of paths for dependency\'s build_config files. Some '
194      'dependencies may not write build_config files. Missing build_config '
195      'files are handled differently based on the type of this target.')
196
197  # android_resources options
198  parser.add_option('--srcjar', help='Path to target\'s resources srcjar.')
199  parser.add_option('--resources-zip', help='Path to target\'s resources zip.')
200  parser.add_option('--r-text', help='Path to target\'s R.txt file.')
201  parser.add_option('--package-name',
202      help='Java package name for these resources.')
203  parser.add_option('--android-manifest', help='Path to android manifest.')
204  parser.add_option('--is-locale-resource', action='store_true',
205                    help='Whether it is locale resource.')
206
207  # android_assets options
208  parser.add_option('--asset-sources', help='List of asset sources.')
209  parser.add_option('--asset-renaming-sources',
210                    help='List of asset sources with custom destinations.')
211  parser.add_option('--asset-renaming-destinations',
212                    help='List of asset custom destinations.')
213  parser.add_option('--disable-asset-compression', action='store_true',
214                    help='Whether to disable asset compression.')
215
216  # java library options
217  parser.add_option('--jar-path', help='Path to target\'s jar output.')
218  parser.add_option('--supports-android', action='store_true',
219      help='Whether this library supports running on the Android platform.')
220  parser.add_option('--requires-android', action='store_true',
221      help='Whether this library requires running on the Android platform.')
222  parser.add_option('--bypass-platform-checks', action='store_true',
223      help='Bypass checks for support/require Android platform.')
224
225  # android library options
226  parser.add_option('--dex-path', help='Path to target\'s dex output.')
227
228  # native library options
229  parser.add_option('--native-libs', help='List of top-level native libs.')
230  parser.add_option('--readelf-path', help='Path to toolchain\'s readelf.')
231
232  # apk options
233  parser.add_option('--apk-path', help='Path to the target\'s apk output.')
234  parser.add_option('--incremental-apk-path',
235                    help="Path to the target's incremental apk output.")
236  parser.add_option('--incremental-install-script-path',
237                    help="Path to the target's generated incremental install "
238                    "script.")
239
240  parser.add_option('--tested-apk-config',
241      help='Path to the build config of the tested apk (for an instrumentation '
242      'test apk).')
243  parser.add_option('--proguard-enabled', action='store_true',
244      help='Whether proguard is enabled for this apk.')
245  parser.add_option('--proguard-info',
246      help='Path to the proguard .info output for this apk.')
247  parser.add_option('--has-alternative-locale-resource', action='store_true',
248      help='Whether there is alternative-locale-resource in direct deps')
249
250  options, args = parser.parse_args(argv)
251
252  if args:
253    parser.error('No positional arguments should be given.')
254
255  required_options_map = {
256      'java_binary': ['build_config', 'jar_path'],
257      'java_library': ['build_config', 'jar_path'],
258      'android_assets': ['build_config'],
259      'android_resources': ['build_config', 'resources_zip'],
260      'android_apk': ['build_config', 'jar_path', 'dex_path', 'resources_zip'],
261      'deps_dex': ['build_config', 'dex_path'],
262      'resource_rewriter': ['build_config'],
263      'group': ['build_config'],
264  }
265  required_options = required_options_map.get(options.type)
266  if not required_options:
267    raise Exception('Unknown type: <%s>' % options.type)
268
269  if options.native_libs:
270    required_options.append('readelf_path')
271
272  build_utils.CheckOptions(options, parser, required_options)
273
274  if options.type == 'java_library':
275    if options.supports_android and not options.dex_path:
276      raise Exception('java_library that supports Android requires a dex path.')
277
278    if options.requires_android and not options.supports_android:
279      raise Exception(
280          '--supports-android is required when using --requires-android')
281
282  possible_deps_config_paths = build_utils.ParseGypList(
283      options.possible_deps_configs)
284
285  unknown_deps = [
286      c for c in possible_deps_config_paths if not os.path.exists(c)]
287
288  direct_deps_config_paths = [
289      c for c in possible_deps_config_paths if not c in unknown_deps]
290  direct_deps_config_paths = _FilterUnwantedDepsPaths(direct_deps_config_paths,
291                                                      options.type)
292
293  deps = Deps(direct_deps_config_paths)
294  all_inputs = deps.AllConfigPaths() + build_utils.GetPythonDependencies()
295
296  # Remove other locale resources if there is alternative_locale_resource in
297  # direct deps.
298  if options.has_alternative_locale_resource:
299    alternative = [r['path'] for r in deps.Direct('android_resources')
300                   if r.get('is_locale_resource')]
301    # We can only have one locale resources in direct deps.
302    if len(alternative) != 1:
303      raise Exception('The number of locale resource in direct deps is wrong %d'
304                       % len(alternative))
305    unwanted = [r['path'] for r in deps.All('android_resources')
306                if r.get('is_locale_resource') and r['path'] not in alternative]
307    for p in unwanted:
308      deps.RemoveNonDirectDep(p)
309
310
311  direct_library_deps = deps.Direct('java_library')
312  all_library_deps = deps.All('java_library')
313
314  direct_resources_deps = deps.Direct('android_resources')
315  all_resources_deps = deps.All('android_resources')
316  # Resources should be ordered with the highest-level dependency first so that
317  # overrides are done correctly.
318  all_resources_deps.reverse()
319
320  if options.type == 'android_apk' and options.tested_apk_config:
321    tested_apk_deps = Deps([options.tested_apk_config])
322    tested_apk_resources_deps = tested_apk_deps.All('android_resources')
323    all_resources_deps = [
324        d for d in all_resources_deps if not d in tested_apk_resources_deps]
325
326  # Initialize some common config.
327  config = {
328    'deps_info': {
329      'name': os.path.basename(options.build_config),
330      'path': options.build_config,
331      'type': options.type,
332      'deps_configs': direct_deps_config_paths
333    }
334  }
335  deps_info = config['deps_info']
336
337  if (options.type in ('java_binary', 'java_library') and
338      not options.bypass_platform_checks):
339    deps_info['requires_android'] = options.requires_android
340    deps_info['supports_android'] = options.supports_android
341
342    deps_require_android = (all_resources_deps +
343        [d['name'] for d in all_library_deps if d['requires_android']])
344    deps_not_support_android = (
345        [d['name'] for d in all_library_deps if not d['supports_android']])
346
347    if deps_require_android and not options.requires_android:
348      raise Exception('Some deps require building for the Android platform: ' +
349          str(deps_require_android))
350
351    if deps_not_support_android and options.supports_android:
352      raise Exception('Not all deps support the Android platform: ' +
353          str(deps_not_support_android))
354
355  if options.type in ('java_binary', 'java_library', 'android_apk'):
356    javac_classpath = [c['jar_path'] for c in direct_library_deps]
357    java_full_classpath = [c['jar_path'] for c in all_library_deps]
358    deps_info['resources_deps'] = [c['path'] for c in all_resources_deps]
359    deps_info['jar_path'] = options.jar_path
360    if options.type == 'android_apk' or options.supports_android:
361      deps_info['dex_path'] = options.dex_path
362    if options.type == 'android_apk':
363      deps_info['apk_path'] = options.apk_path
364      deps_info['incremental_apk_path'] = options.incremental_apk_path
365      deps_info['incremental_install_script_path'] = (
366          options.incremental_install_script_path)
367
368    # Classpath values filled in below (after applying tested_apk_config).
369    config['javac'] = {}
370
371  if options.type in ('java_binary', 'java_library'):
372    # Only resources might have srcjars (normal srcjar targets are listed in
373    # srcjar_deps). A resource's srcjar contains the R.java file for those
374    # resources, and (like Android's default build system) we allow a library to
375    # refer to the resources in any of its dependents.
376    config['javac']['srcjars'] = [
377        c['srcjar'] for c in direct_resources_deps if 'srcjar' in c]
378
379    # Used to strip out R.class for android_prebuilt()s.
380    if options.type == 'java_library':
381      config['javac']['resource_packages'] = [
382          c['package_name'] for c in all_resources_deps if 'package_name' in c]
383
384  if options.type == 'android_apk':
385    # Apks will get their resources srcjar explicitly passed to the java step.
386    config['javac']['srcjars'] = []
387
388  if options.type == 'android_assets':
389    all_asset_sources = []
390    if options.asset_renaming_sources:
391      all_asset_sources.extend(
392          build_utils.ParseGypList(options.asset_renaming_sources))
393    if options.asset_sources:
394      all_asset_sources.extend(build_utils.ParseGypList(options.asset_sources))
395
396    deps_info['assets'] = {
397        'sources': all_asset_sources
398    }
399    if options.asset_renaming_destinations:
400      deps_info['assets']['outputs'] = (
401          build_utils.ParseGypList(options.asset_renaming_destinations))
402    if options.disable_asset_compression:
403      deps_info['assets']['disable_compression'] = True
404
405  if options.type == 'android_resources':
406    deps_info['resources_zip'] = options.resources_zip
407    if options.srcjar:
408      deps_info['srcjar'] = options.srcjar
409    if options.android_manifest:
410      manifest = AndroidManifest(options.android_manifest)
411      deps_info['package_name'] = manifest.GetPackageName()
412    if options.package_name:
413      deps_info['package_name'] = options.package_name
414    if options.r_text:
415      deps_info['r_text'] = options.r_text
416    if options.is_locale_resource:
417      deps_info['is_locale_resource'] = True
418
419  if options.type in ('android_resources','android_apk', 'resource_rewriter'):
420    config['resources'] = {}
421    config['resources']['dependency_zips'] = [
422        c['resources_zip'] for c in all_resources_deps]
423    config['resources']['extra_package_names'] = []
424    config['resources']['extra_r_text_files'] = []
425
426  if options.type == 'android_apk' or options.type == 'resource_rewriter':
427    config['resources']['extra_package_names'] = [
428        c['package_name'] for c in all_resources_deps if 'package_name' in c]
429    config['resources']['extra_r_text_files'] = [
430        c['r_text'] for c in all_resources_deps if 'r_text' in c]
431
432  if options.type in ['android_apk', 'deps_dex']:
433    deps_dex_files = [c['dex_path'] for c in all_library_deps]
434
435  proguard_enabled = options.proguard_enabled
436  if options.type == 'android_apk':
437    deps_info['proguard_enabled'] = proguard_enabled
438
439  if proguard_enabled:
440    deps_info['proguard_info'] = options.proguard_info
441    config['proguard'] = {}
442    proguard_config = config['proguard']
443    proguard_config['input_paths'] = [options.jar_path] + java_full_classpath
444
445  # An instrumentation test apk should exclude the dex files that are in the apk
446  # under test.
447  if options.type == 'android_apk' and options.tested_apk_config:
448    tested_apk_library_deps = tested_apk_deps.All('java_library')
449    tested_apk_deps_dex_files = [c['dex_path'] for c in tested_apk_library_deps]
450    # Include in the classpath classes that are added directly to the apk under
451    # test (those that are not a part of a java_library).
452    tested_apk_config = GetDepConfig(options.tested_apk_config)
453    javac_classpath.append(tested_apk_config['jar_path'])
454    # Exclude dex files from the test apk that exist within the apk under test.
455    deps_dex_files = [
456        p for p in deps_dex_files if not p in tested_apk_deps_dex_files]
457
458    expected_tested_package = tested_apk_config['package_name']
459    AndroidManifest(options.android_manifest).CheckInstrumentation(
460        expected_tested_package)
461    if tested_apk_config['proguard_enabled']:
462      assert proguard_enabled, ('proguard must be enabled for instrumentation'
463          ' apks if it\'s enabled for the tested apk')
464
465  # Dependencies for the final dex file of an apk or a 'deps_dex'.
466  if options.type in ['android_apk', 'deps_dex']:
467    config['final_dex'] = {}
468    dex_config = config['final_dex']
469    dex_config['dependency_dex_files'] = deps_dex_files
470
471  if options.type in ('java_binary', 'java_library', 'android_apk'):
472    config['javac']['classpath'] = javac_classpath
473    config['javac']['interface_classpath'] = [
474        _AsInterfaceJar(p) for p in javac_classpath]
475    config['java'] = {
476      'full_classpath': java_full_classpath
477    }
478
479  if options.type == 'android_apk':
480    dependency_jars = [c['jar_path'] for c in all_library_deps]
481    all_interface_jars = [
482        _AsInterfaceJar(p) for p in dependency_jars + [options.jar_path]]
483    config['dist_jar'] = {
484      'dependency_jars': dependency_jars,
485      'all_interface_jars': all_interface_jars,
486    }
487    manifest = AndroidManifest(options.android_manifest)
488    deps_info['package_name'] = manifest.GetPackageName()
489    if not options.tested_apk_config and manifest.GetInstrumentation():
490      # This must then have instrumentation only for itself.
491      manifest.CheckInstrumentation(manifest.GetPackageName())
492
493    library_paths = []
494    java_libraries_list_holder = [None]
495    libraries = build_utils.ParseGypList(options.native_libs or '[]')
496    if libraries:
497      def recompute_ordered_libraries():
498        libraries_dir = os.path.dirname(libraries[0])
499        write_ordered_libraries.SetReadelfPath(options.readelf_path)
500        write_ordered_libraries.SetLibraryDirs([libraries_dir])
501        all_deps = (
502            write_ordered_libraries.GetSortedTransitiveDependenciesForBinaries(
503                libraries))
504        # Create a java literal array with the "base" library names:
505        # e.g. libfoo.so -> foo
506        java_libraries_list_holder[0] = ('{%s}' % ','.join(
507            ['"%s"' % s[3:-3] for s in all_deps]))
508        library_paths.extend(
509            write_ordered_libraries.FullLibraryPath(x) for x in all_deps)
510
511      # This step takes about 600ms on a z620 for chrome_apk, so it's worth
512      # caching.
513      md5_check.CallAndRecordIfStale(
514          recompute_ordered_libraries,
515          record_path=options.build_config + '.nativelibs.md5.stamp',
516          input_paths=libraries,
517          output_paths=[options.build_config])
518      if not library_paths:
519        prev_config = build_utils.ReadJson(options.build_config)
520        java_libraries_list_holder[0] = (
521            prev_config['native']['java_libraries_list'])
522        library_paths.extend(prev_config['native']['libraries'])
523
524    all_inputs.extend(library_paths)
525    config['native'] = {
526      'libraries': library_paths,
527      'java_libraries_list': java_libraries_list_holder[0],
528    }
529    config['assets'], config['uncompressed_assets'] = (
530        _MergeAssets(deps.All('android_assets')))
531
532  build_utils.WriteJson(config, options.build_config, only_if_changed=True)
533
534  if options.depfile:
535    build_utils.WriteDepfile(options.depfile, all_inputs)
536
537
538if __name__ == '__main__':
539  sys.exit(main(sys.argv[1:]))
540