diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a0cbce --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc +~* diff --git a/README.md b/README.md index ea239be..bdfccb0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,13 @@ Key concepts =============================================== - Interact with the Cordova CLI directly from Python - Enables building and archiving PhoneGap applications from your Python code +- Interact with Jarsigner and Zipalign to sign and verify android apks. + + +***Note:*** +This uses Google's methodology of +[manually signing apks](http://developer.android.com/tools/publishing/app-signing.html#signing-manually) +using [Jarsigner] and [Zipalign] to verify and sign apks. Usage @@ -22,5 +29,31 @@ Usage APPLICATION_ROOT ) + # Build a debug version for any platform application. application.build('android') # or any installed platform - application.archive('ios') # or any installed platform \ No newline at end of file + application.archive('ios') # or any installed platform + + # Build a relase version for any platform application + application.build('android', release=True) # or any installed platform + application.build('ios', release=True) # or any installed platform + + # Signing Android application (apk) + application.sign_android_apk(keystore="/path/to/keystore", keypass="passcode", storepass="passcode") + + + +Requirements +============ + +* [NodeJs] +* [NPM] +* [Jarsigner] +* [Zipalign] +* [Cordova] + + +[NodeJs]: https://nodejs.org/en/ +[NPM]: https://www.npmjs.com/ +[Jarsigner]: http://docs.oracle.com/javase/6/docs/technotes/tools/windows/jarsigner.html +[Zipalign]: http://developer.android.com/tools/help/zipalign.html +[Cordova]: https://cordova.apache.org/ diff --git a/cordova/__init__.py b/cordova/__init__.py index 14edd34..6d2b266 100644 --- a/cordova/__init__.py +++ b/cordova/__init__.py @@ -5,15 +5,18 @@ import os import re import subprocess +from datetime import datetime from .decorators import for_all_methods, chdir_context BUILD_LOCATION = { 'android': { - 'debug': 'platforms/android/ant-build/%s-debug.apk', - 'release': 'platforms/android/ant-build/%s-release-unsigned.apk', - 'archive': 'platforms/%s-android.zip' + 'debug': 'platforms/android/build/outputs/apk/%s-debug.apk', + 'release': ('platforms/android/build/' + 'outputs/apk/%s-release-unsigned.apk'), + 'archive': 'platforms/%s-android.zip', + 'signed': 'platforms/android/build/outputs/apk/%s-%s.apk' }, 'ios': { 'debug': 'platforms/ios/build/emulator/%s.app', @@ -27,18 +30,21 @@ class App(object): path = None name = None + debug = False # We need to initialize the application with the path of the root # of the project - def __init__(self, path, name, *args, **kwargs): + def __init__(self, path, name, debug=False, *args, **kwargs): self.path = path self.name = name + self.debug = debug super(App, self).__init__(*args, **kwargs) def __platform_list(self): + """List platforms supported by cordova.""" platform_ls_output = subprocess.check_output([ 'cordova', 'platform', 'ls' - ]).splitlines() + ], shell=self.debug).splitlines() installed = re.findall(r'[,:]\s(\w+)\s\d+', platform_ls_output[0]) available = re.findall(r'[,:]\s(\w+)\s', platform_ls_output[1]) @@ -46,15 +52,18 @@ def __platform_list(self): return (installed, available) def installed_platform_list(self): + """List the installed platforms for the project.""" return self.__platform_list()[0] def available_platform_list(self): + """List the available platforms that can be used by the project.""" return self.__platform_list()[1] def add_platform(self, platform): + """Add supported platform to the project.""" return_code = subprocess.call([ 'cordova', 'platform', 'add', platform - ]) + ], shell=self.debug) if return_code == 0: return True @@ -62,9 +71,10 @@ def add_platform(self, platform): return False def remove_platform(self, platform): + """Remove supported platfrom from the project.""" return_code = subprocess.call([ 'cordova', 'platform', 'remove', platform - ]) + ], shell=self.debug) if return_code == 0: return True @@ -72,13 +82,14 @@ def remove_platform(self, platform): return False def archive(self, platform): + """Archive the android project files into a zip file.""" os.chdir('platforms') return_code = subprocess.call([ 'tar', '-czf', '%s-%s.zip' % ( self.name, platform ), platform - ]) + ], shell=self.debug) if return_code == 0: return '%s/%s-%s.zip' % ( @@ -88,28 +99,82 @@ def archive(self, platform): return False def build(self, platform, release=False): + """Build cordova app for the project.""" cmd_params = ['cordova', 'build', platform] if release: cmd_params.append('--release') - return_code = subprocess.call(cmd_params) + return_code = subprocess.call(cmd_params, shell=self.debug) if return_code == 0: - return [os.path.join( + return os.path.join( self.path, BUILD_LOCATION[platform] ['release' if release else 'debug'] % ( - self.name - )) - ] + 'android' if platform == 'android' else self.name + ) + ) else: return False + def sign_android_apk(self, keystore, keypass, storepass, + unsigned_apk=None): + """Sign android apk for the cordova project.""" + if not unsigned_apk: + unsigned_apk = os.path.join( + self.path, + BUILD_LOCATION["android"]["release"] % 'android' + ) + + return_code = subprocess.call( + ['jarsigner', + '-verbose', + '-sigalg', + 'SHA1withRSA', + '-digestalg', + 'SHA1', + '-keystore', + keystore, + '-tsa', + 'http://tsa.starfieldtech.com', + '-storepass', + storepass, + '-keypass', + keypass, + unsigned_apk, + self.name + ], shell=self.debug) + + signed_apk_name = os.path.join( + self.path, + BUILD_LOCATION["android"]["signed"] % ( + self.name, + datetime.today().strftime("%d-%m-%y") + ) + ) + + if return_code == 0: + if os.path.exists(signed_apk_name): + os.remove(signed_apk_name) + return_code = subprocess.call( + [ + "zipalign", + "-v", + "4", + unsigned_apk, + signed_apk_name + ] + ) + if return_code == 0: + return signed_apk_name + return False + def prepare(self, platform): + """Prepare cordova app for the project.""" return_code = subprocess.call([ 'cordova', 'prepare', platform - ]) + ], shell=self.debug) if return_code == 0: return True @@ -117,9 +182,10 @@ def prepare(self, platform): return False def compile(self, platform): + """Compile the cordova source code without building the platform app.""" return_code = subprocess.call([ 'cordova', 'compile', platform - ]) + ], shell=self.debug) if return_code == 0: return True