1#!/usr/bin/env python
2#
3# Copyright (c) 2012 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"""Process Android resource directories to generate .resources.zip and R.txt
8files."""
9
10import argparse
11import collections
12import os
13import re
14import shutil
15import sys
16import zipfile
17
18from util import build_utils
19from util import jar_info_utils
20from util import manifest_utils
21from util import md5_check
22from util import resources_parser
23from util import resource_utils
24
25
26def _ParseArgs(args):
27  """Parses command line options.
28
29  Returns:
30    An options object as from argparse.ArgumentParser.parse_args()
31  """
32  parser, input_opts, output_opts = resource_utils.ResourceArgsParser()
33
34  input_opts.add_argument(
35      '--res-sources-path',
36      required=True,
37      help='Path to a list of input resources for this target.')
38
39  input_opts.add_argument(
40      '--shared-resources',
41      action='store_true',
42      help='Make resources shareable by generating an onResourcesLoaded() '
43           'method in the R.java source file.')
44
45  input_opts.add_argument('--custom-package',
46                          help='Optional Java package for main R.java.')
47
48  input_opts.add_argument(
49      '--android-manifest',
50      help='Optional AndroidManifest.xml path. Only used to extract a package '
51           'name for R.java if a --custom-package is not provided.')
52
53  output_opts.add_argument(
54      '--resource-zip-out',
55      help='Path to a zip archive containing all resources from '
56      '--resource-dirs, merged into a single directory tree.')
57
58  output_opts.add_argument('--r-text-out',
59                    help='Path to store the generated R.txt file.')
60
61  input_opts.add_argument(
62      '--strip-drawables',
63      action="store_true",
64      help='Remove drawables from the resources.')
65
66  options = parser.parse_args(args)
67
68  resource_utils.HandleCommonOptions(options)
69
70  with open(options.res_sources_path) as f:
71    options.sources = f.read().splitlines()
72  options.resource_dirs = resource_utils.DeduceResourceDirsFromFileList(
73      options.sources)
74
75  return options
76
77
78def _CheckAllFilesListed(resource_files, resource_dirs):
79  resource_files = set(resource_files)
80  missing_files = []
81  for path, _ in resource_utils.IterResourceFilesInDirectories(resource_dirs):
82    if path not in resource_files:
83      missing_files.append(path)
84
85  if missing_files:
86    sys.stderr.write('Error: Found files not listed in the sources list of '
87                     'the BUILD.gn target:\n')
88    for path in missing_files:
89      sys.stderr.write('{}\n'.format(path))
90    sys.exit(1)
91
92
93def _ZipResources(resource_dirs, zip_path, ignore_pattern):
94  # ignore_pattern is a string of ':' delimited list of globs used to ignore
95  # files that should not be part of the final resource zip.
96  files_to_zip = []
97  path_info = resource_utils.ResourceInfoFile()
98  for index, resource_dir in enumerate(resource_dirs):
99    attributed_aar = None
100    if not resource_dir.startswith('..'):
101      aar_source_info_path = os.path.join(
102          os.path.dirname(resource_dir), 'source.info')
103      if os.path.exists(aar_source_info_path):
104        attributed_aar = jar_info_utils.ReadAarSourceInfo(aar_source_info_path)
105
106    for path, archive_path in resource_utils.IterResourceFilesInDirectories(
107        [resource_dir], ignore_pattern):
108      attributed_path = path
109      if attributed_aar:
110        attributed_path = os.path.join(attributed_aar, 'res',
111                                       path[len(resource_dir) + 1:])
112      # Use the non-prefixed archive_path in the .info file.
113      path_info.AddMapping(archive_path, attributed_path)
114
115      resource_dir_name = os.path.basename(resource_dir)
116      archive_path = '{}_{}/{}'.format(index, resource_dir_name, archive_path)
117      files_to_zip.append((archive_path, path))
118
119  path_info.Write(zip_path + '.info')
120
121  with zipfile.ZipFile(zip_path, 'w') as z:
122    # This magic comment signals to resource_utils.ExtractDeps that this zip is
123    # not just the contents of a single res dir, without the encapsulating res/
124    # (like the outputs of android_generated_resources targets), but instead has
125    # the contents of possibly multiple res/ dirs each within an encapsulating
126    # directory within the zip.
127    z.comment = resource_utils.MULTIPLE_RES_MAGIC_STRING
128    build_utils.DoZip(files_to_zip, z)
129
130
131def _GenerateRTxt(options, dep_subdirs, gen_dir):
132  """Generate R.txt file.
133
134  Args:
135    options: The command-line options tuple.
136    dep_subdirs: List of directories containing extracted dependency resources.
137    gen_dir: Locates where the aapt-generated files will go. In particular
138      the output file is always generated as |{gen_dir}/R.txt|.
139  """
140  ignore_pattern = resource_utils.AAPT_IGNORE_PATTERN
141  if options.strip_drawables:
142    ignore_pattern += ':*drawable*'
143
144  # Adding all dependencies as sources is necessary for @type/foo references
145  # to symbols within dependencies to resolve. However, it has the side-effect
146  # that all Java symbols from dependencies are copied into the new R.java.
147  # E.g.: It enables an arguably incorrect usage of
148  # "mypackage.R.id.lib_symbol" where "libpackage.R.id.lib_symbol" would be
149  # more correct. This is just how Android works.
150  resource_dirs = dep_subdirs + options.resource_dirs
151
152  resources_parser.RTxtGenerator(resource_dirs, ignore_pattern).WriteRTxtFile(
153      os.path.join(gen_dir, 'R.txt'))
154
155
156def _OnStaleMd5(options):
157  with resource_utils.BuildContext() as build:
158    if options.sources:
159      _CheckAllFilesListed(options.sources, options.resource_dirs)
160    if options.r_text_in:
161      r_txt_path = options.r_text_in
162    else:
163      # Extract dependencies to resolve @foo/type references into
164      # dependent packages.
165      dep_subdirs = resource_utils.ExtractDeps(options.dependencies_res_zips,
166                                               build.deps_dir)
167
168      _GenerateRTxt(options, dep_subdirs, build.gen_dir)
169      r_txt_path = build.r_txt_path
170
171    if options.r_text_out:
172      shutil.copyfile(r_txt_path, options.r_text_out)
173
174    if options.resource_zip_out:
175      ignore_pattern = resource_utils.AAPT_IGNORE_PATTERN
176      if options.strip_drawables:
177        ignore_pattern += ':*drawable*'
178      _ZipResources(options.resource_dirs, options.resource_zip_out,
179                    ignore_pattern)
180
181
182def main(args):
183  args = build_utils.ExpandFileArgs(args)
184  options = _ParseArgs(args)
185
186  # Order of these must match order specified in GN so that the correct one
187  # appears first in the depfile.
188  possible_output_paths = [
189    options.resource_zip_out,
190    options.r_text_out,
191  ]
192  output_paths = [x for x in possible_output_paths if x]
193
194  # List python deps in input_strings rather than input_paths since the contents
195  # of them does not change what gets written to the depsfile.
196  input_strings = options.extra_res_packages + [
197      options.custom_package,
198      options.shared_resources,
199      options.strip_drawables,
200  ]
201
202  possible_input_paths = [
203    options.android_manifest,
204  ]
205  possible_input_paths += options.include_resources
206  input_paths = [x for x in possible_input_paths if x]
207  input_paths.extend(options.dependencies_res_zips)
208
209  # Resource files aren't explicitly listed in GN. Listing them in the depfile
210  # ensures the target will be marked stale when resource files are removed.
211  depfile_deps = []
212  resource_names = []
213  for resource_dir in options.resource_dirs:
214    for resource_file in build_utils.FindInDirectory(resource_dir, '*'):
215      # Don't list the empty .keep file in depfile. Since it doesn't end up
216      # included in the .zip, it can lead to -w 'dupbuild=err' ninja errors
217      # if ever moved.
218      if not resource_file.endswith(os.path.join('empty', '.keep')):
219        input_paths.append(resource_file)
220        depfile_deps.append(resource_file)
221      resource_names.append(os.path.relpath(resource_file, resource_dir))
222
223  # Resource filenames matter to the output, so add them to strings as well.
224  # This matters if a file is renamed but not changed (http://crbug.com/597126).
225  input_strings.extend(sorted(resource_names))
226
227  md5_check.CallAndWriteDepfileIfStale(
228      lambda: _OnStaleMd5(options),
229      options,
230      input_paths=input_paths,
231      input_strings=input_strings,
232      output_paths=output_paths,
233      depfile_deps=depfile_deps)
234
235
236if __name__ == '__main__':
237  main(sys.argv[1:])
238