3 # Copyright 2014 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.
7 # pylint: disable=C0301
8 """Package resources into an apk.
10 See https://android.googlesource.com/platform/tools/base/+/master/legacy/ant-tasks/src/main/java/com/android/ant/AaptExecTask.java
12 https://android.googlesource.com/platform/sdk/+/master/files/ant/build.xml
14 # pylint: enable=C0301
22 from util
import build_utils
25 # List is generated from the chrome_apk.apk_intermediates.ap_ via:
26 # unzip -l $FILE_AP_ | cut -c31- | grep res/draw | cut -d'/' -f 2 | sort \
27 # | uniq | grep -- -tvdpi- | cut -c10-
28 # and then manually sorted.
29 # Note that we can't just do a cross-product of dimentions because the filenames
30 # become too big and aapt fails to create the files.
31 # This leaves all default drawables (mdpi) in the main apk. Android gets upset
32 # though if any drawables are missing from the default drawables/ directory.
35 'hdpi-v4', # Order matters for output file names.
39 'ldrtl-sw600dp-hdpi-v17',
47 'ldrtl-sw600dp-xhdpi-v17',
55 'ldrtl-sw600dp-xxhdpi-v17',
61 'sw600dp-xxxhdpi-v13',
63 'ldrtl-sw600dp-xxxhdpi-v17',
69 'ldrtl-sw600dp-tvdpi-v17',
75 """Parses command line options.
78 An options object as from optparse.OptionsParser.parse_args()
80 parser
= optparse
.OptionParser()
81 build_utils
.AddDepfileOption(parser
)
82 parser
.add_option('--android-sdk', help='path to the Android SDK folder')
83 parser
.add_option('--aapt-path',
84 help='path to the Android aapt tool')
86 parser
.add_option('--configuration-name',
87 help='Gyp\'s configuration name (Debug or Release).')
89 parser
.add_option('--android-manifest', help='AndroidManifest.xml path')
90 parser
.add_option('--version-code', help='Version code for apk.')
91 parser
.add_option('--version-name', help='Version name for apk.')
95 help='Make a resource package that can be loaded by a different'
96 'application at runtime to access the package\'s resources.')
97 parser
.add_option('--resource-zips',
98 help='zip files containing resources to be packaged')
99 parser
.add_option('--asset-dir',
100 help='directories containing assets to be packaged')
101 parser
.add_option('--no-compress', help='disables compression for the '
102 'given comma separated list of extensions')
104 '--create-density-splits',
106 help='Enables density splits')
107 parser
.add_option('--language-splits',
108 help='GYP list of languages to create splits for')
110 parser
.add_option('--apk-path',
111 help='Path to output (partial) apk.')
113 (options
, args
) = parser
.parse_args()
116 parser
.error('No positional arguments should be given.')
118 # Check that required options have been provided.
119 required_options
= ('android_sdk', 'aapt_path', 'configuration_name',
120 'android_manifest', 'version_code', 'version_name',
123 build_utils
.CheckOptions(options
, parser
, required
=required_options
)
128 def MoveImagesToNonMdpiFolders(res_root
):
129 """Move images from drawable-*-mdpi-* folders to drawable-* folders.
131 Why? http://crbug.com/289843
133 for src_dir_name
in os
.listdir(res_root
):
134 src_components
= src_dir_name
.split('-')
135 if src_components
[0] != 'drawable' or 'mdpi' not in src_components
:
137 src_dir
= os
.path
.join(res_root
, src_dir_name
)
138 if not os
.path
.isdir(src_dir
):
140 dst_components
= [c
for c
in src_components
if c
!= 'mdpi']
141 assert dst_components
!= src_components
142 dst_dir_name
= '-'.join(dst_components
)
143 dst_dir
= os
.path
.join(res_root
, dst_dir_name
)
144 build_utils
.MakeDirectory(dst_dir
)
145 for src_file_name
in os
.listdir(src_dir
):
146 if not src_file_name
.endswith('.png'):
148 src_file
= os
.path
.join(src_dir
, src_file_name
)
149 dst_file
= os
.path
.join(dst_dir
, src_file_name
)
150 assert not os
.path
.lexists(dst_file
)
151 shutil
.move(src_file
, dst_file
)
154 def PackageArgsForExtractedZip(d
):
155 """Returns the aapt args for an extracted resources zip.
157 A resources zip either contains the resources for a single target or for
158 multiple targets. If it is multiple targets merged into one, the actual
159 resource directories will be contained in the subdirectories 0, 1, 2, ...
161 subdirs
= [os
.path
.join(d
, s
) for s
in os
.listdir(d
)]
162 subdirs
= [s
for s
in subdirs
if os
.path
.isdir(s
)]
163 is_multi
= '0' in [os
.path
.basename(s
) for s
in subdirs
]
165 res_dirs
= sorted(subdirs
, key
=lambda p
: int(os
.path
.basename(p
)))
170 MoveImagesToNonMdpiFolders(d
)
171 package_command
+= ['-S', d
]
172 return package_command
175 def RenameDensitySplits(apk_path
):
176 """Renames all density splits to have shorter / predictable names."""
177 for density
, config
in DENSITY_SPLITS
.iteritems():
178 src_path
= '%s_%s' % (apk_path
, '_'.join(config
))
179 dst_path
= '%s_%s' % (apk_path
, density
)
180 if src_path
!= dst_path
:
181 if os
.path
.exists(dst_path
):
183 os
.rename(src_path
, dst_path
)
186 def CheckForMissedConfigs(apk_path
, check_density
, languages
):
187 """Raises an exception if apk_path contains any unexpected configs."""
190 triggers
.extend(re
.compile('-%s' % density
) for density
in DENSITY_SPLITS
)
192 triggers
.extend(re
.compile(r
'-%s\b' % lang
) for lang
in languages
)
193 with zipfile
.ZipFile(apk_path
) as main_apk_zip
:
194 for name
in main_apk_zip
.namelist():
195 for trigger
in triggers
:
196 if trigger
.search(name
) and not 'mipmap-' in name
:
197 raise Exception(('Found config in main apk that should have been ' +
198 'put into a split: %s\nYou need to update ' +
199 'package_resources.py to include this new ' +
200 'config (trigger=%s)') % (name
, trigger
.pattern
))
204 options
= ParseArgs()
205 android_jar
= os
.path
.join(options
.android_sdk
, 'android.jar')
206 aapt
= options
.aapt_path
208 with build_utils
.TempDir() as temp_dir
:
209 package_command
= [aapt
,
211 '--version-code', options
.version_code
,
212 '--version-name', options
.version_name
,
213 '-M', options
.android_manifest
,
216 '--auto-add-overlay',
218 '-F', options
.apk_path
,
219 '--ignore-assets', build_utils
.AAPT_IGNORE_PATTERN
,
222 if options
.no_compress
:
223 for ext
in options
.no_compress
.split(','):
224 package_command
+= ['-0', ext
]
225 if options
.shared_resources
:
226 package_command
.append('--shared-lib')
228 if options
.asset_dir
and os
.path
.exists(options
.asset_dir
):
229 package_command
+= ['-A', options
.asset_dir
]
231 if options
.resource_zips
:
232 dep_zips
= build_utils
.ParseGypList(options
.resource_zips
)
234 subdir
= os
.path
.join(temp_dir
, os
.path
.basename(z
))
235 if os
.path
.exists(subdir
):
236 raise Exception('Resource zip name conflict: ' + os
.path
.basename(z
))
237 build_utils
.ExtractAll(z
, path
=subdir
)
238 package_command
+= PackageArgsForExtractedZip(subdir
)
240 if options
.create_density_splits
:
241 for config
in DENSITY_SPLITS
.itervalues():
242 package_command
.extend(('--split', ','.join(config
)))
244 language_splits
= None
245 if options
.language_splits
:
246 language_splits
= build_utils
.ParseGypList(options
.language_splits
)
247 for lang
in language_splits
:
248 package_command
.extend(('--split', lang
))
250 if 'Debug' in options
.configuration_name
:
251 package_command
+= ['--debug-mode']
253 build_utils
.CheckOutput(
254 package_command
, print_stdout
=False, print_stderr
=False)
256 if options
.create_density_splits
or language_splits
:
257 CheckForMissedConfigs(
258 options
.apk_path
, options
.create_density_splits
, language_splits
)
260 if options
.create_density_splits
:
261 RenameDensitySplits(options
.apk_path
)
264 build_utils
.WriteDepfile(
266 build_utils
.GetPythonDependencies())
269 if __name__
== '__main__':