#!/usr/bin/python # Copyright (c) 2013 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. """Lists unused Java strings and other resources.""" import optparse import re import subprocess import sys def GetApkResources(apk_path): """Returns the types and names of resources packaged in an APK. Args: apk_path: path to the APK. Returns: The resources in the APK as a list of tuples (type, name). Example: [('drawable', 'arrow'), ('layout', 'month_picker'), ...] """ p = subprocess.Popen( ['aapt', 'dump', 'resources', apk_path], stdout=subprocess.PIPE) dump_out, _ = p.communicate() assert p.returncode == 0, 'aapt dump failed' matches = re.finditer( r'^\s+spec resource 0x[0-9a-fA-F]+ [\w.]+:(?P\w+)/(?P\w+)', dump_out, re.MULTILINE) return [m.group('type', 'name') for m in matches] def GetUsedResources(source_paths, resource_types): """Returns the types and names of resources used in Java or resource files. Args: source_paths: a list of files or folders collectively containing all the Java files, resource files, and the AndroidManifest.xml. resource_types: a list of resource types to look for. Example: ['string', 'drawable'] Returns: The resources referenced by the Java and resource files as a list of tuples (type, name). Example: [('drawable', 'app_icon'), ('layout', 'month_picker'), ...] """ type_regex = '|'.join(map(re.escape, resource_types)) patterns = [r'@(())(%s)/(\w+)' % type_regex, r'\b((\w+\.)*)R\.(%s)\.(\w+)' % type_regex] resources = [] for pattern in patterns: p = subprocess.Popen( ['grep', '-REIhoe', pattern] + source_paths, stdout=subprocess.PIPE) grep_out, grep_err = p.communicate() # Check stderr instead of return code, since return code is 1 when no # matches are found. assert not grep_err, 'grep failed' matches = re.finditer(pattern, grep_out) for match in matches: package = match.group(1) if package == 'android.': continue type_ = match.group(3) name = match.group(4) resources.append((type_, name)) return resources def FormatResources(resources): """Formats a list of resources for printing. Args: resources: a list of resources, given as (type, name) tuples. """ return '\n'.join(['%-12s %s' % (t, n) for t, n in sorted(resources)]) def ParseArgs(args): usage = 'usage: %prog [-v] APK_PATH SOURCE_PATH...' parser = optparse.OptionParser(usage=usage) parser.add_option('-v', help='Show verbose output', action='store_true') options, args = parser.parse_args(args=args) if len(args) < 2: parser.error('must provide APK_PATH and SOURCE_PATH arguments') return options.v, args[0], args[1:] def main(args=None): verbose, apk_path, source_paths = ParseArgs(args) apk_resources = GetApkResources(apk_path) resource_types = list(set([r[0] for r in apk_resources])) used_resources = GetUsedResources(source_paths, resource_types) unused_resources = set(apk_resources) - set(used_resources) undefined_resources = set(used_resources) - set(apk_resources) # aapt dump fails silently. Notify the user if things look wrong. if not apk_resources: print >> sys.stderr, ( 'Warning: No resources found in the APK. Did you provide the correct ' 'APK path?') if not used_resources: print >> sys.stderr, ( 'Warning: No resources references from Java or resource files. Did you ' 'provide the correct source paths?') if undefined_resources: print >> sys.stderr, ( 'Warning: found %d "undefined" resources that are referenced by Java ' 'files or by other resources, but are not in the APK. Run with -v to ' 'see them.' % len(undefined_resources)) if verbose: print '%d undefined resources:' % len(undefined_resources) print FormatResources(undefined_resources), '\n' print '%d resources packaged into the APK:' % len(apk_resources) print FormatResources(apk_resources), '\n' print '%d used resources:' % len(used_resources) print FormatResources(used_resources), '\n' print '%d unused resources:' % len(unused_resources) print FormatResources(unused_resources) if __name__ == '__main__': main()