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