Chromium Code Reviews| 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 |