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