/
sign_apk.py
177 lines (145 loc) · 7.22 KB
/
sign_apk.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# coding=utf-8
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
unicode_literals, with_statement)
import logging
import os
import subprocess
from pants.backend.android.android_config_util import AndroidConfigUtil
from pants.backend.android.keystore.keystore_resolver import KeystoreResolver
from pants.backend.android.targets.android_binary import AndroidBinary
from pants.backend.core.tasks.task import Task
from pants.base.build_environment import get_pants_configdir
from pants.base.exceptions import TaskError
from pants.base.workunit import WorkUnit
from pants.java.distribution.distribution import Distribution
from pants.util.dirutil import safe_mkdir
logger = logging.getLogger(__name__)
class SignApkTask(Task):
"""Sign Android packages with keystores using the jarsigner tool."""
_DEFAULT_KEYSTORE_CONFIG = 'android/keystore/default_config.ini'
@classmethod
def register_options(cls, register):
super(SignApkTask, cls).register_options(register)
register('--keystore-config-location',
help='Location of the .ini file containing keystore definitions.')
@classmethod
def prepare(cls, options, round_manager):
super(SignApkTask, cls).prepare(options, round_manager)
round_manager.require_data('apk')
@classmethod
def is_signtarget(cls, target):
return isinstance(target, AndroidBinary)
@classmethod
def product_types(cls):
return ['release_apk']
@classmethod
def setup_default_config(cls, path):
"""Create the default keystore config file for Android targets.
:param string path: Full path for the created default config file.
"""
# TODO(mateor): Hook into pants global setup instead of relying on building an Android target.
try:
AndroidConfigUtil.setup_keystore_config(path)
except AndroidConfigUtil.AndroidConfigError as e:
raise TaskError('Failed to setup default keystore config: {0}'.format(e))
@classmethod
def signed_package_name(cls, target, build_type):
"""Get package name with 'build_type', a string KeyResolver mandates is in (debug, release)."""
return '{0}.{1}.signed.apk'.format(target.app_name, build_type)
def __init__(self, *args, **kwargs):
super(SignApkTask, self).__init__(*args, **kwargs)
self._config_file = self.get_options().keystore_config_location
self._distdir = self.get_options().pants_distdir
self._configdir = get_pants_configdir()
self._dist = None
@property
def config_file(self):
if not self._config_file:
raise TaskError('The "[sign]: keystore_config_location" option must declare the location '
'of an .ini file holding keystore definitions.')
return os.path.expanduser(self._config_file)
@property
def default_config_location(self):
"""Return the path where pants creates the default keystore config file.
This location will hold the well-known definition of the debug keystore installed with the SDK.
"""
return os.path.join(self._configdir, self._DEFAULT_KEYSTORE_CONFIG)
@property
def distribution(self):
if self._dist is None:
# Currently no Java 8 for Android. I considered max=1.7.0_50. See comment in _render_args().
self._dist = Distribution.cached(minimum_version='1.6.0_00',
maximum_version='1.7.0_99',
jdk=True)
return self._dist
def _render_args(self, target, key, unsigned_apk, outdir):
"""Create arg list for the jarsigner process.
:param AndroidBinary target: Target to be signed.
:param string unsigned_apk: Location of the apk product from the AaptBuilder task.
:param Keystore key: Keystore instance with which to sign the android target.
:param string outdir: output directory for the signed apk.
"""
# After JDK 1.7.0_51, jars without timestamps print a warning. This causes jars to stop working
# past their validity date. But Android purposefully passes 30 years validity. More research
# is needed before passing a -tsa flag indiscriminately.
# http://bugs.java.com/view_bug.do?bug_id=8023338
args = [self.distribution.binary('jarsigner')]
# These first two params are required flags for JDK 7+
args.extend(['-sigalg', 'SHA1withRSA'])
args.extend(['-digestalg', 'SHA1'])
args.extend(['-keystore', key.keystore_location])
args.extend(['-storepass', key.keystore_password])
args.extend(['-keypass', key.key_password])
args.extend(['-signedjar',
os.path.join(outdir, self.signed_package_name(target, key.build_type))])
args.append(unsigned_apk)
args.append(key.keystore_alias)
logger.debug('Executing: {0}'.format(' '.join(args)))
return args
def execute(self):
# One time setup of the default keystore config file.
if not os.path.isfile(self.default_config_location):
self.setup_default_config(self.default_config_location)
targets = self.context.targets(self.is_signtarget)
with self.invalidated(targets) as invalidation_check:
invalid_targets = []
for vt in invalidation_check.invalid_vts:
invalid_targets.extend(vt.targets)
for target in invalid_targets:
def get_products_path(target):
"""Get path of target's unsigned apks as created by AaptBuilder."""
unsigned_apks = self.context.products.get('apk')
packages = unsigned_apks.get(target)
if packages:
for tgts, products in packages.items():
for prod in products:
yield os.path.join(tgts, prod)
packages = list(get_products_path(target))
for unsigned_apk in packages:
keystores = KeystoreResolver.resolve(self.config_file)
for key in keystores:
outdir = self.sign_apk_out(target, keystores[key].build_type)
safe_mkdir(outdir)
args = self._render_args(target, keystores[key], unsigned_apk, outdir)
with self.context.new_workunit(name='sign_apk',
labels=[WorkUnit.MULTITOOL]) as workunit:
returncode = subprocess.call(args, stdout=workunit.output('stdout'),
stderr=workunit.output('stderr'))
if returncode:
raise TaskError('The SignApk jarsigner process exited non-zero: {0}'
.format(returncode))
for target in targets:
release_path = self.sign_apk_out(target, 'release')
release_apk = self.signed_package_name(target, 'release')
if os.path.isfile(os.path.join(release_path, release_apk)):
self.context.products.get('release_apk').add(target, release_path).append(release_apk)
def sign_apk_out(self, target, build_type):
"""Compute the outdir for a target."""
if build_type == 'release':
# If it is a release build, it goes to the workdir for zipalign to operate upon.
return os.path.join(self.workdir, target.name, build_type)
elif build_type == 'debug':
# Debug builds have completed all needed tasks so they can go straight to dist.
return os.path.join(self._distdir, target.name)