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.
7 """Process Android resources to generate R.java, and prepare for packaging.
9 This will crunch images and generate v14 compatible resources
10 (see generate_v14_compatible_resources.py).
21 import generate_v14_compatible_resources
23 from util
import build_utils
25 # Import jinja2 from third_party/jinja2
26 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
), '../../../third_party'))
27 from jinja2
import Template
# pylint: disable=F0401
31 """Parses command line options.
34 An options object as from optparse.OptionsParser.parse_args()
36 parser
= optparse
.OptionParser()
37 build_utils
.AddDepfileOption(parser
)
39 parser
.add_option('--android-sdk', help='path to the Android SDK folder')
40 parser
.add_option('--android-sdk-tools',
41 help='path to the Android SDK build tools folder')
42 parser
.add_option('--non-constant-id', action
='store_true')
44 parser
.add_option('--android-manifest', help='AndroidManifest.xml path')
45 parser
.add_option('--custom-package', help='Java package for R.java')
49 help='Make a resource package that can be loaded by a different'
50 'application at runtime to access the package\'s resources.')
52 parser
.add_option('--resource-dirs',
53 help='Directories containing resources of this target.')
54 parser
.add_option('--dependencies-res-zips',
55 help='Resources from dependents.')
57 parser
.add_option('--resource-zip-out',
58 help='Path for output zipped resources.')
60 parser
.add_option('--R-dir',
61 help='directory to hold generated R.java.')
62 parser
.add_option('--srcjar-out',
63 help='Path to srcjar to contain generated R.java.')
64 parser
.add_option('--r-text-out',
65 help='Path to store the R.txt file generated by appt.')
67 parser
.add_option('--proguard-file',
68 help='Path to proguard.txt generated file')
73 help='Do not generate v14 resources. Instead, just verify that the '
74 'resources are already compatible with v14, i.e. they don\'t use '
75 'attributes that cause crashes on certain devices.')
78 '--extra-res-packages',
79 help='Additional package names to generate R.java files for')
81 '--extra-r-text-files',
82 help='For each additional package, the R.txt file should contain a '
83 'list of resources to be included in the R.java file in the format '
86 '--include-all-resources',
88 help='Include every resource ID in every generated R.java file '
92 '--all-resources-zip-out',
93 help='Path for output of all resources. This includes resources in '
96 parser
.add_option('--stamp', help='File to touch on success')
98 (options
, args
) = parser
.parse_args(args
)
101 parser
.error('No positional arguments should be given.')
103 # Check that required options have been provided.
108 'dependencies_res_zips',
112 build_utils
.CheckOptions(options
, parser
, required
=required_options
)
114 if (options
.R_dir
is None) == (options
.srcjar_out
is None):
115 raise Exception('Exactly one of --R-dir or --srcjar-out must be specified.')
120 def CreateExtraRJavaFiles(
121 r_dir
, extra_packages
, extra_r_text_files
, shared_resources
, include_all
):
123 java_files
= build_utils
.FindInDirectory(r_dir
, "R.java")
124 if len(java_files
) != 1:
126 r_java_file
= java_files
[0]
127 r_java_contents
= codecs
.open(r_java_file
, encoding
='utf-8').read()
129 for package
in extra_packages
:
130 package_r_java_dir
= os
.path
.join(r_dir
, *package
.split('.'))
131 build_utils
.MakeDirectory(package_r_java_dir
)
132 package_r_java_path
= os
.path
.join(package_r_java_dir
, 'R.java')
133 new_r_java
= re
.sub(r
'package [.\w]*;', u
'package %s;' % package
,
135 codecs
.open(package_r_java_path
, 'w', encoding
='utf-8').write(new_r_java
)
137 if len(extra_packages
) != len(extra_r_text_files
):
138 raise Exception('Need one R.txt file per extra package')
141 r_txt_file
= os
.path
.join(r_dir
, 'R.txt')
142 if not os
.path
.exists(r_txt_file
):
144 with
open(r_txt_file
) as f
:
146 m
= re
.match(r
'(int(?:\[\])?) (\w+) (\w+) (.+)$', line
)
148 raise Exception('Unexpected line in R.txt: %s' % line
)
149 java_type
, resource_type
, name
, value
= m
.groups()
150 all_resources
[(resource_type
, name
)] = (java_type
, value
)
152 for package
, r_text_file
in zip(extra_packages
, extra_r_text_files
):
153 if os
.path
.exists(r_text_file
):
154 package_r_java_dir
= os
.path
.join(r_dir
, *package
.split('.'))
155 build_utils
.MakeDirectory(package_r_java_dir
)
156 package_r_java_path
= os
.path
.join(package_r_java_dir
, 'R.java')
157 CreateExtraRJavaFile(
158 package
, package_r_java_path
, r_text_file
, all_resources
,
162 def CreateExtraRJavaFile(
163 package
, r_java_path
, r_text_file
, all_resources
, shared_resources
):
165 with
open(r_text_file
) as f
:
167 m
= re
.match(r
'int(?:\[\])? (\w+) (\w+) ', line
)
169 raise Exception('Unexpected line in R.txt: %s' % line
)
170 resource_type
, name
= m
.groups()
171 java_type
, value
= all_resources
[(resource_type
, name
)]
172 if resource_type
not in resources
:
173 resources
[resource_type
] = []
174 resources
[resource_type
].append((name
, java_type
, value
))
176 template
= Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */
178 package {{ package }};
180 public final class R {
181 {% for resource_type in resources %}
182 public static final class {{ resource_type }} {
183 {% for name, java_type, value in resources[resource_type] %}
184 {% if shared_resources %}
185 public static {{ java_type }} {{ name }} = {{ value }};
187 public static final {{ java_type }} {{ name }} = {{ value }};
192 {% if shared_resources %}
193 public static void onResourcesLoaded(int packageId) {
194 {% for resource_type in resources %}
195 {% for name, java_type, value in resources[resource_type] %}
196 {% if java_type == 'int[]' %}
197 for(int i = 0; i < {{ resource_type }}.{{ name }}.length; ++i) {
198 {{ resource_type }}.{{ name }}[i] =
199 ({{ resource_type }}.{{ name }}[i] & 0x00ffffff)
203 {{ resource_type }}.{{ name }} =
204 ({{ resource_type }}.{{ name }} & 0x00ffffff)
212 """, trim_blocks
=True, lstrip_blocks
=True)
214 output
= template
.render(package
=package
, resources
=resources
,
215 shared_resources
=shared_resources
)
216 with
open(r_java_path
, 'w') as f
:
220 def CrunchDirectory(aapt
, input_dir
, output_dir
):
221 """Crunches the images in input_dir and its subdirectories into output_dir.
223 If an image is already optimized, crunching often increases image size. In
224 this case, the crunched image is overwritten with the original image.
230 '--ignore-assets', build_utils
.AAPT_IGNORE_PATTERN
]
231 build_utils
.CheckOutput(aapt_cmd
, stderr_filter
=FilterCrunchStderr
,
232 fail_func
=DidCrunchFail
)
234 # Check for images whose size increased during crunching and replace them
235 # with their originals (except for 9-patches, which must be crunched).
236 for dir_
, _
, files
in os
.walk(output_dir
):
237 for crunched
in files
:
238 if crunched
.endswith('.9.png'):
240 if not crunched
.endswith('.png'):
241 raise Exception('Unexpected file in crunched dir: ' + crunched
)
242 crunched
= os
.path
.join(dir_
, crunched
)
243 original
= os
.path
.join(input_dir
, os
.path
.relpath(crunched
, output_dir
))
244 original_size
= os
.path
.getsize(original
)
245 crunched_size
= os
.path
.getsize(crunched
)
246 if original_size
< crunched_size
:
247 shutil
.copyfile(original
, crunched
)
250 def FilterCrunchStderr(stderr
):
251 """Filters out lines from aapt crunch's stderr that can safely be ignored."""
253 for line
in stderr
.splitlines(True):
254 # Ignore this libpng warning, which is a known non-error condition.
255 # http://crbug.com/364355
256 if ('libpng warning: iCCP: Not recognizing known sRGB profile that has '
257 + 'been edited' in line
):
259 filtered_lines
.append(line
)
260 return ''.join(filtered_lines
)
263 def DidCrunchFail(returncode
, stderr
):
264 """Determines whether aapt crunch failed from its return code and output.
266 Because aapt's return code cannot be trusted, any output to stderr is
267 an indication that aapt has failed (http://crbug.com/314885).
269 return returncode
!= 0 or stderr
272 def ZipResources(resource_dirs
, zip_path
):
273 # Python zipfile does not provide a way to replace a file (it just writes
274 # another file with the same name). So, first collect all the files to put
275 # in the zip (with proper overriding), and then zip them.
276 files_to_zip
= dict()
277 for d
in resource_dirs
:
278 for root
, _
, files
in os
.walk(d
):
280 archive_path
= os
.path
.join(os
.path
.relpath(root
, d
), f
)
281 path
= os
.path
.join(root
, f
)
282 files_to_zip
[archive_path
] = path
283 with zipfile
.ZipFile(zip_path
, 'w') as outzip
:
284 for archive_path
, path
in files_to_zip
.iteritems():
285 outzip
.write(path
, archive_path
)
288 def CombineZips(zip_files
, output_path
):
289 # When packaging resources, if the top-level directories in the zip file are
290 # of the form 0, 1, ..., then each subdirectory will be passed to aapt as a
291 # resources directory. While some resources just clobber others (image files,
292 # etc), other resources (particularly .xml files) need to be more
293 # intelligently merged. That merging is left up to aapt.
294 with zipfile
.ZipFile(output_path
, 'w') as outzip
:
295 for i
, z
in enumerate(zip_files
):
296 with zipfile
.ZipFile(z
, 'r') as inzip
:
297 for name
in inzip
.namelist():
298 new_name
= '%d/%s' % (i
, name
)
299 outzip
.writestr(new_name
, inzip
.read(name
))
303 args
= build_utils
.ExpandFileArgs(sys
.argv
[1:])
305 options
= ParseArgs(args
)
306 android_jar
= os
.path
.join(options
.android_sdk
, 'android.jar')
307 aapt
= os
.path
.join(options
.android_sdk_tools
, 'aapt')
311 with build_utils
.TempDir() as temp_dir
:
312 deps_dir
= os
.path
.join(temp_dir
, 'deps')
313 build_utils
.MakeDirectory(deps_dir
)
314 v14_dir
= os
.path
.join(temp_dir
, 'v14')
315 build_utils
.MakeDirectory(v14_dir
)
317 gen_dir
= os
.path
.join(temp_dir
, 'gen')
318 build_utils
.MakeDirectory(gen_dir
)
320 input_resource_dirs
= build_utils
.ParseGypList(options
.resource_dirs
)
322 for resource_dir
in input_resource_dirs
:
323 generate_v14_compatible_resources
.GenerateV14Resources(
326 options
.v14_verify_only
)
328 dep_zips
= build_utils
.ParseGypList(options
.dependencies_res_zips
)
329 input_files
+= dep_zips
332 subdir
= os
.path
.join(deps_dir
, os
.path
.basename(z
))
333 if os
.path
.exists(subdir
):
334 raise Exception('Resource zip name conflict: ' + os
.path
.basename(z
))
335 build_utils
.ExtractAll(z
, path
=subdir
)
336 dep_subdirs
.append(subdir
)
338 # Generate R.java. This R.java contains non-final constants and is used only
339 # while compiling the library jar (e.g. chromium_content.jar). When building
340 # an apk, a new R.java file with the correct resource -> ID mappings will be
341 # generated by merging the resources from all libraries and the main apk
343 package_command
= [aapt
,
346 '-M', options
.android_manifest
,
347 '--auto-add-overlay',
349 '--output-text-symbols', gen_dir
,
351 '--ignore-assets', build_utils
.AAPT_IGNORE_PATTERN
]
353 for d
in input_resource_dirs
:
354 package_command
+= ['-S', d
]
356 for d
in dep_subdirs
:
357 package_command
+= ['-S', d
]
359 if options
.non_constant_id
:
360 package_command
.append('--non-constant-id')
361 if options
.custom_package
:
362 package_command
+= ['--custom-package', options
.custom_package
]
363 if options
.proguard_file
:
364 package_command
+= ['-G', options
.proguard_file
]
365 if options
.shared_resources
:
366 package_command
.append('--shared-lib')
367 build_utils
.CheckOutput(package_command
, print_stderr
=False)
369 if options
.extra_res_packages
:
370 CreateExtraRJavaFiles(
372 build_utils
.ParseGypList(options
.extra_res_packages
),
373 build_utils
.ParseGypList(options
.extra_r_text_files
),
374 options
.shared_resources
,
375 options
.include_all_resources
)
377 # This is the list of directories with resources to put in the final .zip
378 # file. The order of these is important so that crunched/v14 resources
379 # override the normal ones.
380 zip_resource_dirs
= input_resource_dirs
+ [v14_dir
]
382 base_crunch_dir
= os
.path
.join(temp_dir
, 'crunch')
384 # Crunch image resources. This shrinks png files and is necessary for
385 # 9-patch images to display correctly. 'aapt crunch' accepts only a single
386 # directory at a time and deletes everything in the output directory.
387 for idx
, input_dir
in enumerate(input_resource_dirs
):
388 crunch_dir
= os
.path
.join(base_crunch_dir
, str(idx
))
389 build_utils
.MakeDirectory(crunch_dir
)
390 zip_resource_dirs
.append(crunch_dir
)
391 CrunchDirectory(aapt
, input_dir
, crunch_dir
)
393 ZipResources(zip_resource_dirs
, options
.resource_zip_out
)
395 if options
.all_resources_zip_out
:
396 CombineZips([options
.resource_zip_out
] + dep_zips
,
397 options
.all_resources_zip_out
)
400 build_utils
.DeleteDirectory(options
.R_dir
)
401 shutil
.copytree(gen_dir
, options
.R_dir
)
403 build_utils
.ZipDir(options
.srcjar_out
, gen_dir
)
405 if options
.r_text_out
:
406 r_text_path
= os
.path
.join(gen_dir
, 'R.txt')
407 if os
.path
.exists(r_text_path
):
408 shutil
.copyfile(r_text_path
, options
.r_text_out
)
410 open(options
.r_text_out
, 'w').close()
413 input_files
+= build_utils
.GetPythonDependencies()
414 build_utils
.WriteDepfile(options
.depfile
, input_files
)
417 build_utils
.Touch(options
.stamp
)
420 if __name__
== '__main__':