blob: da86e734152749a6904ef08062715f9743b386e6 [file] [log] [blame]
#!/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<type>\w+)/(?P<name>\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()