1#!/usr/bin/env python 2# Copyright (c) 2012 Google Inc. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Utility functions to perform Xcode-style build steps. 7 8These functions are executed via gyp-mac-tool when using the Makefile generator. 9""" 10 11from __future__ import print_function 12 13import fcntl 14import fnmatch 15import glob 16import json 17import os 18import plistlib 19import re 20import shutil 21import struct 22import subprocess 23import sys 24import tempfile 25 26 27def main(args): 28 executor = MacTool() 29 exit_code = executor.Dispatch(args) 30 if exit_code is not None: 31 sys.exit(exit_code) 32 33 34class MacTool(object): 35 """This class performs all the Mac tooling steps. The methods can either be 36 executed directly, or dispatched from an argument list.""" 37 38 def Dispatch(self, args): 39 """Dispatches a string command to a method.""" 40 if len(args) < 1: 41 raise Exception("Not enough arguments") 42 43 method = "Exec%s" % self._CommandifyName(args[0]) 44 return getattr(self, method)(*args[1:]) 45 46 def _CommandifyName(self, name_string): 47 """Transforms a tool name like copy-info-plist to CopyInfoPlist""" 48 return name_string.title().replace('-', '') 49 50 def ExecCopyBundleResource(self, source, dest, convert_to_binary): 51 """Copies a resource file to the bundle/Resources directory, performing any 52 necessary compilation on each resource.""" 53 convert_to_binary = convert_to_binary == 'True' 54 extension = os.path.splitext(source)[1].lower() 55 if os.path.isdir(source): 56 # Copy tree. 57 # TODO(thakis): This copies file attributes like mtime, while the 58 # single-file branch below doesn't. This should probably be changed to 59 # be consistent with the single-file branch. 60 if os.path.exists(dest): 61 shutil.rmtree(dest) 62 shutil.copytree(source, dest) 63 elif extension == '.xib': 64 return self._CopyXIBFile(source, dest) 65 elif extension == '.storyboard': 66 return self._CopyXIBFile(source, dest) 67 elif extension == '.strings' and not convert_to_binary: 68 self._CopyStringsFile(source, dest) 69 else: 70 if os.path.exists(dest): 71 os.unlink(dest) 72 shutil.copy(source, dest) 73 74 if convert_to_binary and extension in ('.plist', '.strings'): 75 self._ConvertToBinary(dest) 76 77 def _CopyXIBFile(self, source, dest): 78 """Compiles a XIB file with ibtool into a binary plist in the bundle.""" 79 80 # ibtool sometimes crashes with relative paths. See crbug.com/314728. 81 base = os.path.dirname(os.path.realpath(__file__)) 82 if os.path.relpath(source): 83 source = os.path.join(base, source) 84 if os.path.relpath(dest): 85 dest = os.path.join(base, dest) 86 87 args = ['xcrun', 'ibtool', '--errors', '--warnings', '--notices'] 88 89 if os.environ['XCODE_VERSION_ACTUAL'] > '0700': 90 args.extend(['--auto-activate-custom-fonts']) 91 if 'IPHONEOS_DEPLOYMENT_TARGET' in os.environ: 92 args.extend([ 93 '--target-device', 'iphone', '--target-device', 'ipad', 94 '--minimum-deployment-target', 95 os.environ['IPHONEOS_DEPLOYMENT_TARGET'], 96 ]) 97 else: 98 args.extend([ 99 '--target-device', 'mac', 100 '--minimum-deployment-target', 101 os.environ['MACOSX_DEPLOYMENT_TARGET'], 102 ]) 103 104 args.extend(['--output-format', 'human-readable-text', '--compile', dest, 105 source]) 106 107 ibtool_section_re = re.compile(r'/\*.*\*/') 108 ibtool_re = re.compile(r'.*note:.*is clipping its content') 109 try: 110 stdout = subprocess.check_output(args) 111 except subprocess.CalledProcessError as e: 112 print(e.output) 113 raise 114 current_section_header = None 115 for line in stdout.splitlines(): 116 line_decoded = line.decode('utf-8') 117 if ibtool_section_re.match(line_decoded): 118 current_section_header = line_decoded 119 elif not ibtool_re.match(line_decoded): 120 if current_section_header: 121 print(current_section_header) 122 current_section_header = None 123 print(line_decoded) 124 return 0 125 126 def _ConvertToBinary(self, dest): 127 subprocess.check_call([ 128 'xcrun', 'plutil', '-convert', 'binary1', '-o', dest, dest]) 129 130 def _CopyStringsFile(self, source, dest): 131 """Copies a .strings file using iconv to reconvert the input into UTF-16.""" 132 input_code = self._DetectInputEncoding(source) or "UTF-8" 133 134 # Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call 135 # CFPropertyListCreateFromXMLData() behind the scenes; at least it prints 136 # CFPropertyListCreateFromXMLData(): Old-style plist parser: missing 137 # semicolon in dictionary. 138 # on invalid files. Do the same kind of validation. 139 import CoreFoundation 140 s = open(source, 'rb').read() 141 d = CoreFoundation.CFDataCreate(None, s, len(s)) 142 _, error = CoreFoundation.CFPropertyListCreateFromXMLData(None, d, 0, None) 143 if error: 144 return 145 146 fp = open(dest, 'wb') 147 fp.write(s.decode(input_code).encode('UTF-16')) 148 fp.close() 149 150 def _DetectInputEncoding(self, file_name): 151 """Reads the first few bytes from file_name and tries to guess the text 152 encoding. Returns None as a guess if it can't detect it.""" 153 fp = open(file_name, 'rb') 154 try: 155 header = fp.read(3) 156 except: 157 fp.close() 158 return None 159 fp.close() 160 if header.startswith(b"\xFE\xFF"): 161 return "UTF-16" 162 elif header.startswith(b"\xFF\xFE"): 163 return "UTF-16" 164 elif header.startswith(b"\xEF\xBB\xBF"): 165 return "UTF-8" 166 else: 167 return None 168 169 def ExecCopyInfoPlist(self, source, dest, convert_to_binary, *keys): 170 """Copies the |source| Info.plist to the destination directory |dest|.""" 171 # Read the source Info.plist into memory. 172 fd = open(source, 'r') 173 lines = fd.read() 174 fd.close() 175 176 # Insert synthesized key/value pairs (e.g. BuildMachineOSBuild). 177 plist = plistlib.readPlistFromString(lines) 178 if keys: 179 plist.update(json.loads(keys[0])) 180 lines = plistlib.writePlistToString(plist) 181 182 # Go through all the environment variables and replace them as variables in 183 # the file. 184 IDENT_RE = re.compile(r'[_/\s]') 185 for key in os.environ: 186 if key.startswith('_'): 187 continue 188 evar = '${%s}' % key 189 evalue = os.environ[key] 190 lines = lines.replace(evar, evalue) 191 192 # Xcode supports various suffices on environment variables, which are 193 # all undocumented. :rfc1034identifier is used in the standard project 194 # template these days, and :identifier was used earlier. They are used to 195 # convert non-url characters into things that look like valid urls -- 196 # except that the replacement character for :identifier, '_' isn't valid 197 # in a URL either -- oops, hence :rfc1034identifier was born. 198 evar = '${%s:identifier}' % key 199 evalue = IDENT_RE.sub('_', os.environ[key]) 200 lines = lines.replace(evar, evalue) 201 202 evar = '${%s:rfc1034identifier}' % key 203 evalue = IDENT_RE.sub('-', os.environ[key]) 204 lines = lines.replace(evar, evalue) 205 206 # Remove any keys with values that haven't been replaced. 207 lines = lines.split('\n') 208 for i in range(len(lines)): 209 if lines[i].strip().startswith("<string>${"): 210 lines[i] = None 211 lines[i - 1] = None 212 lines = '\n'.join(filter(lambda x: x is not None, lines)) 213 214 # Write out the file with variables replaced. 215 fd = open(dest, 'w') 216 fd.write(lines) 217 fd.close() 218 219 # Now write out PkgInfo file now that the Info.plist file has been 220 # "compiled". 221 self._WritePkgInfo(dest) 222 223 if convert_to_binary == 'True': 224 self._ConvertToBinary(dest) 225 226 def _WritePkgInfo(self, info_plist): 227 """This writes the PkgInfo file from the data stored in Info.plist.""" 228 plist = plistlib.readPlist(info_plist) 229 if not plist: 230 return 231 232 # Only create PkgInfo for executable types. 233 package_type = plist['CFBundlePackageType'] 234 if package_type != 'APPL': 235 return 236 237 # The format of PkgInfo is eight characters, representing the bundle type 238 # and bundle signature, each four characters. If that is missing, four 239 # '?' characters are used instead. 240 signature_code = plist.get('CFBundleSignature', '????') 241 if len(signature_code) != 4: # Wrong length resets everything, too. 242 signature_code = '?' * 4 243 244 dest = os.path.join(os.path.dirname(info_plist), 'PkgInfo') 245 fp = open(dest, 'w') 246 fp.write('%s%s' % (package_type, signature_code)) 247 fp.close() 248 249 def ExecFlock(self, lockfile, *cmd_list): 250 """Emulates the most basic behavior of Linux's flock(1).""" 251 # Rely on exception handling to report errors. 252 fd = os.open(lockfile, os.O_RDONLY|os.O_NOCTTY|os.O_CREAT, 0o666) 253 fcntl.flock(fd, fcntl.LOCK_EX) 254 return subprocess.call(cmd_list) 255 256 def ExecFilterLibtool(self, *cmd_list): 257 """Calls libtool and filters out '/path/to/libtool: file: foo.o has no 258 symbols'.""" 259 libtool_re = re.compile(r'^.*libtool: (?:for architecture: \S* )?' 260 r'file: .* has no symbols$') 261 libtool_re5 = re.compile( 262 r'^.*libtool: warning for library: ' + 263 r'.* the table of contents is empty ' + 264 r'\(no object file members in the library define global symbols\)$') 265 env = os.environ.copy() 266 # Ref: 267 # http://www.opensource.apple.com/source/cctools/cctools-809/misc/libtool.c 268 # The problem with this flag is that it resets the file mtime on the file to 269 # epoch=0, e.g. 1970-1-1 or 1969-12-31 depending on timezone. 270 env['ZERO_AR_DATE'] = '1' 271 libtoolout = subprocess.Popen(cmd_list, stderr=subprocess.PIPE, env=env) 272 _, err = libtoolout.communicate() 273 for line in err.splitlines(): 274 line_decoded = line.decode('utf-8') 275 if not libtool_re.match(line_decoded) and not libtool_re5.match(line_decoded): 276 print(line_decoded, file=sys.stderr) 277 # Unconditionally touch the output .a file on the command line if present 278 # and the command succeeded. A bit hacky. 279 if not libtoolout.returncode: 280 for i in range(len(cmd_list) - 1): 281 if cmd_list[i] == "-o" and cmd_list[i+1].endswith('.a'): 282 os.utime(cmd_list[i+1], None) 283 break 284 return libtoolout.returncode 285 286 def ExecPackageIosFramework(self, framework): 287 # Find the name of the binary based on the part before the ".framework". 288 binary = os.path.basename(framework).split('.')[0] 289 module_path = os.path.join(framework, 'Modules'); 290 if not os.path.exists(module_path): 291 os.mkdir(module_path) 292 module_template = 'framework module %s {\n' \ 293 ' umbrella header "%s.h"\n' \ 294 '\n' \ 295 ' export *\n' \ 296 ' module * { export * }\n' \ 297 '}\n' % (binary, binary) 298 299 module_file = open(os.path.join(module_path, 'module.modulemap'), "w") 300 module_file.write(module_template) 301 module_file.close() 302 303 def ExecPackageFramework(self, framework, version): 304 """Takes a path to Something.framework and the Current version of that and 305 sets up all the symlinks.""" 306 # Find the name of the binary based on the part before the ".framework". 307 binary = os.path.basename(framework).split('.')[0] 308 309 CURRENT = 'Current' 310 RESOURCES = 'Resources' 311 VERSIONS = 'Versions' 312 313 if not os.path.exists(os.path.join(framework, VERSIONS, version, binary)): 314 # Binary-less frameworks don't seem to contain symlinks (see e.g. 315 # chromium's out/Debug/org.chromium.Chromium.manifest/ bundle). 316 return 317 318 # Move into the framework directory to set the symlinks correctly. 319 pwd = os.getcwd() 320 os.chdir(framework) 321 322 # Set up the Current version. 323 self._Relink(version, os.path.join(VERSIONS, CURRENT)) 324 325 # Set up the root symlinks. 326 self._Relink(os.path.join(VERSIONS, CURRENT, binary), binary) 327 self._Relink(os.path.join(VERSIONS, CURRENT, RESOURCES), RESOURCES) 328 329 # Back to where we were before! 330 os.chdir(pwd) 331 332 def _Relink(self, dest, link): 333 """Creates a symlink to |dest| named |link|. If |link| already exists, 334 it is overwritten.""" 335 if os.path.lexists(link): 336 os.remove(link) 337 os.symlink(dest, link) 338 339 def ExecCompileIosFrameworkHeaderMap(self, out, framework, *all_headers): 340 framework_name = os.path.basename(framework).split('.')[0] 341 all_headers = map(os.path.abspath, all_headers) 342 filelist = {} 343 for header in all_headers: 344 filename = os.path.basename(header) 345 filelist[filename] = header 346 filelist[os.path.join(framework_name, filename)] = header 347 WriteHmap(out, filelist) 348 349 def ExecCopyIosFrameworkHeaders(self, framework, *copy_headers): 350 header_path = os.path.join(framework, 'Headers'); 351 if not os.path.exists(header_path): 352 os.makedirs(header_path) 353 for header in copy_headers: 354 shutil.copy(header, os.path.join(header_path, os.path.basename(header))) 355 356 def ExecCompileXcassets(self, keys, *inputs): 357 """Compiles multiple .xcassets files into a single .car file. 358 359 This invokes 'actool' to compile all the inputs .xcassets files. The 360 |keys| arguments is a json-encoded dictionary of extra arguments to 361 pass to 'actool' when the asset catalogs contains an application icon 362 or a launch image. 363 364 Note that 'actool' does not create the Assets.car file if the asset 365 catalogs does not contains imageset. 366 """ 367 command_line = [ 368 'xcrun', 'actool', '--output-format', 'human-readable-text', 369 '--compress-pngs', '--notices', '--warnings', '--errors', 370 ] 371 is_iphone_target = 'IPHONEOS_DEPLOYMENT_TARGET' in os.environ 372 if is_iphone_target: 373 platform = os.environ['CONFIGURATION'].split('-')[-1] 374 if platform not in ('iphoneos', 'iphonesimulator'): 375 platform = 'iphonesimulator' 376 command_line.extend([ 377 '--platform', platform, '--target-device', 'iphone', 378 '--target-device', 'ipad', '--minimum-deployment-target', 379 os.environ['IPHONEOS_DEPLOYMENT_TARGET'], '--compile', 380 os.path.abspath(os.environ['CONTENTS_FOLDER_PATH']), 381 ]) 382 else: 383 command_line.extend([ 384 '--platform', 'macosx', '--target-device', 'mac', 385 '--minimum-deployment-target', os.environ['MACOSX_DEPLOYMENT_TARGET'], 386 '--compile', 387 os.path.abspath(os.environ['UNLOCALIZED_RESOURCES_FOLDER_PATH']), 388 ]) 389 if keys: 390 keys = json.loads(keys) 391 for key, value in keys.items(): 392 arg_name = '--' + key 393 if isinstance(value, bool): 394 if value: 395 command_line.append(arg_name) 396 elif isinstance(value, list): 397 for v in value: 398 command_line.append(arg_name) 399 command_line.append(str(v)) 400 else: 401 command_line.append(arg_name) 402 command_line.append(str(value)) 403 # Note: actool crashes if inputs path are relative, so use os.path.abspath 404 # to get absolute path name for inputs. 405 command_line.extend(map(os.path.abspath, inputs)) 406 subprocess.check_call(command_line) 407 408 def ExecMergeInfoPlist(self, output, *inputs): 409 """Merge multiple .plist files into a single .plist file.""" 410 merged_plist = {} 411 for path in inputs: 412 plist = self._LoadPlistMaybeBinary(path) 413 self._MergePlist(merged_plist, plist) 414 plistlib.writePlist(merged_plist, output) 415 416 def ExecCodeSignBundle(self, key, entitlements, provisioning, path, preserve): 417 """Code sign a bundle. 418 419 This function tries to code sign an iOS bundle, following the same 420 algorithm as Xcode: 421 1. pick the provisioning profile that best match the bundle identifier, 422 and copy it into the bundle as embedded.mobileprovision, 423 2. copy Entitlements.plist from user or SDK next to the bundle, 424 3. code sign the bundle. 425 """ 426 substitutions, overrides = self._InstallProvisioningProfile( 427 provisioning, self._GetCFBundleIdentifier()) 428 entitlements_path = self._InstallEntitlements( 429 entitlements, substitutions, overrides) 430 431 args = ['codesign', '--force', '--sign', key] 432 if preserve == 'True': 433 args.extend(['--deep', '--preserve-metadata=identifier,entitlements']) 434 else: 435 args.extend(['--entitlements', entitlements_path]) 436 args.extend(['--timestamp=none', path]) 437 subprocess.check_call(args) 438 439 def _InstallProvisioningProfile(self, profile, bundle_identifier): 440 """Installs embedded.mobileprovision into the bundle. 441 442 Args: 443 profile: string, optional, short name of the .mobileprovision file 444 to use, if empty or the file is missing, the best file installed 445 will be used 446 bundle_identifier: string, value of CFBundleIdentifier from Info.plist 447 448 Returns: 449 A tuple containing two dictionary: variables substitutions and values 450 to overrides when generating the entitlements file. 451 """ 452 source_path, provisioning_data, team_id = self._FindProvisioningProfile( 453 profile, bundle_identifier) 454 target_path = os.path.join( 455 os.environ['BUILT_PRODUCTS_DIR'], 456 os.environ['CONTENTS_FOLDER_PATH'], 457 'embedded.mobileprovision') 458 shutil.copy2(source_path, target_path) 459 substitutions = self._GetSubstitutions(bundle_identifier, team_id + '.') 460 return substitutions, provisioning_data['Entitlements'] 461 462 def _FindProvisioningProfile(self, profile, bundle_identifier): 463 """Finds the .mobileprovision file to use for signing the bundle. 464 465 Checks all the installed provisioning profiles (or if the user specified 466 the PROVISIONING_PROFILE variable, only consult it) and select the most 467 specific that correspond to the bundle identifier. 468 469 Args: 470 profile: string, optional, short name of the .mobileprovision file 471 to use, if empty or the file is missing, the best file installed 472 will be used 473 bundle_identifier: string, value of CFBundleIdentifier from Info.plist 474 475 Returns: 476 A tuple of the path to the selected provisioning profile, the data of 477 the embedded plist in the provisioning profile and the team identifier 478 to use for code signing. 479 480 Raises: 481 SystemExit: if no .mobileprovision can be used to sign the bundle. 482 """ 483 profiles_dir = os.path.join( 484 os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles') 485 if not os.path.isdir(profiles_dir): 486 print(( 487 'cannot find mobile provisioning for %s' % bundle_identifier), 488 file=sys.stderr) 489 sys.exit(1) 490 provisioning_profiles = None 491 if profile: 492 profile_path = os.path.join(profiles_dir, profile + '.mobileprovision') 493 if os.path.exists(profile_path): 494 provisioning_profiles = [profile_path] 495 if not provisioning_profiles: 496 provisioning_profiles = glob.glob( 497 os.path.join(profiles_dir, '*.mobileprovision')) 498 valid_provisioning_profiles = {} 499 for profile_path in provisioning_profiles: 500 profile_data = self._LoadProvisioningProfile(profile_path) 501 app_id_pattern = profile_data.get( 502 'Entitlements', {}).get('application-identifier', '') 503 for team_identifier in profile_data.get('TeamIdentifier', []): 504 app_id = '%s.%s' % (team_identifier, bundle_identifier) 505 if fnmatch.fnmatch(app_id, app_id_pattern): 506 valid_provisioning_profiles[app_id_pattern] = ( 507 profile_path, profile_data, team_identifier) 508 if not valid_provisioning_profiles: 509 print(( 510 'cannot find mobile provisioning for %s' % bundle_identifier), 511 file=sys.stderr) 512 sys.exit(1) 513 # If the user has multiple provisioning profiles installed that can be 514 # used for ${bundle_identifier}, pick the most specific one (ie. the 515 # provisioning profile whose pattern is the longest). 516 selected_key = max(valid_provisioning_profiles, key=lambda v: len(v)) 517 return valid_provisioning_profiles[selected_key] 518 519 def _LoadProvisioningProfile(self, profile_path): 520 """Extracts the plist embedded in a provisioning profile. 521 522 Args: 523 profile_path: string, path to the .mobileprovision file 524 525 Returns: 526 Content of the plist embedded in the provisioning profile as a dictionary. 527 """ 528 with tempfile.NamedTemporaryFile() as temp: 529 subprocess.check_call([ 530 'security', 'cms', '-D', '-i', profile_path, '-o', temp.name]) 531 return self._LoadPlistMaybeBinary(temp.name) 532 533 def _MergePlist(self, merged_plist, plist): 534 """Merge |plist| into |merged_plist|.""" 535 for key, value in plist.items(): 536 if isinstance(value, dict): 537 merged_value = merged_plist.get(key, {}) 538 if isinstance(merged_value, dict): 539 self._MergePlist(merged_value, value) 540 merged_plist[key] = merged_value 541 else: 542 merged_plist[key] = value 543 else: 544 merged_plist[key] = value 545 546 def _LoadPlistMaybeBinary(self, plist_path): 547 """Loads into a memory a plist possibly encoded in binary format. 548 549 This is a wrapper around plistlib.readPlist that tries to convert the 550 plist to the XML format if it can't be parsed (assuming that it is in 551 the binary format). 552 553 Args: 554 plist_path: string, path to a plist file, in XML or binary format 555 556 Returns: 557 Content of the plist as a dictionary. 558 """ 559 try: 560 # First, try to read the file using plistlib that only supports XML, 561 # and if an exception is raised, convert a temporary copy to XML and 562 # load that copy. 563 return plistlib.readPlist(plist_path) 564 except: 565 pass 566 with tempfile.NamedTemporaryFile() as temp: 567 shutil.copy2(plist_path, temp.name) 568 subprocess.check_call(['plutil', '-convert', 'xml1', temp.name]) 569 return plistlib.readPlist(temp.name) 570 571 def _GetSubstitutions(self, bundle_identifier, app_identifier_prefix): 572 """Constructs a dictionary of variable substitutions for Entitlements.plist. 573 574 Args: 575 bundle_identifier: string, value of CFBundleIdentifier from Info.plist 576 app_identifier_prefix: string, value for AppIdentifierPrefix 577 578 Returns: 579 Dictionary of substitutions to apply when generating Entitlements.plist. 580 """ 581 return { 582 'CFBundleIdentifier': bundle_identifier, 583 'AppIdentifierPrefix': app_identifier_prefix, 584 } 585 586 def _GetCFBundleIdentifier(self): 587 """Extracts CFBundleIdentifier value from Info.plist in the bundle. 588 589 Returns: 590 Value of CFBundleIdentifier in the Info.plist located in the bundle. 591 """ 592 info_plist_path = os.path.join( 593 os.environ['TARGET_BUILD_DIR'], 594 os.environ['INFOPLIST_PATH']) 595 info_plist_data = self._LoadPlistMaybeBinary(info_plist_path) 596 return info_plist_data['CFBundleIdentifier'] 597 598 def _InstallEntitlements(self, entitlements, substitutions, overrides): 599 """Generates and install the ${BundleName}.xcent entitlements file. 600 601 Expands variables "$(variable)" pattern in the source entitlements file, 602 add extra entitlements defined in the .mobileprovision file and the copy 603 the generated plist to "${BundlePath}.xcent". 604 605 Args: 606 entitlements: string, optional, path to the Entitlements.plist template 607 to use, defaults to "${SDKROOT}/Entitlements.plist" 608 substitutions: dictionary, variable substitutions 609 overrides: dictionary, values to add to the entitlements 610 611 Returns: 612 Path to the generated entitlements file. 613 """ 614 source_path = entitlements 615 target_path = os.path.join( 616 os.environ['BUILT_PRODUCTS_DIR'], 617 os.environ['PRODUCT_NAME'] + '.xcent') 618 if not source_path: 619 source_path = os.path.join( 620 os.environ['SDKROOT'], 621 'Entitlements.plist') 622 shutil.copy2(source_path, target_path) 623 data = self._LoadPlistMaybeBinary(target_path) 624 data = self._ExpandVariables(data, substitutions) 625 if overrides: 626 for key in overrides: 627 if key not in data: 628 data[key] = overrides[key] 629 plistlib.writePlist(data, target_path) 630 return target_path 631 632 def _ExpandVariables(self, data, substitutions): 633 """Expands variables "$(variable)" in data. 634 635 Args: 636 data: object, can be either string, list or dictionary 637 substitutions: dictionary, variable substitutions to perform 638 639 Returns: 640 Copy of data where each references to "$(variable)" has been replaced 641 by the corresponding value found in substitutions, or left intact if 642 the key was not found. 643 """ 644 if isinstance(data, str): 645 for key, value in substitutions.items(): 646 data = data.replace('$(%s)' % key, value) 647 return data 648 if isinstance(data, list): 649 return [self._ExpandVariables(v, substitutions) for v in data] 650 if isinstance(data, dict): 651 return {k: self._ExpandVariables(data[k], substitutions) for k in data} 652 return data 653 654def NextGreaterPowerOf2(x): 655 return 2**(x).bit_length() 656 657def WriteHmap(output_name, filelist): 658 """Generates a header map based on |filelist|. 659 660 Per Mark Mentovai: 661 A header map is structured essentially as a hash table, keyed by names used 662 in #includes, and providing pathnames to the actual files. 663 664 The implementation below and the comment above comes from inspecting: 665 http://www.opensource.apple.com/source/distcc/distcc-2503/distcc_dist/include_server/headermap.py?txt 666 while also looking at the implementation in clang in: 667 https://llvm.org/svn/llvm-project/cfe/trunk/lib/Lex/HeaderMap.cpp 668 """ 669 magic = 1751998832 670 version = 1 671 _reserved = 0 672 count = len(filelist) 673 capacity = NextGreaterPowerOf2(count) 674 strings_offset = 24 + (12 * capacity) 675 max_value_length = len(max(filelist.items(), key=lambda t: len(t[1]))[1]) 676 677 out = open(output_name, "wb") 678 out.write(struct.pack('<LHHLLLL', magic, version, _reserved, strings_offset, 679 count, capacity, max_value_length)) 680 681 # Create empty hashmap buckets. 682 buckets = [None] * capacity 683 for file, path in filelist.items(): 684 key = 0 685 for c in file: 686 key += ord(c.lower()) * 13 687 688 # Fill next empty bucket. 689 while buckets[key & capacity - 1] is not None: 690 key = key + 1 691 buckets[key & capacity - 1] = (file, path) 692 693 next_offset = 1 694 for bucket in buckets: 695 if bucket is None: 696 out.write(struct.pack('<LLL', 0, 0, 0)) 697 else: 698 (file, path) = bucket 699 key_offset = next_offset 700 prefix_offset = key_offset + len(file) + 1 701 suffix_offset = prefix_offset + len(os.path.dirname(path) + os.sep) + 1 702 next_offset = suffix_offset + len(os.path.basename(path)) + 1 703 out.write(struct.pack('<LLL', key_offset, prefix_offset, suffix_offset)) 704 705 # Pad byte since next offset starts at 1. 706 out.write(struct.pack('<x')) 707 708 for bucket in buckets: 709 if bucket is not None: 710 (file, path) = bucket 711 out.write(struct.pack('<%ds' % len(file), file)) 712 out.write(struct.pack('<s', '\0')) 713 base = os.path.dirname(path) + os.sep 714 out.write(struct.pack('<%ds' % len(base), base)) 715 out.write(struct.pack('<s', '\0')) 716 path = os.path.basename(path) 717 out.write(struct.pack('<%ds' % len(path), path)) 718 out.write(struct.pack('<s', '\0')) 719 720if __name__ == '__main__': 721 sys.exit(main(sys.argv[1:])) 722