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 customize_launch_screen import CustomizeLaunchScreen
18 from handle_xml import AddElementAttribute
19 from handle_xml import AddElementAttributeAndText
20 from handle_xml import EditElementAttribute
21 from handle_xml import EditElementValueByNodeName
22 from handle_permissions import HandlePermissions
23 from xml.dom import minidom
26 def VerifyAppName(value, mode='default'):
28 sample = 'helloworld, hello_world, hello_world1'
29 regex = r'^([a-zA-Z](\w)*)+$'
32 print('To be safe, the length of package name or app name '
33 'should be less than 128.')
35 if mode == 'packagename':
36 regex = r'^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$'
37 descrpt = 'Each part of package'
38 sample = 'org.xwalk.example, org.xwalk.example_'
40 if not re.match(regex, value):
41 print('Error: %s name should be started with letters and should not '
42 'conatin invalid characters.\n'
43 'It may conatin letters, numbers and underscores\n'
44 'Sample: %s' % (descrpt, sample))
48 def ReplaceInvalidChars(value, mode='default'):
49 """ Replace the invalid chars with '_' for input string.
51 value: the original string.
52 mode: the target usage mode of original string.
55 invalid_chars = '\/:*?"<>|- '
56 elif mode == 'apkname':
57 invalid_chars = '\/:.*?"<>|- '
58 for c in invalid_chars:
59 if mode == 'apkname' and c in value:
60 print("Illegal character: '%s' is replaced with '_'" % c)
61 value = value.replace(c, '_')
65 def GetFilesByExt(path, ext, sub_dir=True):
66 if os.path.exists(path):
68 for name in os.listdir(path):
69 full_name = os.path.join(path, name)
70 st = os.lstat(full_name)
71 if stat.S_ISDIR(st.st_mode) and sub_dir:
72 file_list += GetFilesByExt(full_name, ext)
73 elif os.path.isfile(full_name):
74 if fnmatch.fnmatch(full_name, ext):
75 file_list.append(full_name)
81 def ParseParameterForCompressor(option, value, values, parser):
82 if ((not values or values.startswith('-'))
83 and value.find('--compressor') != -1):
86 if parser.rargs and not parser.rargs[0].startswith('-'):
89 setattr(parser.values, option.dest, val)
92 def CompressSourceFiles(app_root, compressor):
98 if compressor == 'all' or compressor == 'js':
99 js_list = GetFilesByExt(app_root, js_ext)
100 compress_js_and_css.CompressJavaScript(js_list)
102 if compressor == 'all' or compressor == 'css':
103 css_list = GetFilesByExt(app_root, css_ext)
104 compress_js_and_css.CompressCss(css_list)
107 def Prepare(name, package, app_root, compressor):
108 if os.path.exists(name):
110 shutil.copytree('app_src', name)
111 shutil.rmtree(os.path.join(name, 'src'))
112 src_root = os.path.join('app_src', 'src', 'org', 'xwalk', 'app', 'template')
113 src_activity = os.path.join(src_root, 'AppTemplateActivity.java')
114 if not os.path.isfile(src_activity):
115 print ('Please make sure that the java file'
116 ' of activity does exist.')
118 root_path = os.path.join(name, 'src', package.replace('.', os.path.sep))
119 if not os.path.exists(root_path):
120 os.makedirs(root_path)
121 dest_activity = name + 'Activity.java'
122 shutil.copyfile(src_activity, os.path.join(root_path, dest_activity))
124 assets_path = os.path.join(name, 'assets')
125 shutil.rmtree(assets_path)
126 os.makedirs(assets_path)
127 app_src_path = os.path.join(assets_path, 'www')
128 shutil.copytree(app_root, app_src_path)
130 CompressSourceFiles(app_src_path, compressor)
133 def CustomizeStringXML(name, description):
134 strings_path = os.path.join(name, 'res', 'values', 'strings.xml')
135 if not os.path.isfile(strings_path):
136 print ('Please make sure strings_xml'
137 ' exists under app_src folder.')
141 xmldoc = minidom.parse(strings_path)
142 AddElementAttributeAndText(xmldoc, 'string', 'name', 'description',
144 strings_file = open(strings_path, 'w')
145 xmldoc.writexml(strings_file, encoding='utf-8')
149 def CustomizeThemeXML(name, fullscreen, app_manifest):
150 theme_path = os.path.join(name, 'res', 'values', 'theme.xml')
151 if not os.path.isfile(theme_path):
152 print('Error: theme.xml is missing in the build tool.')
155 theme_xmldoc = minidom.parse(theme_path)
157 EditElementValueByNodeName(theme_xmldoc, 'item',
158 'android:windowFullscreen', 'true')
159 has_background = CustomizeLaunchScreen(app_manifest, name)
161 EditElementValueByNodeName(theme_xmldoc, 'item',
162 'android:windowBackground',
163 '@drawable/launchscreen_bg')
164 theme_file = open(theme_path, 'wb')
165 theme_xmldoc.writexml(theme_file, encoding='utf-8')
169 def CustomizeXML(package, app_versionCode, app_version, description, name,
170 orientation, icon_dict, fullscreen, icon, app_manifest,
171 permissions, app_root):
172 manifest_path = os.path.join(name, 'AndroidManifest.xml')
173 if not os.path.isfile(manifest_path):
174 print ('Please make sure AndroidManifest.xml'
175 ' exists under app_src folder.')
178 CustomizeStringXML(name, description)
179 CustomizeThemeXML(name, fullscreen, app_manifest)
180 xmldoc = minidom.parse(manifest_path)
181 EditElementAttribute(xmldoc, 'manifest', 'package', package)
183 EditElementAttribute(xmldoc, 'manifest', 'android:versionCode',
184 str(app_versionCode))
186 EditElementAttribute(xmldoc, 'manifest', 'android:versionName',
189 EditElementAttribute(xmldoc, 'manifest', 'android:description',
190 "@string/description")
191 HandlePermissions(permissions, xmldoc)
192 EditElementAttribute(xmldoc, 'application', 'android:label', name)
193 activity_name = package + '.' + name + 'Activity'
194 EditElementAttribute(xmldoc, 'activity', 'android:name', activity_name)
195 EditElementAttribute(xmldoc, 'activity', 'android:label', name)
197 EditElementAttribute(xmldoc, 'activity', 'android:screenOrientation',
199 icon_name = CustomizeIcon(name, app_root, icon, icon_dict)
201 EditElementAttribute(xmldoc, 'application', 'android:icon',
202 '@drawable/%s' % icon_name)
204 file_handle = open(os.path.join(name, 'AndroidManifest.xml'), 'w')
205 xmldoc.writexml(file_handle, encoding='utf-8')
209 def ReplaceString(file_path, src, dest):
210 file_handle = open(file_path, 'r')
211 src_content = file_handle.read()
213 file_handle = open(file_path, 'w')
214 dest_content = src_content.replace(src, dest)
215 file_handle.write(dest_content)
219 def SetVariable(file_path, string_line, variable, value):
220 function_string = ('%sset%s(%s);\n' %
221 (' ', variable, value))
222 temp_file_path = file_path + '.backup'
223 file_handle = open(temp_file_path, 'w+')
224 for line in open(file_path):
225 file_handle.write(line)
226 if (line.find(string_line) >= 0):
227 file_handle.write(function_string)
229 shutil.move(temp_file_path, file_path)
232 def CustomizeJava(name, package, app_url, app_local_path,
233 enable_remote_debugging, display_as_fullscreen,
235 root_path = os.path.join(name, 'src', package.replace('.', os.path.sep))
236 dest_activity = os.path.join(root_path, name + 'Activity.java')
237 ReplaceString(dest_activity, 'org.xwalk.app.template', package)
238 ReplaceString(dest_activity, 'AppTemplate', name)
239 manifest_file = os.path.join(name, 'assets/www', 'manifest.json')
240 if os.path.isfile(manifest_file):
243 'loadAppFromUrl("file:///android_asset/www/index.html")',
244 'loadAppFromManifest("file:///android_asset/www/manifest.json")')
247 if re.search(r'^http(|s)', app_url):
248 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
251 if os.path.isfile(os.path.join(name, 'assets/www', app_local_path)):
252 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
253 'app://' + package + '/' + app_local_path)
255 print ('Please make sure that the relative path of entry file'
259 if enable_remote_debugging:
260 SetVariable(dest_activity,
261 'public void onCreate(Bundle savedInstanceState)',
262 'RemoteDebugging', 'true')
263 if display_as_fullscreen:
264 SetVariable(dest_activity,
265 'super.onCreate(savedInstanceState)',
266 'IsFullscreen', 'true')
270 'super.onCreate(savedInstanceState);',
271 'super.onCreate(savedInstanceState);\n ' +
272 'getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);')
275 def CopyExtensionFile(extension_name, suffix, src_path, dest_path):
276 # Copy the file from src_path into dest_path.
277 dest_extension_path = os.path.join(dest_path, extension_name)
278 if os.path.exists(dest_extension_path):
279 # TODO: Refine it by renaming it internally.
280 print('Error: duplicated extension names "%s" are found. Please rename it.'
284 os.mkdir(dest_extension_path)
286 file_name = extension_name + suffix
287 src_file = os.path.join(src_path, file_name)
288 dest_file = os.path.join(dest_extension_path, file_name)
289 if not os.path.isfile(src_file):
291 print('Error: %s is not found in %s.' % (file_name, src_path))
293 shutil.copyfile(src_file, dest_file)
296 def CustomizeExtensions(name, extensions):
297 """Copy the files from external extensions and merge them into APK.
299 The directory of one external extension should be like:
304 That means the name of the internal files should be the same as the
306 For .jar files, they'll be copied to xwalk-extensions/ and then
307 built into classes.dex in make_apk.py.
308 For .js files, they'll be copied into assets/xwalk-extensions/.
309 For .json files, the'll be merged into one file called
310 extensions-config.json and copied into assets/.
315 apk_assets_path = os.path.join(apk_path, 'assets')
316 extensions_string = 'xwalk-extensions'
318 # Set up the target directories and files.
319 dest_jar_path = os.path.join(apk_path, extensions_string)
320 os.mkdir(dest_jar_path)
321 dest_js_path = os.path.join(apk_assets_path, extensions_string)
322 os.mkdir(dest_js_path)
323 apk_extensions_json_path = os.path.join(apk_assets_path,
324 'extensions-config.json')
326 # Split the paths into a list.
327 extension_paths = extensions.split(os.pathsep)
328 extension_json_list = []
329 for source_path in extension_paths:
330 if not os.path.exists(source_path):
331 print('Error: can\'t find the extension directory \'%s\'.' % source_path)
333 # Remove redundant separators to avoid empty basename.
334 source_path = os.path.normpath(source_path)
335 extension_name = os.path.basename(source_path)
337 # Copy .jar file into xwalk-extensions.
338 CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
340 # Copy .js file into assets/xwalk-extensions.
341 CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
343 # Merge .json file into assets/xwalk-extensions.
344 file_name = extension_name + '.json'
345 src_file = os.path.join(source_path, file_name)
346 if not os.path.isfile(src_file):
347 print('Error: %s is not found in %s.' % (file_name, source_path))
350 src_file_handle = file(src_file)
351 src_file_content = src_file_handle.read()
352 json_output = json.JSONDecoder().decode(src_file_content)
353 # Below 3 properties are used by runtime. See extension manager.
354 # And 'permissions' will be merged.
355 if not ('name' in json_output and
356 'class' in json_output and
357 'jsapi' in json_output):
358 print ('Error: properties \'name\', \'class\' and \'jsapi\' in a json '
359 'file are mandatory.')
361 # Reset the path for JavaScript.
362 js_path_prefix = extensions_string + '/' + extension_name + '/'
363 json_output['jsapi'] = js_path_prefix + json_output['jsapi']
364 extension_json_list.append(json_output)
365 # Merge the permissions of extensions into AndroidManifest.xml.
366 manifest_path = os.path.join(name, 'AndroidManifest.xml')
367 xmldoc = minidom.parse(manifest_path)
368 if ('permissions' in json_output):
369 # Get used permission list to avoid repetition as "--permissions"
370 # option can also be used to declare used permissions.
372 usedPermissions = xmldoc.getElementsByTagName("uses-permission")
373 for used in usedPermissions:
374 existingList.append(used.getAttribute("android:name"))
376 # Add the permissions to manifest file if not used yet.
377 for p in json_output['permissions']:
378 if p in existingList:
380 AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
381 existingList.append(p)
383 # Write to the manifest file to save the update.
384 file_handle = open(manifest_path, 'w')
385 xmldoc.writexml(file_handle, encoding='utf-8')
388 # Write configuration of extensions into the target extensions-config.json.
389 if extension_json_list:
390 extensions_string = json.JSONEncoder().encode(extension_json_list)
391 extension_json_file = open(apk_extensions_json_path, 'w')
392 extension_json_file.write(extensions_string)
393 extension_json_file.close()
396 def GenerateCommandLineFile(name, xwalk_command_line):
397 if xwalk_command_line == '':
399 assets_path = os.path.join(name, 'assets')
400 file_path = os.path.join(assets_path, 'xwalk-command-line')
401 command_line_file = open(file_path, 'w')
402 command_line_file.write('xwalk ' + xwalk_command_line)
405 def CustomizeIconByDict(name, app_root, icon_dict):
407 drawable_dict = {'ldpi': [1, 37], 'mdpi': [37, 72], 'hdpi': [72, 96],
408 'xhdpi': [96, 120], 'xxhdpi': [120, 144],
409 'xxxhdpi': [144, 168]}
414 icon_dict = dict((int(k), v) for k, v in icon_dict.items())
416 print('The key of icon in the manifest file should be a number.')
418 if len(icon_dict) > 0:
419 icon_list = sorted(icon_dict.iteritems(), key=lambda d: d[0])
420 for kd, vd in drawable_dict.iteritems():
421 for item in icon_list:
422 if item[0] >= vd[0] and item[0] < vd[1]:
423 drawable_path = os.path.join(name, 'res', 'drawable-' + kd)
424 if not os.path.exists(drawable_path):
425 os.makedirs(drawable_path)
426 icon = os.path.join(app_root, item[1])
427 if icon and os.path.isfile(icon):
428 icon_name = os.path.basename(icon)
429 icon_suffix = icon_name.split('.')[-1]
430 shutil.copyfile(icon, os.path.join(drawable_path,
431 'icon.' + icon_suffix))
433 elif icon and (not os.path.isfile(icon)):
434 print ('Error: Please make sure \"' + icon + '\" does exist!')
440 def CustomizeIconByOption(name, icon):
441 if os.path.isfile(icon):
442 drawable_path = os.path.join(name, 'res', 'drawable')
443 if not os.path.exists(drawable_path):
444 os.makedirs(drawable_path)
445 icon_file = os.path.basename(icon)
446 icon_file = ReplaceInvalidChars(icon_file)
447 shutil.copyfile(icon, os.path.join(drawable_path, icon_file))
448 icon_name = os.path.splitext(icon_file)[0]
451 print ('Error: Please make sure \"' + icon + '\" is a file!')
455 def CustomizeIcon(name, app_root, icon, icon_dict):
458 icon_name = CustomizeIconByOption(name, icon)
460 icon_name = CustomizeIconByDict(name, app_root, icon_dict)
464 def CustomizeAll(app_versionCode, description, icon_dict, permissions, app_url,
465 app_root, app_local_path, enable_remote_debugging,
466 display_as_fullscreen, keep_screen_on, extensions,
467 app_manifest, icon, package='org.xwalk.app.template',
468 name='AppTemplate', app_version='1.0.0',
469 orientation='unspecified', xwalk_command_line='',
472 Prepare(name, package, app_root, compressor)
473 CustomizeXML(package, app_versionCode, app_version, description, name,
474 orientation, icon_dict, display_as_fullscreen, icon,
475 app_manifest, permissions, app_root)
476 CustomizeJava(name, package, app_url, app_local_path,
477 enable_remote_debugging, display_as_fullscreen,
479 CustomizeExtensions(name, extensions)
480 GenerateCommandLineFile(name, xwalk_command_line)
481 except SystemExit as ec:
482 print('Exiting with error code: %d' % ec.code)
487 parser = optparse.OptionParser()
488 info = ('The package name. Such as: '
489 '--package=com.example.YourPackage')
490 parser.add_option('--package', help=info)
491 info = ('The apk name. Such as: --name="Your Application Name"')
492 parser.add_option('--name', help=info)
493 info = ('The version of the app. Such as: --app-version=TheVersionNumber')
494 parser.add_option('--app-version', help=info)
495 info = ('The versionCode of the app. Such as: --app-versionCode=24')
496 parser.add_option('--app-versionCode', type='int', help=info)
497 info = ('The application description. Such as:'
498 '--description=YourApplicationdDescription')
499 parser.add_option('--description', help=info)
500 info = ('The permission list. Such as: --permissions="geolocation"'
501 'For more permissions, such as:'
502 '--permissions="geolocation:permission2"')
503 parser.add_option('--permissions', help=info)
504 info = ('The url of application. '
505 'This flag allows to package website as apk. Such as: '
506 '--app-url=http://www.intel.com')
507 parser.add_option('--app-url', help=info)
508 info = ('The root path of the web app. '
509 'This flag allows to package local web app as apk. Such as: '
510 '--app-root=/root/path/of/the/web/app')
511 parser.add_option('--app-root', help=info)
512 info = ('The reletive path of entry file based on |app_root|. '
513 'This flag should work with "--app-root" together. '
514 'Such as: --app-local-path=/reletive/path/of/entry/file')
515 parser.add_option('--app-local-path', help=info)
516 parser.add_option('--enable-remote-debugging', action='store_true',
517 dest='enable_remote_debugging', default=False,
518 help='Enable remote debugging.')
519 parser.add_option('-f', '--fullscreen', action='store_true',
520 dest='fullscreen', default=False,
521 help='Make application fullscreen.')
522 parser.add_option('--keep-screen-on', action='store_true', default=False,
523 help='Support keeping screen on')
524 info = ('The path list for external extensions separated by os separator.'
525 'On Linux and Mac, the separator is ":". On Windows, it is ";".'
526 'Such as: --extensions="/path/to/extension1:/path/to/extension2"')
527 parser.add_option('--extensions', help=info)
528 info = ('The orientation of the web app\'s display on the device. '
529 'Such as: --orientation=landscape. The default value is "unspecified"'
530 'The value options are the same as those on the Android: '
531 'http://developer.android.com/guide/topics/manifest/'
532 'activity-element.html#screen')
533 parser.add_option('--orientation', help=info)
534 parser.add_option('--manifest', help='The manifest path')
535 info = ('Use command lines.'
536 'Crosswalk is powered by Chromium and supports Chromium command line.'
538 '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
539 parser.add_option('--xwalk-command-line', default='', help=info)
540 info = ('Minify and obfuscate javascript and css.'
541 '--compressor: compress javascript and css.'
542 '--compressor=js: compress javascript.'
543 '--compressor=css: compress css.')
544 parser.add_option('--compressor', dest='compressor', action='callback',
545 callback=ParseParameterForCompressor,
546 type='string', nargs=0, help=info)
547 options, _ = parser.parse_args()
549 icon_dict = {144: 'icons/icon_144.png',
550 72: 'icons/icon_72.png',
551 96: 'icons/icon_96.png',
552 48: 'icons/icon_48.png'}
553 if options.name is None:
554 options.name = 'Example'
555 if options.app_root is None:
556 options.app_root = os.path.join('test_data', 'manifest')
557 if options.package is None:
558 options.package = 'org.xwalk.app.template'
559 if options.orientation is None:
560 options.orientation = 'unspecified'
561 if options.app_version is None:
562 options.app_version = '1.0.0'
563 icon = os.path.join('test_data', 'manifest', 'icons', 'icon_96.png')
564 CustomizeAll(options.app_versionCode, options.description, icon_dict,
565 options.permissions, options.app_url, options.app_root,
566 options.app_local_path, options.enable_remote_debugging,
567 options.fullscreen, options.keep_screen_on,
568 options.extensions, options.manifest,
569 icon, options.package, options.name,
570 options.app_version, options.orientation,
571 options.xwalk_command_line, options.compressor)
572 except SystemExit as ec:
573 print('Exiting with error code: %d' % ec.code)
578 if __name__ == '__main__':