David 'Digit' Turner | 6694004 | 2018-03-23 09:09:42 | [diff] [blame^] | 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, R.txt and |
| 8 | .srcjar files.""" |
| 9 | |
| 10 | import argparse |
| 11 | import collections |
| 12 | import os |
| 13 | import re |
| 14 | import shutil |
| 15 | import sys |
| 16 | |
| 17 | import generate_v14_compatible_resources |
| 18 | |
| 19 | from util import build_utils |
| 20 | from util import resource_utils |
| 21 | |
| 22 | |
| 23 | def _ParseArgs(args): |
| 24 | """Parses command line options. |
| 25 | |
| 26 | Returns: |
| 27 | An options object as from argparse.ArgumentParser.parse_args() |
| 28 | """ |
| 29 | parser, input_opts, output_opts = resource_utils.ResourceArgsParser() |
| 30 | |
| 31 | input_opts.add_argument('--resource-dirs', |
| 32 | default='[]', |
| 33 | help='A list of input directories containing resources ' |
| 34 | 'for this target.') |
| 35 | |
| 36 | input_opts.add_argument( |
| 37 | '--shared-resources', |
| 38 | action='store_true', |
| 39 | help='Make resources shareable by generating an onResourcesLoaded() ' |
| 40 | 'method in the R.java source file.') |
| 41 | |
| 42 | input_opts.add_argument('--custom-package', |
| 43 | help='Optional Java package for main R.java.') |
| 44 | |
| 45 | input_opts.add_argument( |
| 46 | '--android-manifest', |
| 47 | help='Optional AndroidManifest.xml path. Only used to extract a package ' |
| 48 | 'name for R.java if a --custom-package is not provided.') |
| 49 | |
| 50 | output_opts.add_argument( |
| 51 | '--resource-zip-out', |
| 52 | help='Path to a zip archive containing all resources from ' |
| 53 | '--resource-dirs, merged into a single directory tree. This will ' |
| 54 | 'also include auto-generated v14-compatible resources unless ' |
| 55 | '--v14-skip is used.') |
| 56 | |
| 57 | output_opts.add_argument('--srcjar-out', |
| 58 | help='Path to .srcjar to contain the generated R.java.') |
| 59 | |
| 60 | output_opts.add_argument('--r-text-out', |
| 61 | help='Path to store the generated R.txt file.') |
| 62 | |
| 63 | input_opts.add_argument( |
| 64 | '--v14-skip', |
| 65 | action="store_true", |
| 66 | help='Do not generate nor verify v14 resources.') |
| 67 | |
| 68 | options = parser.parse_args(args) |
| 69 | |
| 70 | resource_utils.HandleCommonOptions(options) |
| 71 | |
| 72 | options.resource_dirs = build_utils.ParseGnList(options.resource_dirs) |
| 73 | |
| 74 | return options |
| 75 | |
| 76 | |
| 77 | def _GenerateGlobs(pattern): |
| 78 | # This function processes the aapt ignore assets pattern into a list of globs |
| 79 | # to be used to exclude files on the python side. It removes the '!', which is |
| 80 | # used by aapt to mean 'not chatty' so it does not output if the file is |
| 81 | # ignored (we dont output anyways, so it is not required). This function does |
| 82 | # not handle the <dir> and <file> prefixes used by aapt and are assumed not to |
| 83 | # be included in the pattern string. |
| 84 | return pattern.replace('!', '').split(':') |
| 85 | |
| 86 | |
| 87 | def _ZipResources(resource_dirs, zip_path, ignore_pattern): |
| 88 | # Python zipfile does not provide a way to replace a file (it just writes |
| 89 | # another file with the same name). So, first collect all the files to put |
| 90 | # in the zip (with proper overriding), and then zip them. |
| 91 | # ignore_pattern is a string of ':' delimited list of globs used to ignore |
| 92 | # files that should not be part of the final resource zip. |
| 93 | files_to_zip = dict() |
| 94 | globs = _GenerateGlobs(ignore_pattern) |
| 95 | for d in resource_dirs: |
| 96 | for root, _, files in os.walk(d): |
| 97 | for f in files: |
| 98 | archive_path = f |
| 99 | parent_dir = os.path.relpath(root, d) |
| 100 | if parent_dir != '.': |
| 101 | archive_path = os.path.join(parent_dir, f) |
| 102 | path = os.path.join(root, f) |
| 103 | if build_utils.MatchesGlob(archive_path, globs): |
| 104 | continue |
| 105 | files_to_zip[archive_path] = path |
| 106 | build_utils.DoZip(files_to_zip.iteritems(), zip_path) |
| 107 | |
| 108 | |
| 109 | def _GenerateRTxt(options, dep_subdirs, gen_dir): |
| 110 | """Generate R.txt file. |
| 111 | |
| 112 | Args: |
| 113 | options: The command-line options tuple. |
| 114 | dep_subdirs: List of directories containing extracted dependency resources. |
| 115 | gen_dir: Locates where the aapt-generated files will go. In particular |
| 116 | the output file is always generated as |{gen_dir}/R.txt|. |
| 117 | """ |
| 118 | # NOTE: This uses aapt rather than aapt2 because 'aapt2 compile' does not |
| 119 | # support the --output-text-symbols option yet (https://crbug.com/820460). |
| 120 | package_command = [options.aapt_path, |
| 121 | 'package', |
| 122 | '-m', |
| 123 | '-M', resource_utils.EMPTY_ANDROID_MANIFEST_PATH, |
| 124 | '--no-crunch', |
| 125 | '--auto-add-overlay', |
| 126 | '--no-version-vectors', |
| 127 | '-I', options.android_sdk_jar, |
| 128 | '--output-text-symbols', gen_dir, |
| 129 | '-J', gen_dir, # Required for R.txt generation. |
| 130 | '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN] |
| 131 | |
| 132 | # Adding all dependencies as sources is necessary for @type/foo references |
| 133 | # to symbols within dependencies to resolve. However, it has the side-effect |
| 134 | # that all Java symbols from dependencies are copied into the new R.java. |
| 135 | # E.g.: It enables an arguably incorrect usage of |
| 136 | # "mypackage.R.id.lib_symbol" where "libpackage.R.id.lib_symbol" would be |
| 137 | # more correct. This is just how Android works. |
| 138 | for d in dep_subdirs: |
| 139 | package_command += ['-S', d] |
| 140 | |
| 141 | for d in options.resource_dirs: |
| 142 | package_command += ['-S', d] |
| 143 | |
| 144 | # Only creates an R.txt |
| 145 | build_utils.CheckOutput( |
| 146 | package_command, print_stdout=False, print_stderr=False) |
| 147 | |
| 148 | |
| 149 | def _GenerateResourcesZip(output_resource_zip, input_resource_dirs, |
| 150 | v14_skip, temp_dir): |
| 151 | """Generate a .resources.zip file fron a list of input resource dirs. |
| 152 | |
| 153 | Args: |
| 154 | output_resource_zip: Path to the output .resources.zip file. |
| 155 | input_resource_dirs: A list of input resource directories. |
| 156 | v14_skip: If False, then v14-compatible resource will also be |
| 157 | generated in |{temp_dir}/v14| and added to the final zip. |
| 158 | temp_dir: Path to temporary directory. |
| 159 | """ |
| 160 | if not v14_skip: |
| 161 | # Generate v14-compatible resources in temp_dir. |
| 162 | v14_dir = os.path.join(temp_dir, 'v14') |
| 163 | build_utils.MakeDirectory(v14_dir) |
| 164 | |
| 165 | for resource_dir in input_resource_dirs: |
| 166 | generate_v14_compatible_resources.GenerateV14Resources( |
| 167 | resource_dir, |
| 168 | v14_dir) |
| 169 | |
| 170 | input_resource_dirs.append(v14_dir) |
| 171 | |
| 172 | _ZipResources(input_resource_dirs, output_resource_zip, |
| 173 | build_utils.AAPT_IGNORE_PATTERN) |
| 174 | |
| 175 | |
| 176 | def _OnStaleMd5(options): |
| 177 | with resource_utils.BuildContext() as build: |
| 178 | if options.r_text_in: |
| 179 | r_txt_path = options.r_text_in |
| 180 | else: |
| 181 | # Extract dependencies to resolve @foo/type references into |
| 182 | # dependent packages. |
| 183 | dep_subdirs = resource_utils.ExtractDeps(options.dependencies_res_zips, |
| 184 | build.deps_dir) |
| 185 | |
| 186 | _GenerateRTxt(options, dep_subdirs, build.gen_dir) |
| 187 | r_txt_path = build.r_txt_path |
| 188 | |
| 189 | # 'aapt' doesn't generate any R.txt file if res/ was empty. |
| 190 | if not os.path.exists(r_txt_path): |
| 191 | build_utils.Touch(r_txt_path) |
| 192 | |
| 193 | if options.r_text_out: |
| 194 | shutil.copyfile(r_txt_path, options.r_text_out) |
| 195 | |
| 196 | if options.srcjar_out: |
| 197 | package = options.custom_package |
| 198 | if not package and options.android_manifest: |
| 199 | package = resource_utils.ExtractPackageFromManifest( |
| 200 | options.android_manifest) |
| 201 | |
| 202 | # Don't create a .java file for the current resource target when no |
| 203 | # package name was provided (either by manifest or build rules). |
| 204 | if package: |
| 205 | # All resource IDs should be non-final here, but the |
| 206 | # onResourcesLoaded() method should only be generated if |
| 207 | # --shared-resources is used. |
| 208 | rjava_build_options = resource_utils.RJavaBuildOptions() |
| 209 | rjava_build_options.ExportAllResources() |
| 210 | rjava_build_options.ExportAllStyleables() |
| 211 | if options.shared_resources: |
| 212 | rjava_build_options.GenerateOnResourcesLoaded() |
| 213 | |
| 214 | resource_utils.CreateRJavaFiles( |
| 215 | build.srcjar_dir, package, r_txt_path, |
| 216 | options.extra_res_packages, |
| 217 | options.extra_r_text_files, |
| 218 | rjava_build_options) |
| 219 | |
| 220 | build_utils.ZipDir(options.srcjar_out, build.srcjar_dir) |
| 221 | |
| 222 | if options.resource_zip_out: |
| 223 | _GenerateResourcesZip(options.resource_zip_out, options.resource_dirs, |
| 224 | options.v14_skip, build.temp_dir) |
| 225 | |
| 226 | |
| 227 | def main(args): |
| 228 | args = build_utils.ExpandFileArgs(args) |
| 229 | options = _ParseArgs(args) |
| 230 | |
| 231 | # Order of these must match order specified in GN so that the correct one |
| 232 | # appears first in the depfile. |
| 233 | possible_output_paths = [ |
| 234 | options.resource_zip_out, |
| 235 | options.r_text_out, |
| 236 | options.srcjar_out, |
| 237 | ] |
| 238 | output_paths = [x for x in possible_output_paths if x] |
| 239 | |
| 240 | # List python deps in input_strings rather than input_paths since the contents |
| 241 | # of them does not change what gets written to the depsfile. |
| 242 | input_strings = options.extra_res_packages + [ |
| 243 | options.custom_package, |
| 244 | options.shared_resources, |
| 245 | options.v14_skip, |
| 246 | ] |
| 247 | |
| 248 | possible_input_paths = [ |
| 249 | options.aapt_path, |
| 250 | options.android_manifest, |
| 251 | options.android_sdk_jar, |
| 252 | ] |
| 253 | input_paths = [x for x in possible_input_paths if x] |
| 254 | input_paths.extend(options.dependencies_res_zips) |
| 255 | input_paths.extend(options.extra_r_text_files) |
| 256 | |
| 257 | # Resource files aren't explicitly listed in GN. Listing them in the depfile |
| 258 | # ensures the target will be marked stale when resource files are removed. |
| 259 | depfile_deps = [] |
| 260 | resource_names = [] |
| 261 | for resource_dir in options.resource_dirs: |
| 262 | for resource_file in build_utils.FindInDirectory(resource_dir, '*'): |
| 263 | # Don't list the empty .keep file in depfile. Since it doesn't end up |
| 264 | # included in the .zip, it can lead to -w 'dupbuild=err' ninja errors |
| 265 | # if ever moved. |
| 266 | if not resource_file.endswith(os.path.join('empty', '.keep')): |
| 267 | input_paths.append(resource_file) |
| 268 | depfile_deps.append(resource_file) |
| 269 | resource_names.append(os.path.relpath(resource_file, resource_dir)) |
| 270 | |
| 271 | # Resource filenames matter to the output, so add them to strings as well. |
| 272 | # This matters if a file is renamed but not changed (http://crbug.com/597126). |
| 273 | input_strings.extend(sorted(resource_names)) |
| 274 | |
| 275 | build_utils.CallAndWriteDepfileIfStale( |
| 276 | lambda: _OnStaleMd5(options), |
| 277 | options, |
| 278 | input_paths=input_paths, |
| 279 | input_strings=input_strings, |
| 280 | output_paths=output_paths, |
| 281 | depfile_deps=depfile_deps) |
| 282 | |
| 283 | |
| 284 | if __name__ == '__main__': |
| 285 | main(sys.argv[1:]) |