OLD | NEW |
---|---|
1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import argparse | 5 import argparse |
6 import fnmatch | 6 import fnmatch |
7 import glob | 7 import glob |
8 import os | 8 import os |
9 import plistlib | 9 import plistlib |
10 import shutil | 10 import shutil |
11 import subprocess | 11 import subprocess |
12 import sys | 12 import sys |
13 import tempfile | 13 import tempfile |
14 | 14 |
15 | 15 |
16 class InstallationError(Exception): | |
17 """Signals a local installation error that prevents code signing.""" | |
18 | |
19 def __init__(self, fmt, *args): | |
20 super(Exception, self).__init__(fmt % args) | |
21 | |
22 | |
23 def GetProvisioningProfilesDir(): | 16 def GetProvisioningProfilesDir(): |
24 """Returns the location of the installed mobile provisioning profiles. | 17 """Returns the location of the installed mobile provisioning profiles. |
25 | 18 |
26 Returns: | 19 Returns: |
27 The path to the directory containing the installed mobile provisioning | 20 The path to the directory containing the installed mobile provisioning |
28 profiles as a string. | 21 profiles as a string. |
29 """ | 22 """ |
30 return os.path.join( | 23 return os.path.join( |
31 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') | 24 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') |
32 | 25 |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
144 | 137 |
145 def LoadDefaults(self, defaults): | 138 def LoadDefaults(self, defaults): |
146 for key, value in defaults.iteritems(): | 139 for key, value in defaults.iteritems(): |
147 if key not in self._data: | 140 if key not in self._data: |
148 self._data[key] = value | 141 self._data[key] = value |
149 | 142 |
150 def WriteTo(self, target_path): | 143 def WriteTo(self, target_path): |
151 plistlib.writePlist(self._data, target_path) | 144 plistlib.writePlist(self._data, target_path) |
152 | 145 |
153 | 146 |
154 def FindProvisioningProfile(bundle_identifier, provisioning_profile_short_name): | 147 def FindProvisioningProfile(bundle_identifier, required): |
155 """Finds mobile provisioning profile to use to sign bundle. | 148 """Finds mobile provisioning profile to use to sign bundle. |
156 | 149 |
157 Args: | 150 Args: |
158 bundle_identifier: the identifier of the bundle to sign. | 151 bundle_identifier: the identifier of the bundle to sign. |
159 provisioning_profile_short_path: optional short name of the mobile | |
160 provisioning profile file to use to sign (will still be checked | |
161 to see if it can sign bundle). | |
162 | 152 |
163 Returns: | 153 Returns: |
164 The ProvisioningProfile object that can be used to sign the Bundle | 154 The ProvisioningProfile object that can be used to sign the Bundle |
165 object or None if no matching provisioning profile was found. | 155 object or None if no matching provisioning profile was found. |
166 """ | 156 """ |
167 provisioning_profiles_dir = GetProvisioningProfilesDir() | 157 provisioning_profile_paths = glob.glob( |
168 | 158 os.path.join(GetProvisioningProfilesDir(), '*.mobileprovision')) |
169 # First check if there is a mobile provisioning profile installed with | |
170 # the requested short name. If this is the case, restrict the search to | |
171 # that mobile provisioning profile, otherwise consider all the installed | |
172 # mobile provisioning profiles. | |
173 provisioning_profile_paths = [] | |
174 if provisioning_profile_short_name: | |
175 provisioning_profile_path = os.path.join( | |
176 provisioning_profiles_dir, | |
177 provisioning_profile_short_name + '.mobileprovision') | |
178 if os.path.isfile(provisioning_profile_path): | |
179 provisioning_profile_paths.append(provisioning_profile_path) | |
180 | |
181 if not provisioning_profile_paths: | |
182 provisioning_profile_paths = glob.glob( | |
183 os.path.join(provisioning_profiles_dir, '*.mobileprovision')) | |
184 | 159 |
185 # Iterate over all installed mobile provisioning profiles and filter those | 160 # Iterate over all installed mobile provisioning profiles and filter those |
186 # that can be used to sign the bundle. | 161 # that can be used to sign the bundle. |
187 valid_provisioning_profiles = [] | 162 valid_provisioning_profiles = [] |
188 for provisioning_profile_path in provisioning_profile_paths: | 163 for provisioning_profile_path in provisioning_profile_paths: |
189 provisioning_profile = ProvisioningProfile(provisioning_profile_path) | 164 provisioning_profile = ProvisioningProfile(provisioning_profile_path) |
190 if provisioning_profile.ValidToSignBundle(bundle_identifier): | 165 if provisioning_profile.ValidToSignBundle(bundle_identifier): |
191 valid_provisioning_profiles.append(provisioning_profile) | 166 valid_provisioning_profiles.append(provisioning_profile) |
192 | 167 |
193 if not valid_provisioning_profiles: | 168 if not valid_provisioning_profiles: |
194 return None | 169 if not required: |
170 return None | |
171 sys.stderr.write( | |
172 'No mobile provisioning profile found for "%s".\n' % | |
173 bundle_identifier) | |
174 sys.exit(1) | |
Dirk Pranke
2016/08/08 22:15:36
nit: this is minor, but I'd write this as:
if req
sdefresne
2016/08/08 22:28:55
Done.
| |
195 | 175 |
196 # Select the most specific mobile provisioning profile, i.e. the one with | 176 # Select the most specific mobile provisioning profile, i.e. the one with |
197 # the longest application identifier pattern. | 177 # the longest application identifier pattern. |
198 return max( | 178 return max( |
199 valid_provisioning_profiles, | 179 valid_provisioning_profiles, |
200 key=lambda p: len(p.application_identifier_pattern)) | 180 key=lambda p: len(p.application_identifier_pattern)) |
201 | 181 |
202 | 182 |
203 def InstallFramework(framework_path, bundle, args): | 183 def CodeSignBundle(bundle_path, identity, extra_args): |
184 process = subprocess.Popen(['xcrun', 'codesign', '--force', '--sign', | |
185 identity, '--timestamp=none'] + list(extra_args) + [bundle_path], | |
186 stderr=subprocess.PIPE) | |
187 _, stderr = process.communicate() | |
188 if process.returncode: | |
189 sys.stderr.write(stderr) | |
190 sys.exit(process.returncode) | |
191 for line in stderr.splitlines(): | |
192 if line.endswith(': replacing existing signature'): | |
193 # Ignore warning about replacing existing signature as this should only | |
194 # happen when re-signing system frameworks (and then it is expected). | |
195 continue | |
196 sys.stderr.write(line) | |
197 sys.stderr.write('\n') | |
198 | |
199 | |
200 def InstallSystemFramework(framework_path, bundle_path, args): | |
204 """Install framework from |framework_path| to |bundle| and code-re-sign it.""" | 201 """Install framework from |framework_path| to |bundle| and code-re-sign it.""" |
205 installed_framework_path = os.path.join( | 202 installed_framework_path = os.path.join( |
206 bundle.path, 'Frameworks', os.path.basename(framework_path)) | 203 bundle_path, 'Frameworks', os.path.basename(framework_path)) |
207 | 204 |
208 if os.path.exists(installed_framework_path): | 205 if os.path.exists(installed_framework_path): |
209 shutil.rmtree(installed_framework_path) | 206 shutil.rmtree(installed_framework_path) |
210 | 207 |
211 shutil.copytree(framework_path, installed_framework_path) | 208 shutil.copytree(framework_path, installed_framework_path) |
212 | 209 CodeSignBundle(installed_framework_path, args.identity, |
213 framework_bundle = Bundle(installed_framework_path) | 210 ['--deep', '--preserve-metadata=identifier,entitlements']) |
214 CodeSignBundle(framework_bundle.binary_path, framework_bundle, args, True) | |
215 | 211 |
216 | 212 |
217 def GenerateEntitlements(path, provisioning_profile, bundle_identifier): | 213 def GenerateEntitlements(path, provisioning_profile, bundle_identifier): |
218 """Generates an entitlements file. | 214 """Generates an entitlements file. |
219 | 215 |
220 Args: | 216 Args: |
221 path: path to the entitlements template file | 217 path: path to the entitlements template file |
222 provisioning_profile: ProvisioningProfile object to use, may be None | 218 provisioning_profile: ProvisioningProfile object to use, may be None |
223 bundle_identifier: identifier of the bundle to sign. | 219 bundle_identifier: identifier of the bundle to sign. |
224 """ | 220 """ |
225 entitlements = Entitlements(path) | 221 entitlements = Entitlements(path) |
226 if provisioning_profile: | 222 if provisioning_profile: |
227 entitlements.LoadDefaults(provisioning_profile.entitlements) | 223 entitlements.LoadDefaults(provisioning_profile.entitlements) |
228 app_identifier_prefix = provisioning_profile.team_identifier + '.' | 224 app_identifier_prefix = provisioning_profile.team_identifier + '.' |
229 else: | 225 else: |
230 app_identifier_prefix = '*.' | 226 app_identifier_prefix = '*.' |
231 entitlements.ExpandVariables({ | 227 entitlements.ExpandVariables({ |
232 'CFBundleIdentifier': bundle_identifier, | 228 'CFBundleIdentifier': bundle_identifier, |
233 'AppIdentifierPrefix': app_identifier_prefix, | 229 'AppIdentifierPrefix': app_identifier_prefix, |
234 }) | 230 }) |
235 return entitlements | 231 return entitlements |
236 | 232 |
237 | 233 |
238 def CodeSignBundle(binary, bundle, args, preserve=False): | 234 class Action(object): |
239 """Cryptographically signs bundle. | |
240 | 235 |
241 Args: | 236 """Class implementing one action supported by the script.""" |
242 bundle: the Bundle object to sign. | |
243 args: a dictionary with configuration settings for the code signature, | |
244 need to define 'entitlements_path', 'provisioning_profile_short_name', | |
245 'deep_signature' and 'identify' keys. | |
246 """ | |
247 provisioning_profile = FindProvisioningProfile( | |
248 bundle.identifier, args.provisioning_profile_short_name) | |
249 if provisioning_profile: | |
250 provisioning_profile.Install(bundle) | |
251 else: | |
252 sys.stderr.write( | |
253 'Warning: no mobile provisioning profile found for "%s", some features ' | |
254 'may not be functioning properly.\n' % bundle.identifier) | |
255 | 237 |
256 if preserve: | 238 @classmethod |
257 process = subprocess.Popen([ | 239 def Register(cls, subparsers): |
258 'xcrun', 'codesign', '--force', | 240 parser = subparsers.add_parser(cls._GetName(), help=cls._GetHelp()) |
259 '--sign', args.identity if args.identity else '-', | 241 parser.set_defaults(func=cls._Execute) |
260 '--deep', '--preserve-metadata=identifier,entitlements', | 242 cls._Register(parser) |
261 '--timestamp=none', bundle.path], stderr=subprocess.PIPE) | 243 |
262 _, stderr = process.communicate() | 244 |
263 if process.returncode: | 245 class CodeSignBundleAction(Action): |
264 sys.stderr.write(stderr) | 246 |
265 sys.exit(process.returncode) | 247 """Class implementing the code-sign-bundle action.""" |
266 for line in stderr.splitlines(): | 248 |
267 # Ignore expected warning as we are replacing the signature on purpose. | 249 @staticmethod |
268 if not line.endswith(': replacing existing signature'): | 250 def _GetName(): |
269 sys.stderr.write(line + '\n') | 251 return 'code-sign-bundle' |
270 else: | 252 |
271 signature_file = os.path.join( | 253 @staticmethod |
272 bundle.path, '_CodeSignature', 'CodeResources') | 254 def _GetHelp(): |
255 return 'perform code signature for a bundle' | |
Dirk Pranke
2016/08/08 22:15:36
this would more idiomatically be:
name = 'code-
sdefresne
2016/08/08 22:28:55
Done.
| |
256 | |
257 @staticmethod | |
258 def _Register(parser): | |
259 parser.add_argument( | |
260 '--entitlements', '-e', dest='entitlements_path', | |
261 help='path to the entitlements file to use') | |
262 parser.add_argument( | |
263 'path', help='path to the iOS bundle to codesign') | |
264 parser.add_argument( | |
265 '--identity', '-i', required=True, | |
266 help='identity to use to codesign') | |
267 parser.add_argument( | |
268 '--binary', '-b', required=True, | |
269 help='path to the iOS bundle binary') | |
270 parser.add_argument( | |
271 '--framework', '-F', action='append', default=[], dest="frameworks", | |
272 help='install and resign system framework') | |
273 | |
274 @staticmethod | |
275 def _Execute(args): | |
276 if not args.identity: | |
277 args.identity = '-' | |
278 | |
279 bundle = Bundle(args.path) | |
280 | |
281 # Find mobile provisioning profile and embeds it into the bundle (if a code | |
282 # signing identify has been provided, fails if no valid mobile provisioning | |
283 # is found). | |
284 provisioning_profile_required = args.identity != '-' | |
285 provisioning_profile = FindProvisioningProfile( | |
286 bundle.identifier, provisioning_profile_required) | |
287 if provisioning_profile: | |
288 provisioning_profile.Install(bundle) | |
289 | |
290 # Delete existing code signature. | |
291 signature_file = os.path.join(args.path, '_CodeSignature', 'CodeResources') | |
273 if os.path.isfile(signature_file): | 292 if os.path.isfile(signature_file): |
274 os.unlink(signature_file) | 293 os.unlink(signature_file) |
275 | 294 |
295 # Install system frameworks if requested. | |
296 for framework_path in args.frameworks: | |
297 InstallSystemFramework(framework_path, args.path, args) | |
298 | |
299 # Copy main binary into bundle. | |
276 if os.path.isfile(bundle.binary_path): | 300 if os.path.isfile(bundle.binary_path): |
277 os.unlink(bundle.binary_path) | 301 os.unlink(bundle.binary_path) |
278 shutil.copy(binary, bundle.binary_path) | 302 shutil.copy(args.binary, bundle.binary_path) |
279 | 303 |
280 codesign_command = [ | 304 # Embeds entitlements into the code signature (if code signing identify has |
281 'xcrun', 'codesign', '--force', '--sign', | 305 # been provided). |
282 args.identity if args.identity else '-', | 306 codesign_extra_args = [] |
283 '--timestamp=none', bundle.path, | 307 if provisioning_profile: |
284 ] | 308 temporary_entitlements_file = tempfile.NamedTemporaryFile(suffix='.xcent') |
309 codesign_extra_args.extend( | |
310 ['--entitlements', temporary_entitlements_file.name]) | |
285 | 311 |
286 with tempfile.NamedTemporaryFile(suffix='.xcent') as temporary_file_path: | 312 entitlements = GenerateEntitlements( |
287 if provisioning_profile and args.identity: | 313 args.entitlements_path, provisioning_profile, bundle.identifier) |
288 entitlements = GenerateEntitlements( | 314 entitlements.WriteTo(temporary_entitlements_file.name) |
289 args.entitlements_path, provisioning_profile, bundle.identifier) | 315 |
290 entitlements.WriteTo(temporary_file_path.name) | 316 CodeSignBundle(bundle.path, args.identity, codesign_extra_args) |
291 codesign_command.extend(['--entitlements', temporary_file_path.name]) | |
292 subprocess.check_call(codesign_command) | |
293 | 317 |
294 | 318 |
295 def MainCodeSignBundle(args): | 319 class GenerateEntitlementsAction(Action): |
296 """Adapter to call CodeSignBundle from Main.""" | |
297 bundle = Bundle(args.path) | |
298 for framework in args.frameworks: | |
299 InstallFramework(framework, bundle, args) | |
300 CodeSignBundle(args.binary, bundle, args) | |
301 | 320 |
321 """Class implementing the generate-entitlements action.""" | |
302 | 322 |
303 def MainGenerateEntitlements(args): | 323 @staticmethod |
304 """Adapter to call GenerateEntitlements from Main.""" | 324 def _GetName(): |
305 info_plist = LoadPlistFile(args.info_plist) | 325 return 'generate-entitlements' |
306 bundle_identifier = info_plist['CFBundleIdentifier'] | |
307 provisioning_profile = FindProvisioningProfile( | |
308 bundle_identifier, args.provisioning_profile_short_name) | |
309 | 326 |
310 entitlements = GenerateEntitlements( | 327 @staticmethod |
311 args.entitlements_path, provisioning_profile, bundle_identifier) | 328 def _GetHelp(): |
312 entitlements.WriteTo(args.path) | 329 return 'generate entitlements file' |
330 | |
331 @staticmethod | |
332 def _Register(parser): | |
333 parser.add_argument( | |
334 '--entitlements', '-e', dest='entitlements_path', | |
335 help='path to the entitlements file to use') | |
336 parser.add_argument( | |
337 'path', help='path to the entitlements file to generate') | |
338 parser.add_argument( | |
339 '--info-plist', '-p', required=True, | |
340 help='path to the bundle Info.plist') | |
341 | |
342 @staticmethod | |
343 def _Execute(args): | |
344 info_plist = LoadPlistFile(args.info_plist) | |
345 bundle_identifier = info_plist['CFBundleIdentifier'] | |
346 provisioning_profile = FindProvisioningProfile(bundle_identifier, False) | |
347 entitlements = GenerateEntitlements( | |
348 args.entitlements_path, provisioning_profile, bundle_identifier) | |
349 entitlements.WriteTo(args.path) | |
313 | 350 |
314 | 351 |
315 def Main(): | 352 def Main(): |
316 parser = argparse.ArgumentParser('codesign iOS bundles') | 353 parser = argparse.ArgumentParser('codesign iOS bundles') |
317 parser.add_argument( | |
318 '--provisioning-profile', '-p', dest='provisioning_profile_short_name', | |
319 help='short name of the mobile provisioning profile to use (' | |
320 'if undefined, will autodetect the mobile provisioning ' | |
321 'to use)') | |
322 parser.add_argument( | |
323 '--entitlements', '-e', dest='entitlements_path', | |
324 help='path to the entitlements file to use') | |
325 subparsers = parser.add_subparsers() | 354 subparsers = parser.add_subparsers() |
326 | 355 |
327 code_sign_bundle_parser = subparsers.add_parser( | 356 for action in [ CodeSignBundleAction, GenerateEntitlementsAction ]: |
328 'code-sign-bundle', | 357 action.Register(subparsers) |
329 help='code sign a bundle') | |
330 code_sign_bundle_parser.add_argument( | |
331 'path', help='path to the iOS bundle to codesign') | |
332 code_sign_bundle_parser.add_argument( | |
333 '--identity', '-i', required=True, | |
334 help='identity to use to codesign') | |
335 code_sign_bundle_parser.add_argument( | |
336 '--binary', '-b', required=True, | |
337 help='path to the iOS bundle binary') | |
338 code_sign_bundle_parser.add_argument( | |
339 '--framework', '-F', action='append', default=[], dest="frameworks", | |
340 help='install and resign system framework') | |
341 code_sign_bundle_parser.set_defaults(func=MainCodeSignBundle) | |
342 | |
343 generate_entitlements_parser = subparsers.add_parser( | |
344 'generate-entitlements', | |
345 help='generate entitlements file') | |
346 generate_entitlements_parser.add_argument( | |
347 'path', help='path to the entitlements file to generate') | |
348 generate_entitlements_parser.add_argument( | |
349 '--info-plist', '-p', required=True, | |
350 help='path to the bundle Info.plist') | |
351 generate_entitlements_parser.set_defaults( | |
352 func=MainGenerateEntitlements) | |
353 | 358 |
354 args = parser.parse_args() | 359 args = parser.parse_args() |
355 args.func(args) | 360 args.func(args) |
356 | 361 |
357 | 362 |
358 if __name__ == '__main__': | 363 if __name__ == '__main__': |
359 sys.exit(Main()) | 364 sys.exit(Main()) |
OLD | NEW |