Raw File
debug_sign_tool.py
#!/usr/bin/env python

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""
Sign Android packages using an Android debug keystore, creating the
keystore if it does not exist.

This and |zip| can be combined to replace the Android |apkbuilder|
tool, which was deprecated in SDK r22.

Exits with code 0 if creating the keystore and every signing succeeded,
or with code 1 if any creation or signing failed.
"""

from argparse import ArgumentParser
import errno
import logging
import os
import subprocess
import sys


log = logging.getLogger(os.path.basename(__file__))
log.setLevel(logging.INFO)
sh = logging.StreamHandler(stream=sys.stdout)
sh.setFormatter(logging.Formatter('%(name)s: %(message)s'))
log.addHandler(sh)


class DebugKeystore:
    """
    A thin abstraction on top of an Android debug key store.
    """
    def __init__(self, keystore):
        self._keystore = os.path.abspath(os.path.expanduser(keystore))
        self._alias = 'androiddebugkey'
        self.verbose = False
        self.keytool = 'keytool'
        self.jarsigner = 'jarsigner'

    @property
    def keystore(self):
        return self._keystore

    @property
    def alias(self):
        return self._alias

    def _check(self, args):
        try:
            if self.verbose:
                subprocess.check_call(args)
            else:
                subprocess.check_output(args)
        except OSError as ex:
            if ex.errno != errno.ENOENT:
                raise
            raise Exception("Could not find executable '%s'" % args[0])

    def keystore_contains_alias(self):
        args = [ self.keytool,
                 '-list',
                 '-keystore', self.keystore,
                 '-storepass', 'android',
                 '-alias', self.alias,
               ]
        if self.verbose:
            args.append('-v')
        contains = True
        try:
            self._check(args)
        except subprocess.CalledProcessError as e:
            contains = False
        if self.verbose:
            log.info('Keystore %s %s alias %s' %
                     (self.keystore,
                      'contains' if contains else 'does not contain',
                      self.alias))
        return contains

    def create_alias_in_keystore(self):
        try:
            path = os.path.dirname(self.keystore)
            os.makedirs(path)
        except OSError as exception:
            if exception.errno != errno.EEXIST:
                raise

        args = [ self.keytool,
                 '-genkeypair',
                 '-keystore', self.keystore,
                 '-storepass', 'android',
                 '-alias', self.alias,
                 '-keypass', 'android',
                 '-dname', 'CN=Android Debug,O=Android,C=US',
                 '-keyalg', 'RSA',
                 '-validity', '365',
               ]
        if self.verbose:
            args.append('-v')
        self._check(args)
        if self.verbose:
            log.info('Created alias %s in keystore %s' %
                     (self.alias, self.keystore))

    def sign(self, apk):
        if not self.keystore_contains_alias():
            self.create_alias_in_keystore()

        args = [ self.jarsigner,
                 '-digestalg', 'SHA1',
                 '-sigalg', 'MD5withRSA',
                 '-keystore', self.keystore,
                 '-storepass', 'android',
                 apk,
                 self.alias,
               ]
        if self.verbose:
            args.append('-verbose')
        self._check(args)
        if self.verbose:
            log.info('Signed %s with alias %s from keystore %s' %
                     (apk, self.alias, self.keystore))


def parse_args(argv):
    parser = ArgumentParser(description='Sign Android packages using an Android debug keystore.')
    parser.add_argument('apks', nargs='+',
                        metavar='APK',
                        help='Android packages to be signed')
    parser.add_argument('-v', '--verbose',
                        dest='verbose',
                        default=False,
                        action='store_true',
                        help='verbose output')
    parser.add_argument('--keytool',
                        metavar='PATH',
                        default='keytool',
                        help='path to Java keytool')
    parser.add_argument('--jarsigner',
                        metavar='PATH',
                        default='jarsigner',
                        help='path to Java jarsigner')
    parser.add_argument('--keystore',
                        metavar='PATH',
                        default='~/.android/debug.keystore',
                        help='path to keystore (default: ~/.android/debug.keystore)')
    parser.add_argument('-f', '--force-create-keystore',
                        dest='force',
                        default=False,
                        action='store_true',
                        help='force creating keystore')
    return parser.parse_args(argv)


def main():
    args = parse_args(sys.argv[1:])

    keystore = DebugKeystore(args.keystore)
    keystore.verbose = args.verbose
    keystore.keytool = args.keytool
    keystore.jarsigner = args.jarsigner

    if args.force:
        try:
            keystore.create_alias_in_keystore()
        except subprocess.CalledProcessError as e:
            log.error('Failed to force-create alias %s in keystore %s' %
                      (keystore.alias, keystore.keystore))
            log.error(e)
            return 1

    for apk in args.apks:
        try:
            keystore.sign(apk)
        except subprocess.CalledProcessError as e:
            log.error('Failed to sign %s', apk)
            log.error(e)
            return 1

    return 0

if __name__ == '__main__':
    sys.exit(main())
back to top