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.
6 # pylint: disable=F0401
16 # get xwalk absolute path so we can run this script from any location
17 xwalk_dir = os.path.dirname(os.path.abspath(__file__))
18 sys.path.append(xwalk_dir)
20 from app_info import AppInfo
21 from customize import VerifyPackageName, CustomizeAll, \
22 ParseParameterForCompressor
23 from extension_manager import GetExtensionList, GetExtensionStatus
24 from handle_permissions import permission_mapping_table
25 from util import AllArchitectures, CleanDir, GetVersion, RunCommand
26 from manifest_json_parser import HandlePermissionList
27 from manifest_json_parser import ManifestJsonParser
30 NATIVE_LIBRARY = 'libxwalkcore.so'
33 def ConvertArchNameToArchFolder(arch):
38 return arch_dict.get(arch, None)
41 def AddExeExtensions(name):
42 exts_str = os.environ.get('PATHEXT', '').lower()
43 exts = [_f for _f in exts_str.split(os.pathsep) if _f]
46 result.append(name + e)
52 """Searches PATH for executable files with the given name, also taking
53 PATHEXT into account. Returns the first existing match, or None if no matches
55 for path in os.environ.get('PATH', '').split(os.pathsep):
56 for filename in AddExeExtensions(name):
57 full_path = os.path.join(path, filename)
58 if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
63 def GetAndroidApiLevel(android_path):
64 """Get Highest Android target level installed.
65 return -1 if no targets have been found.
67 target_output = RunCommand([android_path, 'list', 'target', '-c'])
68 target_regex = re.compile(r'android-(\d+)')
69 targets = [int(i) for i in target_regex.findall(target_output)]
74 def ContainsNativeLibrary(path):
75 return os.path.isfile(os.path.join(path, NATIVE_LIBRARY))
78 def ParseManifest(options):
79 parser = ManifestJsonParser(os.path.expanduser(options.manifest))
81 options.name = parser.GetAppName()
82 if not options.app_version:
83 options.app_version = parser.GetVersion()
84 if not options.app_versionCode and not options.app_versionCodeBase:
85 options.app_versionCode = 1
86 if parser.GetDescription():
87 options.description = parser.GetDescription()
88 if parser.GetPermissions():
89 options.permissions = parser.GetPermissions()
90 if parser.GetAppUrl():
91 options.app_url = parser.GetAppUrl()
92 elif parser.GetAppLocalPath():
93 options.app_local_path = parser.GetAppLocalPath()
95 print('Error: there is no app launch path defined in manifest.json.')
97 if parser.GetAppRoot():
98 options.app_root = parser.GetAppRoot()
99 options.icon_dict = parser.GetIcons()
100 if parser.GetOrientation():
101 options.orientation = parser.GetOrientation()
102 if parser.GetFullScreenFlag().lower() == 'true':
103 options.fullscreen = True
104 elif parser.GetFullScreenFlag().lower() == 'false':
105 options.fullscreen = False
109 def ParseXPK(options, out_dir):
110 cmd = ['python', os.path.join (xwalk_dir, 'parse_xpk.py'),
111 '--file=%s' % os.path.expanduser(options.xpk),
112 '--out=%s' % out_dir]
115 print ('Use the manifest from XPK by default '
116 'when "--xpk" option is specified, and '
117 'the "--manifest" option would be ignored.')
120 if os.path.isfile(os.path.join(out_dir, 'manifest.json')):
121 options.manifest = os.path.join(out_dir, 'manifest.json')
123 print('XPK doesn\'t contain manifest file.')
127 def FindExtensionJars(root_path):
128 ''' Find all .jar files for external extensions. '''
130 if not os.path.exists(root_path):
131 return extension_jars
133 for afile in os.listdir(root_path):
134 if os.path.isdir(os.path.join(root_path, afile)):
135 base_name = os.path.basename(afile)
136 extension_jar = os.path.join(root_path, afile, base_name + '.jar')
137 if os.path.isfile(extension_jar):
138 extension_jars.append(extension_jar)
139 return extension_jars
142 # Follows the recommendation from
143 # http://software.intel.com/en-us/blogs/2012/11/12/how-to-publish-
144 # your-apps-on-google-play-for-x86-based-android-devices-using
145 def MakeVersionCode(options):
146 ''' Construct a version code'''
147 if options.app_versionCode:
148 return options.app_versionCode
150 # First digit is ABI, ARM=2, x86=6
152 if options.arch == 'arm':
154 if options.arch == 'x86':
157 if options.app_versionCodeBase:
158 b = str(options.app_versionCodeBase)
160 print('Version code base must be 7 digits or less: '
161 'versionCodeBase=%s' % (b))
163 # zero pad to 7 digits, middle digits can be used for other
164 # features, according to recommendation in URL
165 return '%s%s' % (abi, b.zfill(7))
168 def GetExtensionBinaryPathList():
169 local_extension_list = []
170 extensions_path = os.path.join(os.getcwd(), "extensions")
171 exist_extension_list = GetExtensionList(extensions_path)
172 for item in exist_extension_list:
173 build_json_path = os.path.join(extensions_path, item, "build.json")
174 with open(build_json_path) as fd:
176 if not GetExtensionStatus(item, extensions_path):
179 if data.get("binary_path", False):
180 extension_binary_path = os.path.join(extensions_path,
184 print("The extension \"%s\" doesn't exists." % item)
186 if os.path.isdir(extension_binary_path):
187 local_extension_list.append(extension_binary_path)
189 print("The extension \"%s\" doesn't exists." % item)
192 return local_extension_list
195 def Customize(options, app_info, manifest):
196 app_info.package = options.package
197 app_info.app_name = options.name
198 # 'org.xwalk.my_first_app' => 'MyFirstApp'
199 android_name = options.package.split('.')[-1].split('_')
200 app_info.android_name = ''.join([i.capitalize() for i in android_name if i])
201 if options.app_version:
202 app_info.app_version = options.app_version
203 app_info.app_versionCode = MakeVersionCode(options)
205 app_info.app_root = os.path.expanduser(options.app_root)
206 if options.enable_remote_debugging:
207 app_info.remote_debugging = '--enable-remote-debugging'
208 if options.use_animatable_view:
209 app_info.use_animatable_view = '--use-animatable-view'
210 if options.fullscreen:
211 app_info.fullscreen_flag = '-f'
212 if options.orientation:
213 app_info.orientation = options.orientation
215 app_info.icon = '%s' % os.path.expanduser(options.icon)
217 #Add local extensions to extension list.
218 extension_binary_path_list = GetExtensionBinaryPathList()
219 if len(extension_binary_path_list) > 0:
220 if options.extensions is None:
221 options.extensions = ""
223 options.extensions += os.pathsep
225 for item in extension_binary_path_list:
226 options.extensions += item
227 options.extensions += os.pathsep
228 #trim final path separator
229 options.extensions = options.extensions[0:-1]
231 CustomizeAll(app_info, options.description, options.icon_dict,
232 options.permissions, options.app_url, options.app_local_path,
233 options.keep_screen_on, options.extensions, manifest,
234 options.xwalk_command_line, options.compressor)
237 def Execution(options, name):
238 android_path = Which('android')
239 if android_path is None:
240 print('The "android" binary could not be found. Check your Android SDK '
241 'installation and your PATH environment variable.')
244 api_level = GetAndroidApiLevel(android_path)
246 print('Please install Android API level (>=14) first.')
248 target_string = 'android-%d' % api_level
250 if options.keystore_path:
251 key_store = os.path.expanduser(options.keystore_path)
252 if options.keystore_alias:
253 key_alias = options.keystore_alias
255 print('Please provide an alias name of the developer key.')
257 if options.keystore_passcode:
258 key_code = options.keystore_passcode
261 if options.keystore_alias_passcode:
262 key_alias_code = options.keystore_alias_passcode
264 key_alias_code = None
266 print ('Use xwalk\'s keystore by default for debugging. '
267 'Please switch to your keystore when distributing it to app market.')
268 key_store = os.path.join(xwalk_dir, 'xwalk-debug.keystore')
269 key_alias = 'xwalkdebugkey'
270 key_code = 'xwalkdebug'
271 key_alias_code = 'xwalkdebug'
273 # Check whether ant is installed.
274 ant_path = Which('ant')
276 print('Ant could not be found. Please make sure it is installed.')
279 # Update android project for app and xwalk_core_library.
280 update_project_cmd = [android_path, 'update', 'project',
281 '--path', os.path.join (xwalk_dir, name),
282 '--target', target_string,
284 if options.mode == 'embedded':
285 RunCommand([android_path, 'update', 'lib-project',
286 '--path', os.path.join(xwalk_dir, name, 'xwalk_core_library'),
287 '--target', target_string])
288 update_project_cmd.extend(['-l', 'xwalk_core_library'])
290 # Shared mode doesn't need xwalk_runtime_java.jar.
291 os.remove(os.path.join(xwalk_dir, name, 'libs', 'xwalk_runtime_java.jar'))
293 RunCommand(update_project_cmd)
295 # Check whether external extensions are included.
296 extensions_string = 'xwalk-extensions'
297 extensions_dir = os.path.join(xwalk_dir, name, extensions_string)
298 external_extension_jars = FindExtensionJars(extensions_dir)
299 for external_extension_jar in external_extension_jars:
300 shutil.copyfile(external_extension_jar,
301 os.path.join(xwalk_dir, name, 'libs',
302 os.path.basename(external_extension_jar)))
304 if options.mode == 'embedded':
305 # Remove existing native libraries in xwalk_core_library, they are probably
306 # for the last execution to make apk for another CPU arch.
307 # And then copy the native libraries for the specified arch into
308 # xwalk_core_library.
309 arch = ConvertArchNameToArchFolder(options.arch)
311 print ('Invalid CPU arch: %s.' % arch)
313 library_lib_path = os.path.join(xwalk_dir, name, 'xwalk_core_library',
315 for dir_name in os.listdir(library_lib_path):
316 lib_dir = os.path.join(library_lib_path, dir_name)
317 if ContainsNativeLibrary(lib_dir):
318 shutil.rmtree(lib_dir)
319 native_lib_path = os.path.join(xwalk_dir, name, 'native_libs', arch)
320 if ContainsNativeLibrary(native_lib_path):
321 shutil.copytree(native_lib_path, os.path.join(library_lib_path, arch))
323 print('No %s native library has been found for creating a Crosswalk '
324 'embedded APK.' % arch)
327 ant_cmd = [ant_path, 'release', '-f',
328 os.path.join(xwalk_dir, name, 'build.xml')]
329 if not options.verbose:
330 ant_cmd.extend(['-quiet'])
331 ant_cmd.extend(['-Dkey.store=%s' % os.path.abspath(key_store)])
332 ant_cmd.extend(['-Dkey.alias=%s' % key_alias])
334 ant_cmd.extend(['-Dkey.store.password=%s' % key_code])
336 ant_cmd.extend(['-Dkey.alias.password=%s' % key_alias_code])
337 ant_result = subprocess.call(ant_cmd)
339 print('Command "%s" exited with non-zero exit code %d'
340 % (' '.join(ant_cmd), ant_result))
343 src_file = os.path.join(xwalk_dir, name, 'bin', '%s-release.apk' % name)
345 if options.app_version:
346 package_name += ('_' + options.app_version)
347 if options.mode == 'shared':
348 dst_file = os.path.join(options.target_dir, '%s.apk' % package_name)
349 elif options.mode == 'embedded':
350 dst_file = os.path.join(options.target_dir,
351 '%s_%s.apk' % (package_name, options.arch))
352 shutil.copyfile(src_file, dst_file)
355 def PrintPackageInfo(options, name, packaged_archs):
356 package_name_version = os.path.join(options.target_dir, name)
357 if options.app_version:
358 package_name_version += '_' + options.app_version
360 if len(packaged_archs) == 0:
361 print ('A non-platform specific APK for the web application "%s" was '
362 'generated successfully at\n%s.apk. It requires a shared Crosswalk '
363 'Runtime to be present.'
364 % (name, package_name_version))
367 for arch in packaged_archs:
368 print ('An APK for the web application "%s" including the Crosswalk '
369 'Runtime built for %s was generated successfully, which can be '
370 'found at\n%s_%s.apk.'
371 % (name, arch, package_name_version, arch))
373 all_archs = set(AllArchitectures())
375 if len(packaged_archs) != len(all_archs):
376 missed_archs = all_archs - set(packaged_archs)
377 print ('\n\nWARNING: ')
378 print ('This APK will only work on %s based Android devices. Consider '
379 'building for %s as well.' %
380 (', '.join(packaged_archs), ', '.join(missed_archs)))
382 print ('\n\n%d APKs were created for %s devices. '
383 % (len(all_archs), ', '.join(all_archs)))
384 print ('Please install the one that matches the processor architecture '
385 'of your device.\n\n')
386 print ('If you are going to submit this application to an application '
387 'store, please make sure you submit both packages.\nInstructions '
388 'for submitting multiple APKs to Google Play Store are available '
389 'here:\nhttps://software.intel.com/en-us/html5/articles/submitting'
390 '-multiple-crosswalk-apk-to-google-play-store')
392 def MakeApk(options, app_info, manifest):
393 Customize(options, app_info, manifest)
394 name = app_info.android_name
396 if options.mode == 'shared':
397 Execution(options, name)
398 elif options.mode == 'embedded':
399 # Copy xwalk_core_library into app folder and move the native libraries
401 # When making apk for specified CPU arch, will only include the
402 # corresponding native library by copying it back into xwalk_core_library.
403 target_library_path = os.path.join(xwalk_dir, name, 'xwalk_core_library')
404 shutil.copytree(os.path.join(xwalk_dir, 'xwalk_core_library'),
406 library_lib_path = os.path.join(target_library_path, 'libs')
407 native_lib_path = os.path.join(xwalk_dir, name, 'native_libs')
408 os.makedirs(native_lib_path)
410 for dir_name in os.listdir(library_lib_path):
411 lib_dir = os.path.join(library_lib_path, dir_name)
412 if ContainsNativeLibrary(lib_dir):
413 shutil.move(lib_dir, os.path.join(native_lib_path, dir_name))
414 available_archs.append(dir_name)
416 Execution(options, name)
417 packaged_archs.append(options.arch)
419 # If the arch option is unspecified, all of available platform APKs
421 valid_archs = ['x86', 'armeabi-v7a']
422 for arch in valid_archs:
423 if arch in available_archs:
424 if arch.find('x86') != -1:
426 elif arch.find('arm') != -1:
428 Execution(options, name)
429 packaged_archs.append(options.arch)
431 print('Warning: failed to create package for arch "%s" '
432 'due to missing native library' % arch)
434 if len(packaged_archs) == 0:
435 print('No packages created, aborting')
438 PrintPackageInfo(options, name, packaged_archs)
441 parser = optparse.OptionParser()
442 parser.add_option('-v', '--version', action='store_true',
443 dest='version', default=False,
444 help='The version of this python tool.')
445 parser.add_option('--verbose', action="store_true",
446 dest='verbose', default=False,
447 help='Print debug messages.')
448 info = ('The packaging mode of the web application. The value \'shared\' '
449 'means that the runtime is shared across multiple application '
450 'instances and that the runtime needs to be distributed separately. '
451 'The value \'embedded\' means that the runtime is embedded into the '
452 'application itself and distributed along with it.'
453 'Set the default mode as \'embedded\'. For example: --mode=embedded')
454 parser.add_option('--mode', choices=('embedded', 'shared'),
455 default='embedded', help=info)
456 info = ('The target architecture of the embedded runtime. Supported values '
457 'are \'x86\' and \'arm\'. Note, if undefined, APKs for all possible '
458 'architestures will be generated.')
459 parser.add_option('--arch', choices=AllArchitectures(), help=info)
460 group = optparse.OptionGroup(parser, 'Application Source Options',
461 'This packaging tool supports 3 kinds of web application source: '
462 '1) XPK package; 2) manifest.json; 3) various command line options, '
463 'for example, \'--app-url\' for website, \'--app-root\' and '
464 '\'--app-local-path\' for local web application.')
465 info = ('The path of the XPK package. For example, --xpk=/path/to/xpk/file')
466 group.add_option('--xpk', help=info)
467 info = ('The manifest file with the detail description of the application. '
468 'For example, --manifest=/path/to/your/manifest/file')
469 group.add_option('--manifest', help=info)
470 info = ('The url of application. '
471 'This flag allows to package website as apk. For example, '
472 '--app-url=http://www.intel.com')
473 group.add_option('--app-url', help=info)
474 info = ('The root path of the web app. '
475 'This flag allows to package local web app as apk. For example, '
476 '--app-root=/root/path/of/the/web/app')
477 group.add_option('--app-root', help=info)
478 info = ('The relative path of entry file based on the value from '
479 '\'app_root\'. This flag should work with \'--app-root\' together. '
480 'For example, --app-local-path=/relative/path/of/entry/file')
481 group.add_option('--app-local-path', help=info)
482 parser.add_option_group(group)
483 group = optparse.OptionGroup(parser, 'Mandatory arguments',
484 'They are used for describing the APK information through '
485 'command line options.')
486 info = ('The apk name. For example, --name="Your Application Name"')
487 group.add_option('--name', help=info)
488 info = ('The package name. For example, '
489 '--package=com.example.YourPackage')
490 group.add_option('--package', help=info)
491 parser.add_option_group(group)
492 group = optparse.OptionGroup(parser, 'Optional arguments',
493 'They are used for various settings for applications through '
494 'command line options.')
495 info = ('The version name of the application. '
496 'For example, --app-version=1.0.0')
497 group.add_option('--app-version', help=info)
498 info = ('The version code of the application. '
499 'For example, --app-versionCode=24')
500 group.add_option('--app-versionCode', type='int', help=info)
501 info = ('The version code base of the application. Version code will '
502 'be made by adding a prefix based on architecture to the version '
503 'code base. For example, --app-versionCodeBase=24')
504 group.add_option('--app-versionCodeBase', type='int', help=info)
505 info = ('Use command lines.'
506 'Crosswalk is powered by Chromium and supports Chromium command line.'
508 '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
509 group.add_option('--xwalk-command-line', default='', help=info)
510 info = ('The description of the application. For example, '
511 '--description=YourApplicationDescription')
512 group.add_option('--description', help=info)
513 group.add_option('--enable-remote-debugging', action='store_true',
514 dest='enable_remote_debugging', default=False,
515 help='Enable remote debugging.')
516 group.add_option('--use-animatable-view', action='store_true',
517 dest='use_animatable_view', default=False,
518 help='Enable using animatable view (TextureView).')
519 info = ('The list of external extension paths splitted by OS separators. '
520 'The separators are \':\' , \';\' and \':\' on Linux, Windows and '
521 'Mac OS respectively. For example, '
522 '--extensions=/path/to/extension1:/path/to/extension2.')
523 group.add_option('--extensions', help=info)
524 group.add_option('-f', '--fullscreen', action='store_true',
525 dest='fullscreen', default=False,
526 help='Make application fullscreen.')
527 group.add_option('--keep-screen-on', action='store_true', default=False,
528 help='Support keeping screen on')
529 info = ('The path of application icon. '
530 'Such as: --icon=/path/to/your/customized/icon')
531 group.add_option('--icon', help=info)
532 info = ('The orientation of the web app\'s display on the device. '
533 'For example, --orientation=landscape. The default value is '
534 '\'unspecified\'. The permitted values are from Android: '
535 'http://developer.android.com/guide/topics/manifest/'
536 'activity-element.html#screen')
537 group.add_option('--orientation', help=info)
538 info = ('The list of permissions to be used by web application. For example, '
539 '--permissions=geolocation:webgl')
540 group.add_option('--permissions', help=info)
541 info = ('Packaging tool will move the output APKS to the target directory')
542 group.add_option('--target-dir', default=os.getcwd(), help=info)
543 parser.add_option_group(group)
544 group = optparse.OptionGroup(parser, 'Keystore Options',
545 'The keystore is a signature from web developer, it\'s used when '
546 'developer wants to distribute the applications.')
547 info = ('The path to the developer keystore. For example, '
548 '--keystore-path=/path/to/your/developer/keystore')
549 group.add_option('--keystore-path', help=info)
550 info = ('The alias name of keystore. For example, --keystore-alias=name')
551 group.add_option('--keystore-alias', help=info)
552 info = ('The passcode of keystore. For example, --keystore-passcode=code')
553 group.add_option('--keystore-passcode', help=info)
554 info = ('Passcode for alias\'s private key in the keystore, '
555 'For example, --keystore-alias-passcode=alias-code')
556 group.add_option('--keystore-alias-passcode', help=info)
557 info = ('Minify and obfuscate javascript and css.'
558 '--compressor: compress javascript and css.'
559 '--compressor=js: compress javascript.'
560 '--compressor=css: compress css.')
561 group.add_option('--compressor', dest='compressor', action='callback',
562 callback=ParseParameterForCompressor, type='string',
564 parser.add_option_group(group)
565 options, _ = parser.parse_args()
571 if os.path.isfile('VERSION'):
572 print(GetVersion('VERSION'))
575 parser.error('VERSION was not found, so Crosswalk\'s version could not '
580 xpk_name = os.path.splitext(os.path.basename(options.xpk))[0]
581 xpk_temp_dir = os.path.join(xwalk_dir, xpk_name + '_xpk')
582 ParseXPK(options, xpk_temp_dir)
584 if options.app_root and not options.manifest:
585 manifest_path = os.path.join(options.app_root, 'manifest.json')
586 if os.path.exists(manifest_path):
587 print('Using manifest.json distributed with the application.')
588 options.manifest = manifest_path
592 if not options.manifest:
593 # The checks here are really convoluted, but at the moment make_apk
594 # misbehaves any of the following conditions is true.
596 # 1) --app-url must be passed without either --app-local-path or
598 if options.app_root or options.app_local_path:
599 parser.error('You must pass either "--app-url" or "--app-local-path" '
600 'with "--app-root", but not all.')
602 # 2) --app-url is not passed but only one of --app-local-path and
604 if bool(options.app_root) != bool(options.app_local_path):
605 parser.error('You must specify both "--app-local-path" and '
607 # 3) None of --app-url, --app-local-path and --app-root are passed.
608 elif not options.app_root and not options.app_local_path:
609 parser.error('You must pass either "--app-url" or "--app-local-path" '
610 'with "--app-root".')
612 if options.permissions:
613 permission_list = options.permissions.split(':')
615 print('Warning: all supported permissions on Android port are added. '
616 'Refer to https://github.com/crosswalk-project/'
617 'crosswalk-website/wiki/Crosswalk-manifest')
618 permission_list = permission_mapping_table.keys()
619 options.permissions = HandlePermissionList(permission_list)
620 options.icon_dict = {}
623 manifest = ParseManifest(options)
624 except SystemExit as ec:
628 parser.error('An APK name is required. Please use the "--name" option.')
630 if not options.package:
631 parser.error('A package name is required. Please use the "--package" '
633 VerifyPackageName(options.package)
635 if (options.app_root and options.app_local_path and
636 not os.path.isfile(os.path.join(options.app_root,
637 options.app_local_path))):
638 print('Please make sure that the local path file of launching app '
642 if options.target_dir:
643 target_dir = os.path.abspath(os.path.expanduser(options.target_dir))
644 options.target_dir = target_dir
645 if not os.path.isdir(target_dir):
646 os.makedirs(target_dir)
649 MakeApk(options, app_info, manifest)
650 except SystemExit as ec:
651 CleanDir(app_info.android_name)
653 CleanDir(xpk_temp_dir)
658 if __name__ == '__main__':
659 sys.exit(main(sys.argv))