3 # Copyright (c) 2013 Intel Corporation. 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 import compress_js_and_css
17 from app_info import AppInfo
18 from customize_launch_screen import CustomizeLaunchScreen
19 from handle_xml import AddElementAttribute
20 from handle_xml import AddElementAttributeAndText
21 from handle_xml import EditElementAttribute
22 from handle_xml import EditElementValueByNodeName
23 from handle_permissions import HandlePermissions
24 from xml.dom import minidom
27 def VerifyAppName(value, mode='default'):
29 sample = 'helloworld, hello world, hello_world, hello_world1'
30 regex = r'[a-zA-Z][\w ]*$'
33 print('To be safe, the length of package name or app name '
34 'should be less than 128.')
36 if mode == 'packagename':
37 regex = r'^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$'
38 descrpt = 'Each part of package'
39 sample = 'org.xwalk.example, org.xwalk.example_'
41 if not re.match(regex, value):
42 print('Error: %s name should be started with letters and should not '
43 'contain invalid characters.\n'
44 'It may contain lowercase letters, numbers, blank spaces and '
46 'Sample: %s' % (descrpt, sample))
50 def ReplaceSpaceWithUnderscore(value):
51 return value.replace(' ', '_')
54 def ReplaceInvalidChars(value, mode='default'):
55 """ Replace the invalid chars with '_' for input string.
57 value: the original string.
58 mode: the target usage mode of original string.
61 invalid_chars = '\/:*?"<>|- '
62 elif mode == 'apkname':
63 invalid_chars = '\/:.*?"<>|- '
64 for c in invalid_chars:
65 if mode == 'apkname' and c in value:
66 print("Illegal character: '%s' is replaced with '_'" % c)
67 value = value.replace(c, '_')
71 def GetFilesByExt(path, ext, sub_dir=True):
72 if os.path.exists(path):
74 for name in os.listdir(path):
75 full_name = os.path.join(path, name)
76 st = os.lstat(full_name)
77 if stat.S_ISDIR(st.st_mode) and sub_dir:
78 file_list += GetFilesByExt(full_name, ext)
79 elif os.path.isfile(full_name):
80 if fnmatch.fnmatch(full_name, ext):
81 file_list.append(full_name)
87 def ParseParameterForCompressor(option, value, values, parser):
88 if ((not values or values.startswith('-'))
89 and value.find('--compressor') != -1):
92 if parser.rargs and not parser.rargs[0].startswith('-'):
95 setattr(parser.values, option.dest, val)
98 def CompressSourceFiles(app_root, compressor):
104 if compressor == 'all' or compressor == 'js':
105 js_list = GetFilesByExt(app_root, js_ext)
106 compress_js_and_css.CompressJavaScript(js_list)
108 if compressor == 'all' or compressor == 'css':
109 css_list = GetFilesByExt(app_root, css_ext)
110 compress_js_and_css.CompressCss(css_list)
113 def Prepare(app_info, compressor):
114 name = app_info.android_name
115 package = app_info.package
116 app_root = app_info.app_root
117 if os.path.exists(name):
119 shutil.copytree('app_src', name)
120 shutil.rmtree(os.path.join(name, 'src'))
121 src_root = os.path.join('app_src', 'src', 'org', 'xwalk', 'app', 'template')
122 src_activity = os.path.join(src_root, 'AppTemplateActivity.java')
123 if not os.path.isfile(src_activity):
124 print ('Please make sure that the java file'
125 ' of activity does exist.')
127 root_path = os.path.join(name, 'src', package.replace('.', os.path.sep))
128 if not os.path.exists(root_path):
129 os.makedirs(root_path)
130 dest_activity = name + 'Activity.java'
131 shutil.copyfile(src_activity, os.path.join(root_path, dest_activity))
133 assets_path = os.path.join(name, 'assets')
134 shutil.rmtree(assets_path)
135 os.makedirs(assets_path)
136 app_src_path = os.path.join(assets_path, 'www')
137 shutil.copytree(app_root, app_src_path)
139 CompressSourceFiles(app_src_path, compressor)
142 def CustomizeStringXML(name, description):
143 strings_path = os.path.join(name, 'res', 'values', 'strings.xml')
144 if not os.path.isfile(strings_path):
145 print ('Please make sure strings_xml'
146 ' exists under app_src folder.')
150 xmldoc = minidom.parse(strings_path)
151 AddElementAttributeAndText(xmldoc, 'string', 'name', 'description',
153 strings_file = open(strings_path, 'w')
154 xmldoc.writexml(strings_file, encoding='utf-8')
158 def CustomizeThemeXML(name, fullscreen, manifest):
159 theme_path = os.path.join(name, 'res', 'values-v17', 'theme.xml')
160 if not os.path.isfile(theme_path):
161 print('Error: theme.xml is missing in the build tool.')
164 theme_xmldoc = minidom.parse(theme_path)
166 EditElementValueByNodeName(theme_xmldoc, 'item',
167 'android:windowFullscreen', 'true')
168 has_background = CustomizeLaunchScreen(manifest, name)
170 EditElementValueByNodeName(theme_xmldoc, 'item',
171 'android:windowBackground',
172 '@drawable/launchscreen_bg')
173 theme_file = open(theme_path, 'w')
174 theme_xmldoc.writexml(theme_file, encoding='utf-8')
178 def CustomizeXML(app_info, description, icon_dict, manifest, permissions):
179 app_version = app_info.app_version
180 app_versionCode = app_info.app_versionCode
181 name = app_info.android_name
182 orientation = app_info.orientation
183 package = app_info.package
184 app_name = app_info.app_name
185 manifest_path = os.path.join(name, 'AndroidManifest.xml')
186 if not os.path.isfile(manifest_path):
187 print ('Please make sure AndroidManifest.xml'
188 ' exists under app_src folder.')
191 CustomizeStringXML(name, description)
192 CustomizeThemeXML(name, app_info.fullscreen_flag, manifest)
193 xmldoc = minidom.parse(manifest_path)
194 EditElementAttribute(xmldoc, 'manifest', 'package', package)
196 EditElementAttribute(xmldoc, 'manifest', 'android:versionCode',
197 str(app_versionCode))
199 EditElementAttribute(xmldoc, 'manifest', 'android:versionName',
202 EditElementAttribute(xmldoc, 'manifest', 'android:description',
203 "@string/description")
204 HandlePermissions(permissions, xmldoc)
205 EditElementAttribute(xmldoc, 'application', 'android:label', app_name)
206 activity_name = package + '.' + name + 'Activity'
207 EditElementAttribute(xmldoc, 'activity', 'android:name', activity_name)
208 EditElementAttribute(xmldoc, 'activity', 'android:label', app_name)
210 EditElementAttribute(xmldoc, 'activity', 'android:screenOrientation',
212 icon_name = CustomizeIcon(name, app_info.app_root, app_info.icon, icon_dict)
214 EditElementAttribute(xmldoc, 'application', 'android:icon',
215 '@drawable/%s' % icon_name)
217 file_handle = open(os.path.join(name, 'AndroidManifest.xml'), 'w')
218 xmldoc.writexml(file_handle, encoding='utf-8')
222 def ReplaceString(file_path, src, dest):
223 file_handle = open(file_path, 'r')
224 src_content = file_handle.read()
226 file_handle = open(file_path, 'w')
227 dest_content = src_content.replace(src, dest)
228 file_handle.write(dest_content)
232 def SetVariable(file_path, string_line, variable, value):
233 function_string = ('%sset%s(%s);\n' %
234 (' ', variable, value))
235 temp_file_path = file_path + '.backup'
236 file_handle = open(temp_file_path, 'w+')
237 for line in open(file_path):
238 file_handle.write(line)
239 if (line.find(string_line) >= 0):
240 file_handle.write(function_string)
242 shutil.move(temp_file_path, file_path)
245 def CustomizeJava(app_info, app_url, app_local_path, keep_screen_on):
246 name = app_info.android_name
247 package = app_info.package
248 root_path = os.path.join(name, 'src', package.replace('.', os.path.sep))
249 dest_activity = os.path.join(root_path, name + 'Activity.java')
250 ReplaceString(dest_activity, 'org.xwalk.app.template', package)
251 ReplaceString(dest_activity, 'AppTemplate', name)
252 manifest_file = os.path.join(name, 'assets/www', 'manifest.json')
253 if os.path.isfile(manifest_file):
256 'loadAppFromUrl("file:///android_asset/www/index.html")',
257 'loadAppFromManifest("file:///android_asset/www/manifest.json")')
260 if re.search(r'^http(|s)', app_url):
261 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
264 if os.path.isfile(os.path.join(name, 'assets/www', app_local_path)):
265 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
266 'app://' + package + '/' + app_local_path)
268 print ('Please make sure that the relative path of entry file'
272 if app_info.remote_debugging:
273 SetVariable(dest_activity,
274 'public void onCreate(Bundle savedInstanceState)',
275 'RemoteDebugging', 'true')
276 if app_info.fullscreen_flag:
277 SetVariable(dest_activity,
278 'super.onCreate(savedInstanceState)',
279 'IsFullscreen', 'true')
283 'super.onCreate(savedInstanceState);',
284 'super.onCreate(savedInstanceState);\n ' +
285 'getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);')
288 def CopyExtensionFile(extension_name, suffix, src_path, dest_path):
289 # Copy the file from src_path into dest_path.
290 dest_extension_path = os.path.join(dest_path, extension_name)
291 if os.path.exists(dest_extension_path):
292 # TODO: Refine it by renaming it internally.
293 print('Error: duplicated extension names "%s" are found. Please rename it.'
297 os.mkdir(dest_extension_path)
299 file_name = extension_name + suffix
300 src_file = os.path.join(src_path, file_name)
301 dest_file = os.path.join(dest_extension_path, file_name)
302 if not os.path.isfile(src_file):
303 print('Error: %s was not found in %s.' % (file_name, src_path))
306 shutil.copyfile(src_file, dest_file)
309 def CustomizeExtensions(app_info, extensions):
310 """Copy the files from external extensions and merge them into APK.
312 The directory of one external extension should be like:
317 That means the name of the internal files should be the same as the
319 For .jar files, they'll be copied to xwalk-extensions/ and then
320 built into classes.dex in make_apk.py.
321 For .js files, they'll be copied into assets/xwalk-extensions/.
322 For .json files, the'll be merged into one file called
323 extensions-config.json and copied into assets/.
327 name = app_info.android_name
329 apk_assets_path = os.path.join(apk_path, 'assets')
330 extensions_string = 'xwalk-extensions'
332 # Set up the target directories and files.
333 dest_jar_path = os.path.join(apk_path, extensions_string)
334 os.mkdir(dest_jar_path)
335 dest_js_path = os.path.join(apk_assets_path, extensions_string)
336 os.mkdir(dest_js_path)
337 apk_extensions_json_path = os.path.join(apk_assets_path,
338 'extensions-config.json')
340 # Split the paths into a list.
341 extension_paths = extensions.split(os.pathsep)
342 extension_json_list = []
343 for source_path in extension_paths:
344 if not os.path.exists(source_path):
345 print('Error: can\'t find the extension directory \'%s\'.' % source_path)
347 # Remove redundant separators to avoid empty basename.
348 source_path = os.path.normpath(source_path)
349 extension_name = os.path.basename(source_path)
351 # Copy .jar file into xwalk-extensions.
352 CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
354 # Copy .js file into assets/xwalk-extensions.
355 CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
357 # Merge .json file into assets/xwalk-extensions.
358 file_name = extension_name + '.json'
359 src_file = os.path.join(source_path, file_name)
360 if not os.path.isfile(src_file):
361 print('Error: %s is not found in %s.' % (file_name, source_path))
364 src_file_handle = open(src_file)
365 src_file_content = src_file_handle.read()
366 json_output = json.JSONDecoder().decode(src_file_content)
367 # Below 3 properties are used by runtime. See extension manager.
368 # And 'permissions' will be merged.
369 if not ('name' in json_output and
370 'class' in json_output and
371 'jsapi' in json_output):
372 print ('Error: properties \'name\', \'class\' and \'jsapi\' in a json '
373 'file are mandatory.')
375 # Reset the path for JavaScript.
376 js_path_prefix = extensions_string + '/' + extension_name + '/'
377 json_output['jsapi'] = js_path_prefix + json_output['jsapi']
378 extension_json_list.append(json_output)
379 # Merge the permissions of extensions into AndroidManifest.xml.
380 manifest_path = os.path.join(name, 'AndroidManifest.xml')
381 xmldoc = minidom.parse(manifest_path)
382 if ('permissions' in json_output):
383 # Get used permission list to avoid repetition as "--permissions"
384 # option can also be used to declare used permissions.
386 usedPermissions = xmldoc.getElementsByTagName("uses-permission")
387 for used in usedPermissions:
388 existingList.append(used.getAttribute("android:name"))
390 # Add the permissions to manifest file if not used yet.
391 for p in json_output['permissions']:
392 if p in existingList:
394 AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
395 existingList.append(p)
397 # Write to the manifest file to save the update.
398 file_handle = open(manifest_path, 'w')
399 xmldoc.writexml(file_handle, encoding='utf-8')
402 # Write configuration of extensions into the target extensions-config.json.
403 if extension_json_list:
404 extensions_string = json.JSONEncoder().encode(extension_json_list)
405 extension_json_file = open(apk_extensions_json_path, 'w')
406 extension_json_file.write(extensions_string)
407 extension_json_file.close()
410 def GenerateCommandLineFile(app_info, xwalk_command_line):
411 if xwalk_command_line == '':
413 assets_path = os.path.join(app_info.android_name, 'assets')
414 file_path = os.path.join(assets_path, 'xwalk-command-line')
415 command_line_file = open(file_path, 'w')
416 command_line_file.write('xwalk ' + xwalk_command_line)
419 def CustomizeIconByDict(name, app_root, icon_dict):
421 drawable_dict = {'ldpi': [1, 37], 'mdpi': [37, 72], 'hdpi': [72, 96],
422 'xhdpi': [96, 120], 'xxhdpi': [120, 144],
423 'xxxhdpi': [144, 168]}
428 icon_dict = dict((int(k), v) for k, v in icon_dict.items())
430 print('The key of icon in the manifest file should be a number.')
432 if len(icon_dict) > 0:
433 icon_list = sorted(icon_dict.items(), key=lambda d: d[0])
434 for kd, vd in drawable_dict.items():
435 for item in icon_list:
436 if item[0] >= vd[0] and item[0] < vd[1]:
437 drawable_path = os.path.join(name, 'res', 'drawable-' + kd)
438 if not os.path.exists(drawable_path):
439 os.makedirs(drawable_path)
440 icon = os.path.join(app_root, item[1])
441 if icon and os.path.isfile(icon):
442 icon_name = os.path.basename(icon)
443 icon_suffix = icon_name.split('.')[-1]
444 shutil.copyfile(icon, os.path.join(drawable_path,
445 'icon.' + icon_suffix))
447 elif icon and (not os.path.isfile(icon)):
448 print('Error: "%s" does not exist.' % icon)
454 def CustomizeIconByOption(name, icon):
455 if os.path.isfile(icon):
456 drawable_path = os.path.join(name, 'res', 'drawable')
457 if not os.path.exists(drawable_path):
458 os.makedirs(drawable_path)
459 icon_file = os.path.basename(icon)
460 icon_file = ReplaceInvalidChars(icon_file)
461 shutil.copyfile(icon, os.path.join(drawable_path, icon_file))
462 icon_name = os.path.splitext(icon_file)[0]
465 print('Error: "%s" does not exist.')
469 def CustomizeIcon(name, app_root, icon, icon_dict):
472 icon_name = CustomizeIconByOption(name, icon)
474 icon_name = CustomizeIconByDict(name, app_root, icon_dict)
478 def CustomizeAll(app_info, description, icon_dict, permissions, app_url,
479 app_local_path, keep_screen_on, extensions, manifest,
480 xwalk_command_line='', compressor=None):
482 Prepare(app_info, compressor)
483 CustomizeXML(app_info, description, icon_dict, manifest, permissions)
484 CustomizeJava(app_info, app_url, app_local_path, keep_screen_on)
485 CustomizeExtensions(app_info, extensions)
486 GenerateCommandLineFile(app_info, xwalk_command_line)
487 except SystemExit as ec:
488 print('Exiting with error code: %d' % ec.code)
493 parser = optparse.OptionParser()
494 info = ('The package name. Such as: '
495 '--package=com.example.YourPackage')
496 parser.add_option('--package', help=info)
497 info = ('The apk name. Such as: --name="Your Application Name"')
498 parser.add_option('--name', help=info)
499 info = ('The version of the app. Such as: --app-version=TheVersionNumber')
500 parser.add_option('--app-version', help=info)
501 info = ('The versionCode of the app. Such as: --app-versionCode=24')
502 parser.add_option('--app-versionCode', type='int', help=info)
503 info = ('The application description. Such as:'
504 '--description=YourApplicationdDescription')
505 parser.add_option('--description', help=info)
506 info = ('The permission list. Such as: --permissions="geolocation"'
507 'For more permissions, such as:'
508 '--permissions="geolocation:permission2"')
509 parser.add_option('--permissions', help=info)
510 info = ('The url of application. '
511 'This flag allows to package website as apk. Such as: '
512 '--app-url=http://www.intel.com')
513 parser.add_option('--app-url', help=info)
514 info = ('The root path of the web app. '
515 'This flag allows to package local web app as apk. Such as: '
516 '--app-root=/root/path/of/the/web/app')
517 parser.add_option('--app-root', help=info)
518 info = ('The reletive path of entry file based on |app_root|. '
519 'This flag should work with "--app-root" together. '
520 'Such as: --app-local-path=/reletive/path/of/entry/file')
521 parser.add_option('--app-local-path', help=info)
522 parser.add_option('--enable-remote-debugging', action='store_true',
523 dest='enable_remote_debugging', default=False,
524 help='Enable remote debugging.')
525 parser.add_option('-f', '--fullscreen', action='store_true',
526 dest='fullscreen', default=False,
527 help='Make application fullscreen.')
528 parser.add_option('--keep-screen-on', action='store_true', default=False,
529 help='Support keeping screen on')
530 info = ('The path list for external extensions separated by os separator.'
531 'On Linux and Mac, the separator is ":". On Windows, it is ";".'
532 'Such as: --extensions="/path/to/extension1:/path/to/extension2"')
533 parser.add_option('--extensions', help=info)
534 info = ('The orientation of the web app\'s display on the device. '
535 'Such as: --orientation=landscape. The default value is "unspecified"'
536 'The value options are the same as those on the Android: '
537 'http://developer.android.com/guide/topics/manifest/'
538 'activity-element.html#screen')
539 parser.add_option('--orientation', help=info)
540 parser.add_option('--manifest', help='The manifest path')
541 info = ('Use command lines.'
542 'Crosswalk is powered by Chromium and supports Chromium command line.'
544 '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
545 parser.add_option('--xwalk-command-line', default='', help=info)
546 info = ('Minify and obfuscate javascript and css.'
547 '--compressor: compress javascript and css.'
548 '--compressor=js: compress javascript.'
549 '--compressor=css: compress css.')
550 parser.add_option('--compressor', dest='compressor', action='callback',
551 callback=ParseParameterForCompressor,
552 type='string', nargs=0, help=info)
553 options, _ = parser.parse_args()
555 icon_dict = {144: 'icons/icon_144.png',
556 72: 'icons/icon_72.png',
557 96: 'icons/icon_96.png',
558 48: 'icons/icon_48.png'}
560 if options.name is not None:
561 app_info.android_name = options.name
562 if options.app_root is None:
563 app_info.app_root = os.path.join('test_data', 'manifest')
565 app_info.app_root = options.app_root
566 if options.package is not None:
567 app_info.package = options.package
568 if options.orientation is not None:
569 app_info.orientation = options.orientation
570 if options.app_version is not None:
571 app_info.app_version = options.app_version
572 if options.enable_remote_debugging is not None:
573 app_info.remote_debugging = options.enable_remote_debugging
574 if options.fullscreen is not None:
575 app_info.fullscreen_flag = options.fullscreen
576 app_info.icon = os.path.join('test_data', 'manifest', 'icons',
578 CustomizeAll(app_info, options.description, icon_dict,
579 options.permissions, options.app_url, options.app_local_path,
580 options.keep_screen_on, options.extensions, None,
581 options.xwalk_command_line, options.compressor)
582 except SystemExit as ec:
583 print('Exiting with error code: %d' % ec.code)
588 if __name__ == '__main__':