#!/usr/bin/env python # # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Process Android resource directories to generate .resources.zip, R.txt and .srcjar files.""" import argparse import collections import os import re import shutil import sys import generate_v14_compatible_resources from util import build_utils from util import resource_utils _AAPT_IGNORE_PATTERN = ':'.join([ 'OWNERS', # Allow OWNERS files within res/ '*.py', # PRESUBMIT.py sometimes exist. '*.pyc', '*~', # Some editors create these as temp files. '.*', # Never makes sense to include dot(files/dirs). '*.d.stamp', # Ignore stamp files ]) def _ParseArgs(args): """Parses command line options. Returns: An options object as from argparse.ArgumentParser.parse_args() """ parser, input_opts, output_opts = resource_utils.ResourceArgsParser() input_opts.add_argument('--resource-dirs', default='[]', help='A list of input directories containing resources ' 'for this target.') input_opts.add_argument( '--shared-resources', action='store_true', help='Make resources shareable by generating an onResourcesLoaded() ' 'method in the R.java source file.') input_opts.add_argument('--custom-package', help='Optional Java package for main R.java.') input_opts.add_argument( '--android-manifest', help='Optional AndroidManifest.xml path. Only used to extract a package ' 'name for R.java if a --custom-package is not provided.') output_opts.add_argument( '--resource-zip-out', help='Path to a zip archive containing all resources from ' '--resource-dirs, merged into a single directory tree. This will ' 'also include auto-generated v14-compatible resources unless ' '--v14-skip is used.') output_opts.add_argument('--srcjar-out', help='Path to .srcjar to contain the generated R.java.') output_opts.add_argument('--r-text-out', help='Path to store the generated R.txt file.') input_opts.add_argument( '--v14-skip', action="store_true", help='Do not generate nor verify v14 resources.') options = parser.parse_args(args) resource_utils.HandleCommonOptions(options) options.resource_dirs = build_utils.ParseGnList(options.resource_dirs) return options def _GenerateGlobs(pattern): # This function processes the aapt ignore assets pattern into a list of globs # to be used to exclude files on the python side. It removes the '!', which is # used by aapt to mean 'not chatty' so it does not output if the file is # ignored (we dont output anyways, so it is not required). This function does # not handle the and prefixes used by aapt and are assumed not to # be included in the pattern string. return pattern.replace('!', '').split(':') def _ZipResources(resource_dirs, zip_path, ignore_pattern): # Python zipfile does not provide a way to replace a file (it just writes # another file with the same name). So, first collect all the files to put # in the zip (with proper overriding), and then zip them. # ignore_pattern is a string of ':' delimited list of globs used to ignore # files that should not be part of the final resource zip. files_to_zip = dict() files_to_zip_without_generated = dict() globs = _GenerateGlobs(ignore_pattern) for d in resource_dirs: for root, _, files in os.walk(d): for f in files: archive_path = f parent_dir = os.path.relpath(root, d) if parent_dir != '.': archive_path = os.path.join(parent_dir, f) path = os.path.join(root, f) if build_utils.MatchesGlob(archive_path, globs): continue # We want the original resource dirs in the .info file rather than the # generated overridden path. if not path.startswith('/tmp'): files_to_zip_without_generated[archive_path] = path files_to_zip[archive_path] = path resource_utils.CreateResourceInfoFile(files_to_zip_without_generated, zip_path) build_utils.DoZip(files_to_zip.iteritems(), zip_path) def _GenerateRTxt(options, dep_subdirs, gen_dir): """Generate R.txt file. Args: options: The command-line options tuple. dep_subdirs: List of directories containing extracted dependency resources. gen_dir: Locates where the aapt-generated files will go. In particular the output file is always generated as |{gen_dir}/R.txt|. """ # NOTE: This uses aapt rather than aapt2 because 'aapt2 compile' does not # support the --output-text-symbols option yet (https://crbug.com/820460). package_command = [options.aapt_path, 'package', '-m', '-M', resource_utils.EMPTY_ANDROID_MANIFEST_PATH, '--no-crunch', '--auto-add-overlay', '--no-version-vectors', ] for j in options.android_sdk_jars: package_command += ['-I', j] package_command += [ '--output-text-symbols', gen_dir, '-J', gen_dir, # Required for R.txt generation. '--ignore-assets', _AAPT_IGNORE_PATTERN] # Adding all dependencies as sources is necessary for @type/foo references # to symbols within dependencies to resolve. However, it has the side-effect # that all Java symbols from dependencies are copied into the new R.java. # E.g.: It enables an arguably incorrect usage of # "mypackage.R.id.lib_symbol" where "libpackage.R.id.lib_symbol" would be # more correct. This is just how Android works. for d in dep_subdirs: package_command += ['-S', d] for d in options.resource_dirs: package_command += ['-S', d] # Only creates an R.txt build_utils.CheckOutput( package_command, print_stdout=False, print_stderr=False) def _GenerateResourcesZip(output_resource_zip, input_resource_dirs, v14_skip, temp_dir): """Generate a .resources.zip file fron a list of input resource dirs. Args: output_resource_zip: Path to the output .resources.zip file. input_resource_dirs: A list of input resource directories. v14_skip: If False, then v14-compatible resource will also be generated in |{temp_dir}/v14| and added to the final zip. temp_dir: Path to temporary directory. """ if not v14_skip: # Generate v14-compatible resources in temp_dir. v14_dir = os.path.join(temp_dir, 'v14') build_utils.MakeDirectory(v14_dir) for resource_dir in input_resource_dirs: generate_v14_compatible_resources.GenerateV14Resources( resource_dir, v14_dir) input_resource_dirs.append(v14_dir) _ZipResources(input_resource_dirs, output_resource_zip, _AAPT_IGNORE_PATTERN) def _OnStaleMd5(options): with resource_utils.BuildContext() as build: if options.r_text_in: r_txt_path = options.r_text_in else: # Extract dependencies to resolve @foo/type references into # dependent packages. dep_subdirs = resource_utils.ExtractDeps(options.dependencies_res_zips, build.deps_dir) _GenerateRTxt(options, dep_subdirs, build.gen_dir) r_txt_path = build.r_txt_path # 'aapt' doesn't generate any R.txt file if res/ was empty. if not os.path.exists(r_txt_path): build_utils.Touch(r_txt_path) if options.r_text_out: shutil.copyfile(r_txt_path, options.r_text_out) if options.srcjar_out: package = options.custom_package if not package and options.android_manifest: package = resource_utils.ExtractPackageFromManifest( options.android_manifest) # Don't create a .java file for the current resource target when no # package name was provided (either by manifest or build rules). if package: # All resource IDs should be non-final here, but the # onResourcesLoaded() method should only be generated if # --shared-resources is used. rjava_build_options = resource_utils.RJavaBuildOptions() rjava_build_options.ExportAllResources() rjava_build_options.ExportAllStyleables() if options.shared_resources: rjava_build_options.GenerateOnResourcesLoaded() resource_utils.CreateRJavaFiles( build.srcjar_dir, package, r_txt_path, options.extra_res_packages, options.extra_r_text_files, rjava_build_options) build_utils.ZipDir(options.srcjar_out, build.srcjar_dir) if options.resource_zip_out: _GenerateResourcesZip(options.resource_zip_out, options.resource_dirs, options.v14_skip, build.temp_dir) def main(args): args = build_utils.ExpandFileArgs(args) options = _ParseArgs(args) # Order of these must match order specified in GN so that the correct one # appears first in the depfile. possible_output_paths = [ options.resource_zip_out, options.r_text_out, options.srcjar_out, ] output_paths = [x for x in possible_output_paths if x] # List python deps in input_strings rather than input_paths since the contents # of them does not change what gets written to the depsfile. input_strings = options.extra_res_packages + [ options.custom_package, options.shared_resources, options.v14_skip, ] possible_input_paths = [ options.aapt_path, options.android_manifest, ] possible_input_paths += options.android_sdk_jars input_paths = [x for x in possible_input_paths if x] input_paths.extend(options.dependencies_res_zips) input_paths.extend(options.extra_r_text_files) # Resource files aren't explicitly listed in GN. Listing them in the depfile # ensures the target will be marked stale when resource files are removed. depfile_deps = [] resource_names = [] for resource_dir in options.resource_dirs: for resource_file in build_utils.FindInDirectory(resource_dir, '*'): # Don't list the empty .keep file in depfile. Since it doesn't end up # included in the .zip, it can lead to -w 'dupbuild=err' ninja errors # if ever moved. if not resource_file.endswith(os.path.join('empty', '.keep')): input_paths.append(resource_file) depfile_deps.append(resource_file) resource_names.append(os.path.relpath(resource_file, resource_dir)) # Resource filenames matter to the output, so add them to strings as well. # This matters if a file is renamed but not changed (http://crbug.com/597126). input_strings.extend(sorted(resource_names)) build_utils.CallAndWriteDepfileIfStale( lambda: _OnStaleMd5(options), options, input_paths=input_paths, input_strings=input_strings, output_paths=output_paths, depfile_deps=depfile_deps, add_pydeps=False) if __name__ == '__main__': main(sys.argv[1:])