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
15 from app_info import AppInfo
16 from customize import VerifyAppName, CustomizeAll, \
17 ParseParameterForCompressor, ReplaceSpaceWithUnderscore
18 from handle_permissions import permission_mapping_table
19 from manifest_json_parser import HandlePermissionList
20 from manifest_json_parser import ManifestJsonParser
23 NATIVE_LIBRARY = 'libxwalkcore.so'
27 if os.path.exists(path):
31 def AllArchitectures():
35 def ConvertArchNameToArchFolder(arch):
40 return arch_dict.get(arch, None)
43 def AddExeExtensions(name):
44 exts_str = os.environ.get('PATHEXT', '').lower()
45 exts = [_f for _f in exts_str.split(os.pathsep) if _f]
49 result.append(name + e)
53 def RunCommand(command, verbose=False, shell=False):
54 """Runs the command list, print the output, and propagate its result."""
55 proc = subprocess.Popen(command, stdout=subprocess.PIPE,
56 stderr=subprocess.STDOUT, shell=shell)
58 output = proc.communicate()[0]
59 result = proc.returncode
61 print(output.decode("utf-8").strip())
63 print ('Command "%s" exited with non-zero exit code %d'
64 % (' '.join(command), result))
66 return output.decode("utf-8")
70 """Searches PATH for executable files with the given name, also taking
71 PATHEXT into account. Returns the first existing match, or None if no matches
73 for path in os.environ.get('PATH', '').split(os.pathsep):
74 for filename in AddExeExtensions(name):
75 full_path = os.path.join(path, filename)
76 if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
81 def GetAndroidApiLevel():
82 """Get Highest Android target level installed.
83 return -1 if no targets have been found.
85 target_output = RunCommand(['android', 'list', 'target', '-c'])
86 target_regex = re.compile(r'android-(\d+)')
87 targets = [int(i) for i in target_regex.findall(target_output)]
93 """Get the version of this python tool."""
94 version_str = 'Crosswalk app packaging tool version is '
95 file_handle = open(path, 'r')
96 src_content = file_handle.read()
97 version_nums = re.findall(r'\d+', src_content)
98 version_str += ('.').join(version_nums)
103 def ContainsNativeLibrary(path):
104 return os.path.isfile(os.path.join(path, NATIVE_LIBRARY))
107 def ParseManifest(options):
108 parser = ManifestJsonParser(os.path.expanduser(options.manifest))
110 options.name = parser.GetAppName()
111 if not options.app_version:
112 options.app_version = parser.GetVersion()
113 if not options.app_versionCode and not options.app_versionCodeBase:
114 options.app_versionCode = 1
115 if parser.GetDescription():
116 options.description = parser.GetDescription()
117 if parser.GetPermissions():
118 options.permissions = parser.GetPermissions()
119 if parser.GetAppUrl():
120 options.app_url = parser.GetAppUrl()
121 elif parser.GetAppLocalPath():
122 options.app_local_path = parser.GetAppLocalPath()
124 print('Error: there is no app launch path defined in manifest.json.')
126 if parser.GetAppRoot():
127 options.app_root = parser.GetAppRoot()
128 options.icon_dict = parser.GetIcons()
129 if parser.GetOrientation():
130 options.orientation = parser.GetOrientation()
131 if parser.GetFullScreenFlag().lower() == 'true':
132 options.fullscreen = True
133 elif parser.GetFullScreenFlag().lower() == 'false':
134 options.fullscreen = False
138 def ParseXPK(options, out_dir):
139 cmd = ['python', 'parse_xpk.py',
140 '--file=%s' % os.path.expanduser(options.xpk),
141 '--out=%s' % out_dir]
144 print ('Use the manifest from XPK by default '
145 'when "--xpk" option is specified, and '
146 'the "--manifest" option would be ignored.')
149 if os.path.isfile(os.path.join(out_dir, 'manifest.json')):
150 options.manifest = os.path.join(out_dir, 'manifest.json')
152 print('XPK doesn\'t contain manifest file.')
156 def FindExtensionJars(root_path):
157 ''' Find all .jar files for external extensions. '''
159 if not os.path.exists(root_path):
160 return extension_jars
162 for afile in os.listdir(root_path):
163 if os.path.isdir(os.path.join(root_path, afile)):
164 base_name = os.path.basename(afile)
165 extension_jar = os.path.join(root_path, afile, base_name + '.jar')
166 if os.path.isfile(extension_jar):
167 extension_jars.append(extension_jar)
168 return extension_jars
171 # Follows the recommendation from
172 # http://software.intel.com/en-us/blogs/2012/11/12/how-to-publish-
173 # your-apps-on-google-play-for-x86-based-android-devices-using
174 def MakeVersionCode(options):
175 ''' Construct a version code'''
176 if options.app_versionCode:
177 return options.app_versionCode
179 # First digit is ABI, ARM=2, x86=6
181 if options.arch == 'arm':
183 if options.arch == 'x86':
186 if options.app_versionCodeBase:
187 b = str(options.app_versionCodeBase)
189 print('Version code base must be 7 digits or less: '
190 'versionCodeBase=%s' % (b))
192 # zero pad to 7 digits, middle digits can be used for other
193 # features, according to recommendation in URL
194 return '%s%s' % (abi, b.zfill(7))
197 def Customize(options, app_info, manifest):
198 app_info.package = options.package
199 app_info.app_name = options.name
200 app_info.android_name = ReplaceSpaceWithUnderscore(options.name)
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.fullscreen:
209 app_info.fullscreen_flag = '-f'
210 if options.orientation:
211 app_info.orientation = options.orientation
213 app_info.icon = '%s' % os.path.expanduser(options.icon)
214 CustomizeAll(app_info, options.description, options.icon_dict,
215 options.permissions, options.app_url, options.app_local_path,
216 options.keep_screen_on, options.extensions, manifest,
217 options.xwalk_command_line, options.compressor)
220 def Execution(options, name):
221 android_path = Which('android')
222 if android_path is None:
223 print('The "android" binary could not be found. Check your Android SDK '
224 'installation and your PATH environment variable.')
227 api_level = GetAndroidApiLevel()
229 print('Please install Android API level (>=14) first.')
231 target_string = 'android-%d' % api_level
233 if options.keystore_path:
234 key_store = os.path.expanduser(options.keystore_path)
235 if options.keystore_alias:
236 key_alias = options.keystore_alias
238 print('Please provide an alias name of the developer key.')
240 if options.keystore_passcode:
241 key_code = options.keystore_passcode
244 if options.keystore_alias_passcode:
245 key_alias_code = options.keystore_alias_passcode
247 key_alias_code = None
249 print ('Use xwalk\'s keystore by default for debugging. '
250 'Please switch to your keystore when distributing it to app market.')
251 key_store = 'xwalk-debug.keystore'
252 key_alias = 'xwalkdebugkey'
253 key_code = 'xwalkdebug'
254 key_alias_code = 'xwalkdebug'
256 # Check whether ant is installed.
258 cmd = ['ant', '-version']
259 RunCommand(cmd, shell=True)
260 except EnvironmentError:
261 print('Please install ant first.')
264 # Update android project for app and xwalk_core_library.
265 update_project_cmd = ['android', 'update', 'project',
266 '--path', name, '--target', target_string,
268 if options.mode == 'embedded':
269 RunCommand(['android', 'update', 'lib-project',
270 '--path', os.path.join(name, 'xwalk_core_library'),
271 '--target', target_string])
272 update_project_cmd.extend(['-l', 'xwalk_core_library'])
274 # Shared mode doesn't need xwalk_runtime_java.jar.
275 os.remove(os.path.join(name, 'libs', 'xwalk_runtime_java.jar'))
277 RunCommand(update_project_cmd)
279 # Check whether external extensions are included.
280 extensions_string = 'xwalk-extensions'
281 extensions_dir = os.path.join(os.getcwd(), name, extensions_string)
282 external_extension_jars = FindExtensionJars(extensions_dir)
283 for external_extension_jar in external_extension_jars:
284 shutil.copyfile(external_extension_jar,
285 os.path.join(name, 'libs',
286 os.path.basename(external_extension_jar)))
288 if options.mode == 'embedded':
289 # Remove existing native libraries in xwalk_core_library, they are probably
290 # for the last execution to make apk for another CPU arch.
291 # And then copy the native libraries for the specified arch into
292 # xwalk_core_library.
293 arch = ConvertArchNameToArchFolder(options.arch)
295 print ('Invalid CPU arch: %s.' % arch)
297 library_lib_path = os.path.join(name, 'xwalk_core_library', 'libs')
298 for dir_name in os.listdir(library_lib_path):
299 lib_dir = os.path.join(library_lib_path, dir_name)
300 if ContainsNativeLibrary(lib_dir):
301 shutil.rmtree(lib_dir)
302 native_lib_path = os.path.join(name, 'native_libs', arch)
303 if ContainsNativeLibrary(native_lib_path):
304 shutil.copytree(native_lib_path, os.path.join(library_lib_path, arch))
306 print('No %s native library has been found for creating a Crosswalk '
307 'embedded APK.' % arch)
310 ant_cmd = ['ant', 'release', '-f', os.path.join(name, 'build.xml')]
311 if not options.verbose:
312 ant_cmd.extend(['-quiet'])
313 ant_cmd.extend(['-Dkey.store="%s"' % os.path.abspath(key_store)])
314 ant_cmd.extend(['-Dkey.alias="%s"' % key_alias])
316 ant_cmd.extend(['-Dkey.store.password="%s"' % key_code])
318 ant_cmd.extend(['-Dkey.alias.password="%s"' % key_alias_code])
319 ant_result = subprocess.call(ant_cmd)
321 print('Command "%s" exited with non-zero exit code %d'
322 % (' '.join(ant_cmd), ant_result))
325 src_file = os.path.join(name, 'bin', '%s-release.apk' % name)
327 if options.app_version:
328 package_name += ('_' + options.app_version)
329 if options.mode == 'shared':
330 dst_file = os.path.join(options.target_dir, '%s.apk' % package_name)
331 elif options.mode == 'embedded':
332 dst_file = os.path.join(options.target_dir,
333 '%s_%s.apk' % (package_name, options.arch))
334 shutil.copyfile(src_file, dst_file)
337 def PrintPackageInfo(options, name, packaged_archs):
338 package_name_version = os.path.join(options.target_dir, name)
339 if options.app_version:
340 package_name_version += '_' + options.app_version
342 if len(packaged_archs) == 0:
343 print ('A non-platform specific APK for the web application "%s" was '
344 'generated successfully at\n%s.apk. It requires a shared Crosswalk '
345 'Runtime to be present.'
346 % (name, package_name_version))
349 for arch in packaged_archs:
350 print ('An APK for the web application "%s" including the Crosswalk '
351 'Runtime built for %s was generated successfully, which can be '
352 'found at\n%s_%s.apk.'
353 % (name, arch, package_name_version, arch))
355 all_archs = set(AllArchitectures())
357 if len(packaged_archs) != len(all_archs):
358 missed_archs = all_archs - set(packaged_archs)
359 print ('\n\nWARNING: ')
360 print ('This APK will only work on %s based Android devices. Consider '
361 'building for %s as well.' %
362 (', '.join(packaged_archs), ', '.join(missed_archs)))
364 print ('\n\n%d APKs were created for %s devices. '
365 % (len(all_archs), ', '.join(all_archs)))
366 print ('Please install the one that matches the processor architecture '
367 'of your device.\n\n')
368 print ('If you are going to submit this application to an application '
369 'store, please make sure you submit both packages.\nInstructions '
370 'for submitting multiple APKs to Google Play Store are available '
371 'here:\nhttps://software.intel.com/en-us/html5/articles/submitting'
372 '-multiple-crosswalk-apk-to-google-play-store')
374 def MakeApk(options, app_info, manifest):
375 Customize(options, app_info, manifest)
376 name = app_info.android_name
378 if options.mode == 'shared':
379 Execution(options, name)
380 elif options.mode == 'embedded':
381 # Copy xwalk_core_library into app folder and move the native libraries
383 # When making apk for specified CPU arch, will only include the
384 # corresponding native library by copying it back into xwalk_core_library.
385 target_library_path = os.path.join(name, 'xwalk_core_library')
386 shutil.copytree('xwalk_core_library', target_library_path)
387 library_lib_path = os.path.join(target_library_path, 'libs')
388 native_lib_path = os.path.join(name, 'native_libs')
389 os.makedirs(native_lib_path)
391 for dir_name in os.listdir(library_lib_path):
392 lib_dir = os.path.join(library_lib_path, dir_name)
393 if ContainsNativeLibrary(lib_dir):
394 shutil.move(lib_dir, os.path.join(native_lib_path, dir_name))
395 available_archs.append(dir_name)
397 Execution(options, name)
398 packaged_archs.append(options.arch)
400 # If the arch option is unspecified, all of available platform APKs
402 valid_archs = ['x86', 'armeabi-v7a']
403 for arch in valid_archs:
404 if arch in available_archs:
405 if arch.find('x86') != -1:
407 elif arch.find('arm') != -1:
409 Execution(options, name)
410 packaged_archs.append(options.arch)
412 print('Warning: failed to create package for arch "%s" '
413 'due to missing native library' % arch)
415 if len(packaged_archs) == 0:
416 print('No packages created, aborting')
419 PrintPackageInfo(options, name, packaged_archs)
422 parser = optparse.OptionParser()
423 parser.add_option('-v', '--version', action='store_true',
424 dest='version', default=False,
425 help='The version of this python tool.')
426 parser.add_option('--verbose', action="store_true",
427 dest='verbose', default=False,
428 help='Print debug messages.')
429 info = ('The packaging mode of the web application. The value \'shared\' '
430 'means that the runtime is shared across multiple application '
431 'instances and that the runtime needs to be distributed separately. '
432 'The value \'embedded\' means that the runtime is embedded into the '
433 'application itself and distributed along with it.'
434 'Set the default mode as \'embedded\'. For example: --mode=embedded')
435 parser.add_option('--mode', choices=('embedded', 'shared'),
436 default='embedded', help=info)
437 info = ('The target architecture of the embedded runtime. Supported values '
438 'are \'x86\' and \'arm\'. Note, if undefined, APKs for all possible '
439 'architestures will be generated.')
440 parser.add_option('--arch', choices=AllArchitectures(), help=info)
441 group = optparse.OptionGroup(parser, 'Application Source Options',
442 'This packaging tool supports 3 kinds of web application source: '
443 '1) XPK package; 2) manifest.json; 3) various command line options, '
444 'for example, \'--app-url\' for website, \'--app-root\' and '
445 '\'--app-local-path\' for local web application.')
446 info = ('The path of the XPK package. For example, --xpk=/path/to/xpk/file')
447 group.add_option('--xpk', help=info)
448 info = ('The manifest file with the detail description of the application. '
449 'For example, --manifest=/path/to/your/manifest/file')
450 group.add_option('--manifest', help=info)
451 info = ('The url of application. '
452 'This flag allows to package website as apk. For example, '
453 '--app-url=http://www.intel.com')
454 group.add_option('--app-url', help=info)
455 info = ('The root path of the web app. '
456 'This flag allows to package local web app as apk. For example, '
457 '--app-root=/root/path/of/the/web/app')
458 group.add_option('--app-root', help=info)
459 info = ('The relative path of entry file based on the value from '
460 '\'app_root\'. This flag should work with \'--app-root\' together. '
461 'For example, --app-local-path=/relative/path/of/entry/file')
462 group.add_option('--app-local-path', help=info)
463 parser.add_option_group(group)
464 group = optparse.OptionGroup(parser, 'Mandatory arguments',
465 'They are used for describing the APK information through '
466 'command line options.')
467 info = ('The apk name. For example, --name="Your Application Name"')
468 group.add_option('--name', help=info)
469 info = ('The package name. For example, '
470 '--package=com.example.YourPackage')
471 group.add_option('--package', help=info)
472 parser.add_option_group(group)
473 group = optparse.OptionGroup(parser, 'Optional arguments',
474 'They are used for various settings for applications through '
475 'command line options.')
476 info = ('The version name of the application. '
477 'For example, --app-version=1.0.0')
478 group.add_option('--app-version', help=info)
479 info = ('The version code of the application. '
480 'For example, --app-versionCode=24')
481 group.add_option('--app-versionCode', type='int', help=info)
482 info = ('The version code base of the application. Version code will '
483 'be made by adding a prefix based on architecture to the version '
484 'code base. For example, --app-versionCodeBase=24')
485 group.add_option('--app-versionCodeBase', type='int', help=info)
486 info = ('Use command lines.'
487 'Crosswalk is powered by Chromium and supports Chromium command line.'
489 '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
490 group.add_option('--xwalk-command-line', default='', help=info)
491 info = ('The description of the application. For example, '
492 '--description=YourApplicationDescription')
493 group.add_option('--description', help=info)
494 group.add_option('--enable-remote-debugging', action='store_true',
495 dest='enable_remote_debugging', default=False,
496 help='Enable remote debugging.')
497 info = ('The list of external extension paths splitted by OS separators. '
498 'The separators are \':\' , \';\' and \':\' on Linux, Windows and '
499 'Mac OS respectively. For example, '
500 '--extensions=/path/to/extension1:/path/to/extension2.')
501 group.add_option('--extensions', help=info)
502 group.add_option('-f', '--fullscreen', action='store_true',
503 dest='fullscreen', default=False,
504 help='Make application fullscreen.')
505 group.add_option('--keep-screen-on', action='store_true', default=False,
506 help='Support keeping screen on')
507 info = ('The path of application icon. '
508 'Such as: --icon=/path/to/your/customized/icon')
509 group.add_option('--icon', help=info)
510 info = ('The orientation of the web app\'s display on the device. '
511 'For example, --orientation=landscape. The default value is '
512 '\'unspecified\'. The permitted values are from Android: '
513 'http://developer.android.com/guide/topics/manifest/'
514 'activity-element.html#screen')
515 group.add_option('--orientation', help=info)
516 info = ('The list of permissions to be used by web application. For example, '
517 '--permissions=geolocation:webgl')
518 group.add_option('--permissions', help=info)
519 info = ('Packaging tool will move the output APKS to the target directory')
520 group.add_option('--target-dir', default=os.getcwd(), help=info)
521 parser.add_option_group(group)
522 group = optparse.OptionGroup(parser, 'Keystore Options',
523 'The keystore is a signature from web developer, it\'s used when '
524 'developer wants to distribute the applications.')
525 info = ('The path to the developer keystore. For example, '
526 '--keystore-path=/path/to/your/developer/keystore')
527 group.add_option('--keystore-path', help=info)
528 info = ('The alias name of keystore. For example, --keystore-alias=name')
529 group.add_option('--keystore-alias', help=info)
530 info = ('The passcode of keystore. For example, --keystore-passcode=code')
531 group.add_option('--keystore-passcode', help=info)
532 info = ('Passcode for alias\'s private key in the keystore, '
533 'For example, --keystore-alias-passcode=alias-code')
534 group.add_option('--keystore-alias-passcode', help=info)
535 info = ('Minify and obfuscate javascript and css.'
536 '--compressor: compress javascript and css.'
537 '--compressor=js: compress javascript.'
538 '--compressor=css: compress css.')
539 group.add_option('--compressor', dest='compressor', action='callback',
540 callback=ParseParameterForCompressor, type='string',
542 parser.add_option_group(group)
543 options, _ = parser.parse_args()
549 if os.path.isfile('VERSION'):
550 print(GetVersion('VERSION'))
553 parser.error('VERSION was not found, so Crosswalk\'s version could not '
558 xpk_name = os.path.splitext(os.path.basename(options.xpk))[0]
559 xpk_temp_dir = xpk_name + '_xpk'
560 ParseXPK(options, xpk_temp_dir)
562 if options.app_root and not options.manifest:
563 manifest_path = os.path.join(options.app_root, 'manifest.json')
564 if os.path.exists(manifest_path):
565 print('Using manifest.json distributed with the application.')
566 options.manifest = manifest_path
570 if not options.manifest:
571 # The checks here are really convoluted, but at the moment make_apk
572 # misbehaves any of the following conditions is true.
574 # 1) --app-url must be passed without either --app-local-path or
576 if options.app_root or options.app_local_path:
577 parser.error('You must pass either "--app-url" or "--app-local-path" '
578 'with "--app-root", but not all.')
580 # 2) --app-url is not passed but only one of --app-local-path and
582 if bool(options.app_root) != bool(options.app_local_path):
583 parser.error('You must specify both "--app-local-path" and '
585 # 3) None of --app-url, --app-local-path and --app-root are passed.
586 elif not options.app_root and not options.app_local_path:
587 parser.error('You must pass either "--app-url" or "--app-local-path" '
588 'with "--app-root".')
590 if options.permissions:
591 permission_list = options.permissions.split(':')
593 print('Warning: all supported permissions on Android port are added. '
594 'Refer to https://github.com/crosswalk-project/'
595 'crosswalk-website/wiki/Crosswalk-manifest')
596 permission_list = permission_mapping_table.keys()
597 options.permissions = HandlePermissionList(permission_list)
598 options.icon_dict = {}
601 manifest = ParseManifest(options)
602 except SystemExit as ec:
606 parser.error('An APK name is required. Please use the "--name" option.')
607 VerifyAppName(options.name)
609 if not options.package:
610 parser.error('A package name is required. Please use the "--package" '
612 VerifyAppName(options.package, 'packagename')
614 if (options.app_root and options.app_local_path and
615 not os.path.isfile(os.path.join(options.app_root,
616 options.app_local_path))):
617 print('Please make sure that the local path file of launching app '
621 if options.target_dir:
622 target_dir = os.path.abspath(os.path.expanduser(options.target_dir))
623 options.target_dir = target_dir
624 if not os.path.isdir(target_dir):
625 os.makedirs(target_dir)
628 MakeApk(options, app_info, manifest)
629 except SystemExit as ec:
630 CleanDir(app_info.android_name)
632 CleanDir(xpk_temp_dir)
637 if __name__ == '__main__':
638 sys.exit(main(sys.argv))