3 # Copyright (c) 2013,2014 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
18 # get xwalk absolute path so we can run this script from any location
19 xwalk_dir = os.path.dirname(os.path.abspath(__file__))
20 sys.path.append(xwalk_dir)
22 from app_info import AppInfo
23 from customize_launch_screen import CustomizeLaunchScreen
24 from handle_xml import AddElementAttribute
25 from handle_xml import AddElementAttributeAndText
26 from handle_xml import EditElementAttribute
27 from handle_xml import EditElementValueByNodeName
28 from handle_permissions import HandlePermissions
29 from util import CleanDir, CreateAndCopyDir
30 from xml.dom import minidom
32 TEMPLATE_DIR_NAME = 'template'
34 def VerifyPackageName(value):
35 regex = r'^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$'
36 descrpt = 'Each part of package'
37 sample = 'org.xwalk.example, org.xwalk.example_'
40 print('To be safe, the length of package name or app name '
41 'should be less than 128.')
44 if not re.match(regex, value):
45 print('Error: %s name should be started with letters and should not '
46 'contain invalid characters.\n'
47 'It may contain lowercase letters, numbers, blank spaces and '
49 'Sample: %s' % (descrpt, sample))
53 def ReplaceSpaceWithUnderscore(value):
54 return value.replace(' ', '_')
57 def ReplaceInvalidChars(value, mode='default'):
58 """ Replace the invalid chars with '_' for input string.
60 value: the original string.
61 mode: the target usage mode of original string.
64 invalid_chars = '\/:*?"<>|- '
65 elif mode == 'apkname':
66 invalid_chars = '\/:.*?"<>|- '
67 for c in invalid_chars:
68 if mode == 'apkname' and c in value:
69 print('Illegal character: "%s" replaced with "_"' % c)
70 value = value.replace(c, '_')
74 def GetFilesByExt(path, ext, sub_dir=True):
75 if os.path.exists(path):
77 for name in os.listdir(path):
78 full_name = os.path.join(path, name)
79 st = os.lstat(full_name)
80 if stat.S_ISDIR(st.st_mode) and sub_dir:
81 file_list += GetFilesByExt(full_name, ext)
82 elif os.path.isfile(full_name):
83 if fnmatch.fnmatch(full_name, ext):
84 file_list.append(full_name)
90 def ParseParameterForCompressor(option, value, values, parser):
91 if ((not values or values.startswith('-'))
92 and value.find('--compressor') != -1):
95 if parser.rargs and not parser.rargs[0].startswith('-'):
98 setattr(parser.values, option.dest, val)
101 def CompressSourceFiles(app_root, compressor):
107 if compressor == 'all' or compressor == 'js':
108 js_list = GetFilesByExt(app_root, js_ext)
109 compress_js_and_css.CompressJavaScript(js_list)
111 if compressor == 'all' or compressor == 'css':
112 css_list = GetFilesByExt(app_root, css_ext)
113 compress_js_and_css.CompressCss(css_list)
116 def Prepare(app_info, compressor):
117 """Copy the Android template project to a new app project
118 named app_info.app_name
120 # create new app_dir in temp dir
121 app_name = app_info.android_name
122 app_dir = os.path.join(tempfile.gettempdir(), app_name)
123 app_package = app_info.package
124 app_root = app_info.app_root
125 template_app_dir = os.path.join(xwalk_dir, TEMPLATE_DIR_NAME)
127 # 1) copy template project to app_dir
129 if not os.path.isdir(template_app_dir):
130 print('Error: The template directory could not be found (%s).' %
133 shutil.copytree(template_app_dir, app_dir)
135 # 2) replace app_dir 'src' dir with template 'src' dir
136 CleanDir(os.path.join(app_dir, 'src'))
137 template_src_root = os.path.join(template_app_dir, 'src', 'org', 'xwalk',
140 # 3) Create directory tree from app package (org.xyz.foo -> src/org/xyz/foo)
141 # and copy AppTemplateActivity.java to <app_name>Activity.java
142 template_activity_file = os.path.join(template_src_root,
143 'AppTemplateActivity.java')
144 if not os.path.isfile(template_activity_file):
145 print ('Error: The template file %s was not found. '
146 'Please make sure this file exists.' % template_activity_file)
148 app_pkg_dir = os.path.join(app_dir, 'src',
149 app_package.replace('.', os.path.sep))
150 if not os.path.exists(app_pkg_dir):
151 os.makedirs(app_pkg_dir)
152 app_activity_file = app_name + 'Activity.java'
153 shutil.copyfile(template_activity_file,
154 os.path.join(app_pkg_dir, app_activity_file))
156 # 4) Copy all HTML source from app_root to app_dir
158 app_assets_dir = os.path.join(app_dir, 'assets', 'www')
159 CleanDir(app_assets_dir)
160 shutil.copytree(app_root, app_assets_dir)
162 CompressSourceFiles(app_assets_dir, compressor)
165 def EncodingUnicodeValue(value):
167 if isinstance(value, unicode):
168 value = value.encode("utf-8")
174 def CustomizeStringXML(name, description):
175 strings_path = os.path.join(tempfile.gettempdir(), name, 'res', 'values',
177 if not os.path.isfile(strings_path):
178 print ('Please make sure strings_xml'
179 ' exists under template folder.')
183 description = EncodingUnicodeValue(description)
184 xmldoc = minidom.parse(strings_path)
185 AddElementAttributeAndText(xmldoc, 'string', 'name', 'description',
187 strings_file = open(strings_path, 'w')
188 xmldoc.writexml(strings_file, encoding='utf-8')
192 def CustomizeThemeXML(name, fullscreen, manifest):
193 theme_path = os.path.join(tempfile.gettempdir(), name, 'res', 'values-v14',
195 if not os.path.isfile(theme_path):
196 print('Error: theme.xml is missing in the build tool.')
199 theme_xmldoc = minidom.parse(theme_path)
201 EditElementValueByNodeName(theme_xmldoc, 'item',
202 'android:windowFullscreen', 'true')
203 has_background = CustomizeLaunchScreen(manifest, name)
205 EditElementValueByNodeName(theme_xmldoc, 'item',
206 'android:windowBackground',
207 '@drawable/launchscreen_bg')
208 theme_file = open(theme_path, 'w')
209 theme_xmldoc.writexml(theme_file, encoding='utf-8')
213 def CustomizeXML(app_info, description, icon_dict, manifest, permissions):
214 app_version = app_info.app_version
215 app_versionCode = app_info.app_versionCode
216 name = app_info.android_name
217 orientation = app_info.orientation
218 package = app_info.package
219 app_name = app_info.app_name
220 app_dir = os.path.join(tempfile.gettempdir(), name)
221 # Chinese character with unicode get from 'manifest.json' will cause
222 # 'UnicodeEncodeError' when finally wrote to 'AndroidManifest.xml'.
223 app_name = EncodingUnicodeValue(app_name)
224 # If string start with '@' or '?', it will be treated as Android resource,
225 # which will cause 'No resource found' error,
226 # append a space before '@' or '?' to fix that.
227 if app_name.startswith('@') or app_name.startswith('?'):
228 app_name = ' ' + app_name
229 manifest_path = os.path.join(app_dir, 'AndroidManifest.xml')
230 if not os.path.isfile(manifest_path):
231 print ('Please make sure AndroidManifest.xml'
232 ' exists under template folder.')
235 CustomizeStringXML(name, description)
236 CustomizeThemeXML(name, app_info.fullscreen_flag, manifest)
237 xmldoc = minidom.parse(manifest_path)
238 EditElementAttribute(xmldoc, 'manifest', 'package', package)
240 EditElementAttribute(xmldoc, 'manifest', 'android:versionCode',
241 str(app_versionCode))
243 EditElementAttribute(xmldoc, 'manifest', 'android:versionName',
246 EditElementAttribute(xmldoc, 'manifest', 'android:description',
247 "@string/description")
248 HandlePermissions(permissions, xmldoc)
249 EditElementAttribute(xmldoc, 'application', 'android:label', app_name)
250 activity_name = package + '.' + name + 'Activity'
251 EditElementAttribute(xmldoc, 'activity', 'android:name', activity_name)
252 EditElementAttribute(xmldoc, 'activity', 'android:label', app_name)
254 EditElementAttribute(xmldoc, 'activity', 'android:screenOrientation',
256 icon_name = CustomizeIcon(name, app_info.app_root, app_info.icon, icon_dict)
258 EditElementAttribute(xmldoc, 'application', 'android:icon',
259 '@drawable/%s' % icon_name)
261 file_handle = open(os.path.join(app_dir, 'AndroidManifest.xml'), 'w')
262 xmldoc.writexml(file_handle, encoding='utf-8')
266 def ReplaceString(file_path, src, dest):
267 file_handle = open(file_path, 'r')
268 src_content = file_handle.read()
270 file_handle = open(file_path, 'w')
271 dest_content = src_content.replace(src, dest)
272 file_handle.write(dest_content)
276 def SetVariable(file_path, string_line, variable, value):
277 function_string = ('%sset%s(%s);\n' %
278 (' ', variable, value))
279 temp_file_path = file_path + '.backup'
280 file_handle = open(temp_file_path, 'w+')
281 for line in open(file_path):
282 file_handle.write(line)
283 if (line.find(string_line) >= 0):
284 file_handle.write(function_string)
286 shutil.move(temp_file_path, file_path)
289 def CustomizeJava(app_info, app_url, app_local_path, keep_screen_on):
290 name = app_info.android_name
291 package = app_info.package
292 app_dir = os.path.join(tempfile.gettempdir(), name)
293 app_pkg_dir = os.path.join(app_dir, 'src', package.replace('.', os.path.sep))
294 dest_activity = os.path.join(app_pkg_dir, name + 'Activity.java')
295 ReplaceString(dest_activity, 'org.xwalk.app.template', package)
296 ReplaceString(dest_activity, 'AppTemplate', name)
297 manifest_file = os.path.join(app_dir, 'assets', 'www', 'manifest.json')
298 if os.path.isfile(manifest_file):
301 'loadAppFromUrl("file:///android_asset/www/index.html")',
302 'loadAppFromManifest("file:///android_asset/www/manifest.json")')
305 if re.search(r'^http(|s)', app_url):
306 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
309 if os.path.isfile(os.path.join(app_dir, 'assets', 'www', app_local_path)):
310 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
311 'app://' + package + '/' + app_local_path)
313 print ('Please make sure that the relative path of entry file'
317 if app_info.remote_debugging:
318 SetVariable(dest_activity,
319 'public void onCreate(Bundle savedInstanceState)',
320 'RemoteDebugging', 'true')
321 if app_info.use_animatable_view:
322 SetVariable(dest_activity,
323 'public void onCreate(Bundle savedInstanceState)',
324 'UseAnimatableView', 'true')
325 if app_info.fullscreen_flag:
326 SetVariable(dest_activity,
327 'super.onCreate(savedInstanceState)',
328 'IsFullscreen', 'true')
332 'super.onCreate(savedInstanceState);',
333 'super.onCreate(savedInstanceState);\n ' +
334 'getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);')
337 def CopyExtensionFile(extension_name, suffix, src_path, dest_path):
338 # Copy the file from src_path into dest_path.
339 dest_extension_path = os.path.join(dest_path, extension_name)
340 if os.path.exists(dest_extension_path):
341 # TODO: Refine it by renaming it internally.
342 print('Error: duplicate extension names were found (%s). Please rename it.'
346 os.mkdir(dest_extension_path)
348 file_name = extension_name + suffix
349 src_file = os.path.join(src_path, file_name)
350 dest_file = os.path.join(dest_extension_path, file_name)
351 if not os.path.isfile(src_file):
352 print('Error: %s was not found in %s.' % (file_name, src_path))
355 shutil.copyfile(src_file, dest_file)
358 def CustomizeExtensions(app_info, extensions):
359 """Copy the files from external extensions and merge them into APK.
361 The directory of one external extension should be like:
366 That means the name of the internal files should be the same as the
368 For .jar files, they'll be copied to xwalk-extensions/ and then
369 built into classes.dex in make_apk.py.
370 For .js files, they'll be copied into assets/xwalk-extensions/.
371 For .json files, the'll be merged into one file called
372 extensions-config.json and copied into assets/.
376 name = app_info.android_name
377 app_dir = os.path.join(tempfile.gettempdir(), name)
378 apk_assets_path = os.path.join(app_dir, 'assets')
379 extensions_string = 'xwalk-extensions'
381 # Set up the target directories and files.
382 dest_jar_path = os.path.join(app_dir, extensions_string)
383 os.mkdir(dest_jar_path)
384 dest_js_path = os.path.join(apk_assets_path, extensions_string)
385 os.mkdir(dest_js_path)
386 apk_extensions_json_path = os.path.join(apk_assets_path,
387 'extensions-config.json')
389 # Split the paths into a list.
390 extension_paths = extensions.split(os.pathsep)
391 extension_json_list = []
392 for source_path in extension_paths:
393 if not os.path.exists(source_path):
394 print('Error: can not find the extension directory \'%s\'.' % source_path)
396 # Remove redundant separators to avoid empty basename.
397 source_path = os.path.normpath(source_path)
398 extension_name = os.path.basename(source_path)
400 # Copy .jar file into xwalk-extensions.
401 CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
403 # Copy .js file into assets/xwalk-extensions.
404 CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
406 # Merge .json file into assets/xwalk-extensions.
407 file_name = extension_name + '.json'
408 src_file = os.path.join(source_path, file_name)
409 if not os.path.isfile(src_file):
410 print('Error: %s was not found in %s.' % (file_name, source_path))
413 src_file_handle = open(src_file)
414 src_file_content = src_file_handle.read()
415 json_output = json.JSONDecoder().decode(src_file_content)
416 # Below 3 properties are used by runtime. See extension manager.
417 # And 'permissions' will be merged.
418 if not ('name' in json_output and
419 'class' in json_output and
420 'jsapi' in json_output):
421 print ('Error: properties \'name\', \'class\' and \'jsapi\' in a json '
422 'file are mandatory.')
424 # Reset the path for JavaScript.
425 js_path_prefix = extensions_string + '/' + extension_name + '/'
426 json_output['jsapi'] = js_path_prefix + json_output['jsapi']
427 extension_json_list.append(json_output)
428 # Merge the permissions of extensions into AndroidManifest.xml.
429 manifest_path = os.path.join(app_dir, 'AndroidManifest.xml')
430 xmldoc = minidom.parse(manifest_path)
431 if ('permissions' in json_output):
432 # Get used permission list to avoid repetition as "--permissions"
433 # option can also be used to declare used permissions.
435 usedPermissions = xmldoc.getElementsByTagName("uses-permission")
436 for used in usedPermissions:
437 existingList.append(used.getAttribute("android:name"))
439 # Add the permissions to manifest file if not used yet.
440 for p in json_output['permissions']:
441 if p in existingList:
443 AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
444 existingList.append(p)
446 # Write to the manifest file to save the update.
447 file_handle = open(manifest_path, 'w')
448 xmldoc.writexml(file_handle, encoding='utf-8')
451 # Write configuration of extensions into the target extensions-config.json.
452 if extension_json_list:
453 extensions_string = json.JSONEncoder().encode(extension_json_list)
454 extension_json_file = open(apk_extensions_json_path, 'w')
455 extension_json_file.write(extensions_string)
456 extension_json_file.close()
459 def GenerateCommandLineFile(app_info, xwalk_command_line):
460 if xwalk_command_line == '':
462 assets_path = os.path.join(tempfile.gettempdir(), app_info.android_name,
464 file_path = os.path.join(assets_path, 'xwalk-command-line')
465 command_line_file = open(file_path, 'w')
466 command_line_file.write('xwalk ' + xwalk_command_line)
469 def CustomizeIconByDict(name, app_root, icon_dict):
470 app_dir = os.path.join(tempfile.gettempdir(), name)
472 drawable_dict = {'ldpi': [1, 37], 'mdpi': [37, 72], 'hdpi': [72, 96],
473 'xhdpi': [96, 120], 'xxhdpi': [120, 144],
474 'xxxhdpi': [144, 168]}
479 icon_dict = dict((int(k), v) for k, v in icon_dict.items())
481 print('The key of icon in the manifest file should be a number.')
483 if len(icon_dict) > 0:
484 icon_list = sorted(icon_dict.items(), key=lambda d: d[0])
485 for kd, vd in drawable_dict.items():
486 for item in icon_list:
487 if item[0] >= vd[0] and item[0] < vd[1]:
488 drawable_path = os.path.join(app_dir, 'res', 'drawable-' + kd)
489 if not os.path.exists(drawable_path):
490 os.makedirs(drawable_path)
491 icon = os.path.join(app_root, item[1])
492 if icon and os.path.isfile(icon):
493 icon_name = os.path.basename(icon)
494 icon_suffix = icon_name.split('.')[-1]
495 shutil.copyfile(icon, os.path.join(drawable_path,
496 'icon.' + icon_suffix))
498 elif icon and (not os.path.isfile(icon)):
499 print('Error: "%s" does not exist.' % icon)
505 def CustomizeIconByOption(name, icon):
506 if os.path.isfile(icon):
507 drawable_path = os.path.join(tempfile.gettempdir(), name, 'res', 'drawable')
508 if not os.path.exists(drawable_path):
509 os.makedirs(drawable_path)
510 icon_file = os.path.basename(icon)
511 icon_file = ReplaceInvalidChars(icon_file)
512 shutil.copyfile(icon, os.path.join(drawable_path, icon_file))
513 icon_name = os.path.splitext(icon_file)[0]
516 print('Error: "%s" does not exist.' % icon)
520 def CustomizeIcon(name, app_root, icon, icon_dict):
523 icon_name = CustomizeIconByOption(name, icon)
525 icon_name = CustomizeIconByDict(name, app_root, icon_dict)
529 def CustomizeAll(app_info, description, icon_dict, permissions, app_url,
530 app_local_path, keep_screen_on, extensions, manifest,
531 xwalk_command_line='', compressor=None):
533 Prepare(app_info, compressor)
534 CustomizeXML(app_info, description, icon_dict, manifest, permissions)
535 CustomizeJava(app_info, app_url, app_local_path, keep_screen_on)
536 CustomizeExtensions(app_info, extensions)
537 GenerateCommandLineFile(app_info, xwalk_command_line)
538 except SystemExit as ec:
539 print('Exiting with error code: %d' % ec.code)
544 parser = optparse.OptionParser()
545 info = ('The package name. Such as: '
546 '--package=com.example.YourPackage')
547 parser.add_option('--package', help=info)
548 info = ('The apk name. Such as: --name="Your Application Name"')
549 parser.add_option('--name', help=info)
550 info = ('The version of the app. Such as: --app-version=TheVersionNumber')
551 parser.add_option('--app-version', help=info)
552 info = ('The versionCode of the app. Such as: --app-versionCode=24')
553 parser.add_option('--app-versionCode', type='int', help=info)
554 info = ('The application description. Such as:'
555 '--description=YourApplicationdDescription')
556 parser.add_option('--description', help=info)
557 info = ('The permission list. Such as: --permissions="geolocation"'
558 'For more permissions, such as:'
559 '--permissions="geolocation:permission2"')
560 parser.add_option('--permissions', help=info)
561 info = ('The url of application. '
562 'This flag allows to package website as apk. Such as: '
563 '--app-url=http://www.intel.com')
564 parser.add_option('--app-url', help=info)
565 info = ('The root path of the web app. '
566 'This flag allows to package local web app as apk. Such as: '
567 '--app-root=/root/path/of/the/web/app')
568 parser.add_option('--app-root', help=info)
569 info = ('The reletive path of entry file based on |app_root|. '
570 'This flag should work with "--app-root" together. '
571 'Such as: --app-local-path=/reletive/path/of/entry/file')
572 parser.add_option('--app-local-path', help=info)
573 parser.add_option('--enable-remote-debugging', action='store_true',
574 dest='enable_remote_debugging', default=False,
575 help='Enable remote debugging.')
576 parser.add_option('--use-animatable-view', action='store_true',
577 dest='use_animatable_view', default=False,
578 help='Enable using animatable view (TextureView).')
579 parser.add_option('-f', '--fullscreen', action='store_true',
580 dest='fullscreen', default=False,
581 help='Make application fullscreen.')
582 parser.add_option('--keep-screen-on', action='store_true', default=False,
583 help='Support keeping screen on')
584 info = ('The path list for external extensions separated by os separator.'
585 'On Linux and Mac, the separator is ":". On Windows, it is ";".'
586 'Such as: --extensions="/path/to/extension1:/path/to/extension2"')
587 parser.add_option('--extensions', help=info)
588 info = ('The orientation of the web app\'s display on the device. '
589 'Such as: --orientation=landscape. The default value is "unspecified"'
590 'The value options are the same as those on the Android: '
591 'http://developer.android.com/guide/topics/manifest/'
592 'activity-element.html#screen')
593 parser.add_option('--orientation', help=info)
594 parser.add_option('--manifest', help='The manifest path')
595 info = ('Use command lines.'
596 'Crosswalk is powered by Chromium and supports Chromium command line.'
598 '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
599 info = ('Create an Android project directory at this location. ')
600 parser.add_option('--project-dir', help=info)
601 parser.add_option('--xwalk-command-line', default='', help=info)
602 info = ('Minify and obfuscate javascript and css.'
603 '--compressor: compress javascript and css.'
604 '--compressor=js: compress javascript.'
605 '--compressor=css: compress css.')
606 parser.add_option('--compressor', dest='compressor', action='callback',
607 callback=ParseParameterForCompressor,
608 type='string', nargs=0, help=info)
609 options, _ = parser.parse_args()
611 icon_dict = {144: 'icons/icon_144.png',
612 72: 'icons/icon_72.png',
613 96: 'icons/icon_96.png',
614 48: 'icons/icon_48.png'}
616 if options.name is not None:
617 app_info.android_name = options.name
618 if options.app_root is None:
619 app_info.app_root = os.path.join(xwalk_dir, 'test_data', 'manifest')
621 app_info.app_root = options.app_root
622 if options.package is not None:
623 app_info.package = options.package
624 if options.orientation is not None:
625 app_info.orientation = options.orientation
626 if options.app_version is not None:
627 app_info.app_version = options.app_version
628 if options.enable_remote_debugging is not None:
629 app_info.remote_debugging = options.enable_remote_debugging
630 if options.fullscreen is not None:
631 app_info.fullscreen_flag = options.fullscreen
632 app_info.icon = os.path.join('test_data', 'manifest', 'icons',
634 CustomizeAll(app_info, options.description, icon_dict,
635 options.permissions, options.app_url, options.app_local_path,
636 options.keep_screen_on, options.extensions, None,
637 options.xwalk_command_line, options.compressor)
639 # build project is now in /tmp/<name>. Copy to project_dir
640 if options.project_dir:
641 src_dir = os.path.join(tempfile.gettempdir(), app_info.android_name)
642 dest_dir = os.path.join(options.project_dir, app_info.android_name)
643 CreateAndCopyDir(src_dir, dest_dir, True)
645 except SystemExit as ec:
646 print('Exiting with error code: %d' % ec.code)
649 CleanDir(os.path.join(tempfile.gettempdir(), app_info.android_name))
653 if __name__ == '__main__':