codekingpro/portable-devtools
114k
1#!/usr/bin/env python3
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
11import fcntl
12import fnmatch
13import glob
14import json
15import os
16import plistlib
17import re
18import shutil
19import struct
20import subprocess
21import sys
22import tempfile
23
24
25def main(args):
26 executor = MacTool()
27 if (exit_code := executor.Dispatch(args)) is not None:
28 sys.exit(exit_code)
29
30
31class MacTool:
32 """This class performs all the Mac tooling steps. The methods can either be
33 executed directly, or dispatched from an argument list."""
34
35 def Dispatch(self, args):
36 """Dispatches a string command to a method."""
37 if len(args) < 1:
38 raise Exception("Not enough arguments")
39
40 method = "Exec%s" % self._CommandifyName(args[0])
41 return getattr(self, method)(*args[1:])
42
43 def _CommandifyName(self, name_string):
44 """Transforms a tool name like copy-info-plist to CopyInfoPlist"""
45 return name_string.title().replace("-", "")
46
47 def ExecCopyBundleResource(self, source, dest, convert_to_binary):
48 """Copies a resource file to the bundle/Resources directory, performing any
49 necessary compilation on each resource."""
50 convert_to_binary = convert_to_binary == "True"
51 extension = os.path.splitext(source)[1].lower()
52 if os.path.isdir(source):
53 # Copy tree.
54 # TODO(thakis): This copies file attributes like mtime, while the
55 # single-file branch below doesn't. This should probably be changed to
56 # be consistent with the single-file branch.
57 if os.path.exists(dest):
58 shutil.rmtree(dest)
59 shutil.copytree(source, dest)
60 elif extension in {".xib", ".storyboard"}:
61 return self._CopyXIBFile(source, dest)
62 elif extension == ".strings" and not convert_to_binary:
63 self._CopyStringsFile(source, dest)
64 else:
65 if os.path.exists(dest):
66 os.unlink(dest)
67 shutil.copy(source, dest)
68
69 if convert_to_binary and extension in {".plist", ".strings"}:
70 self._ConvertToBinary(dest)
71
72 def _CopyXIBFile(self, source, dest):
73 """Compiles a XIB file with ibtool into a binary plist in the bundle."""
74
75 # ibtool sometimes crashes with relative paths. See crbug.com/314728.
76 base = os.path.dirname(os.path.realpath(__file__))
77 if os.path.relpath(source):
78 source = os.path.join(base, source)
79 if os.path.relpath(dest):
80 dest = os.path.join(base, dest)
81
82 args = ["xcrun", "ibtool", "--errors", "--warnings", "--notices"]
83
84 if os.environ["XCODE_VERSION_ACTUAL"] > "0700":
85 args.extend(["--auto-activate-custom-fonts"])
86 if "IPHONEOS_DEPLOYMENT_TARGET" in os.environ:
87 args.extend(
88 [
89 "--target-device",
90 "iphone",
91 "--target-device",
92 "ipad",
93 "--minimum-deployment-target",
94 os.environ["IPHONEOS_DEPLOYMENT_TARGET"],
95 ]
96 )
97 else:
98 args.extend(
99 [
100 "--target-device",
101 "mac",
102 "--minimum-deployment-target",
103 os.environ["MACOSX_DEPLOYMENT_TARGET"],
104 ]
105 )
106
107 args.extend(
108 ["--output-format", "human-readable-text", "--compile", dest, source]
109 )
110
111 ibtool_section_re = re.compile(r"/\*.*\*/")
112 ibtool_re = re.compile(r".*note:.*is clipping its content")
113 try:
114 stdout = subprocess.check_output(args)
115 except subprocess.CalledProcessError as e:
116 print(e.output)
117 raise
118 current_section_header = None
119 for line in stdout.splitlines():
120 if ibtool_section_re.match(line):
121 current_section_header = line
122 elif not ibtool_re.match(line):
123 if current_section_header:
124 print(current_section_header)
125 current_section_header = None
126 print(line)
127 return 0
128
129 def _ConvertToBinary(self, dest):
130 subprocess.check_call(
131 ["xcrun", "plutil", "-convert", "binary1", "-o", dest, dest]
132 )
133
134 def _CopyStringsFile(self, source, dest):
135 """Copies a .strings file using iconv to reconvert the input into UTF-16."""
136 input_code = self._DetectInputEncoding(source) or "UTF-8"
137
138 # Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call
139 # CFPropertyListCreateFromXMLData() behind the scenes; at least it prints
140 # CFPropertyListCreateFromXMLData(): Old-style plist parser: missing
141 # semicolon in dictionary.
142 # on invalid files. Do the same kind of validation.
143 import CoreFoundation # noqa: PLC0415
144
145 with open(source, "rb") as in_file:
146 s = in_file.read()
147 d = CoreFoundation.CFDataCreate(None, s, len(s))
148 _, error = CoreFoundation.CFPropertyListCreateFromXMLData(None, d, 0, None)
149 if error:
150 return
151
152 with open(dest, "wb") as fp:
153 fp.write(s.decode(input_code).encode("UTF-16"))
154
155 def _DetectInputEncoding(self, file_name):
156 """Reads the first few bytes from file_name and tries to guess the text
157 encoding. Returns None as a guess if it can't detect it."""
158 with open(file_name, "rb") as fp:
159 try:
160 header = fp.read(3)
161 except Exception:
162 return None
163 if header.startswith((b"\xfe\xff", b"\xff\xfe")):
164 return "UTF-16"
165 elif header.startswith(b"\xef\xbb\xbf"):
166 return "UTF-8"
167 else:
168 return None
169
170 def ExecCopyInfoPlist(self, source, dest, convert_to_binary, *keys):
171 """Copies the |source| Info.plist to the destination directory |dest|."""
172 # Read the source Info.plist into memory.
173 with open(source) as fd:
174 lines = fd.read()
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(lines, 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(lines, evar, evalue)
201
202 evar = "${%s:rfc1034identifier}" % key
203 evalue = IDENT_RE.sub("-", os.environ[key])
204 lines = lines.replace(lines, evar, evalue)
205
206 # Remove any keys with values that haven't been replaced.
207 lines = lines.splitlines()
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(line for line in lines if line is not None)
213
214 # Write out the file with variables replaced.
215 with open(dest, "w") as fd:
216 fd.write(lines)
217
218 # Now write out PkgInfo file now that the Info.plist file has been
219 # "compiled".
220 self._WritePkgInfo(dest)
221
222 if convert_to_binary == "True":
223 self._ConvertToBinary(dest)
224
225 def _WritePkgInfo(self, info_plist):
226 """This writes the PkgInfo file from the data stored in Info.plist."""
227 plist = plistlib.readPlist(info_plist)
228 if not plist:
229 return
230
231 # Only create PkgInfo for executable types.
232 package_type = plist["CFBundlePackageType"]
233 if package_type != "APPL":
234 return
235
236 # The format of PkgInfo is eight characters, representing the bundle type
237 # and bundle signature, each four characters. If that is missing, four
238 # '?' characters are used instead.
239 signature_code = plist.get("CFBundleSignature", "????")
240 if len(signature_code) != 4: # Wrong length resets everything, too.
241 signature_code = "?" * 4
242
243 dest = os.path.join(os.path.dirname(info_plist), "PkgInfo")
244 with open(dest, "w") as fp:
245 fp.write(f"{package_type}{signature_code}")
246
247 def ExecFlock(self, lockfile, *cmd_list):
248 """Emulates the most basic behavior of Linux's flock(1)."""
249 # Rely on exception handling to report errors.
250 fd = os.open(lockfile, os.O_RDONLY | os.O_NOCTTY | os.O_CREAT, 0o666)
251 fcntl.flock(fd, fcntl.LOCK_EX)
252 return subprocess.call(cmd_list)
253
254 def ExecFilterLibtool(self, *cmd_list):
255 """Calls libtool and filters out '/path/to/libtool: file: foo.o has no
256 symbols'."""
257 libtool_re = re.compile(
258 r"^.*libtool: (?:for architecture: \S* )?file: .* has no symbols$"
259 )
260 libtool_re5 = re.compile(
261 r"^.*libtool: warning for library: "
262 + r".* the table of contents is empty "
263 + r"\(no object file members in the library define global symbols\)$"
264 )
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()[1].decode("utf-8")
273 for line in err.splitlines():
274 if not libtool_re.match(line) and not libtool_re5.match(line):
275 print(line, file=sys.stderr)
276 # Unconditionally touch the output .a file on the command line if present
277 # and the command succeeded. A bit hacky.
278 if not libtoolout.returncode:
279 for i in range(len(cmd_list) - 1):
280 if cmd_list[i] == "-o" and cmd_list[i + 1].endswith(".a"):
281 os.utime(cmd_list[i + 1], None)
282 break
283 return libtoolout.returncode
284
285 def ExecPackageIosFramework(self, framework):
286 # Find the name of the binary based on the part before the ".framework".
287 binary = os.path.basename(framework).split(".")[0]
288 module_path = os.path.join(framework, "Modules")
289 if not os.path.exists(module_path):
290 os.mkdir(module_path)
291 module_template = (
292 "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
300 with open(os.path.join(module_path, "module.modulemap"), "w") as module_file:
301 module_file.write(module_template)
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 = [os.path.abspath(header) for header in 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",
369 "actool",
370 "--output-format",
371 "human-readable-text",
372 "--compress-pngs",
373 "--notices",
374 "--warnings",
375 "--errors",
376 ]
377 is_iphone_target = "IPHONEOS_DEPLOYMENT_TARGET" in os.environ
378 if is_iphone_target:
379 platform = os.environ["CONFIGURATION"].split("-")[-1]
380 if platform not in ("iphoneos", "iphonesimulator"):
381 platform = "iphonesimulator"
382 command_line.extend(
383 [
384 "--platform",
385 platform,
386 "--target-device",
387 "iphone",
388 "--target-device",
389 "ipad",
390 "--minimum-deployment-target",
391 os.environ["IPHONEOS_DEPLOYMENT_TARGET"],
392 "--compile",
393 os.path.abspath(os.environ["CONTENTS_FOLDER_PATH"]),
394 ]
395 )
396 else:
397 command_line.extend(
398 [
399 "--platform",
400 "macosx",
401 "--target-device",
402 "mac",
403 "--minimum-deployment-target",
404 os.environ["MACOSX_DEPLOYMENT_TARGET"],
405 "--compile",
406 os.path.abspath(os.environ["UNLOCALIZED_RESOURCES_FOLDER_PATH"]),
407 ]
408 )
409 if keys:
410 keys = json.loads(keys)
411 for key, value in keys.items():
412 arg_name = "--" + key
413 if isinstance(value, bool):
414 if value:
415 command_line.append(arg_name)
416 elif isinstance(value, list):
417 for v in value:
418 command_line.append(arg_name)
419 command_line.append(str(v))
420 else:
421 command_line.append(arg_name)
422 command_line.append(str(value))
423 # Note: actool crashes if inputs path are relative, so use os.path.abspath
424 # to get absolute path name for inputs.
425 command_line.extend(map(os.path.abspath, inputs))
426 subprocess.check_call(command_line)
427
428 def ExecMergeInfoPlist(self, output, *inputs):
429 """Merge multiple .plist files into a single .plist file."""
430 merged_plist = {}
431 for path in inputs:
432 plist = self._LoadPlistMaybeBinary(path)
433 self._MergePlist(merged_plist, plist)
434 plistlib.writePlist(merged_plist, output)
435
436 def ExecCodeSignBundle(self, key, entitlements, provisioning, path, preserve):
437 """Code sign a bundle.
438
439 This function tries to code sign an iOS bundle, following the same
440 algorithm as Xcode:
441 1. pick the provisioning profile that best match the bundle identifier,
442 and copy it into the bundle as embedded.mobileprovision,
443 2. copy Entitlements.plist from user or SDK next to the bundle,
444 3. code sign the bundle.
445 """
446 substitutions, overrides = self._InstallProvisioningProfile(
447 provisioning, self._GetCFBundleIdentifier()
448 )
449 entitlements_path = self._InstallEntitlements(
450 entitlements, substitutions, overrides
451 )
452
453 args = ["codesign", "--force", "--sign", key]
454 if preserve == "True":
455 args.extend(["--deep", "--preserve-metadata=identifier,entitlements"])
456 else:
457 args.extend(["--entitlements", entitlements_path])
458 args.extend(["--timestamp=none", path])
459 subprocess.check_call(args)
460
461 def _InstallProvisioningProfile(self, profile, bundle_identifier):
462 """Installs embedded.mobileprovision into the bundle.
463
464 Args:
465 profile: string, optional, short name of the .mobileprovision file
466 to use, if empty or the file is missing, the best file installed
467 will be used
468 bundle_identifier: string, value of CFBundleIdentifier from Info.plist
469
470 Returns:
471 A tuple containing two dictionary: variables substitutions and values
472 to overrides when generating the entitlements file.
473 """
474 source_path, provisioning_data, team_id = self._FindProvisioningProfile(
475 profile, bundle_identifier
476 )
477 target_path = os.path.join(
478 os.environ["BUILT_PRODUCTS_DIR"],
479 os.environ["CONTENTS_FOLDER_PATH"],
480 "embedded.mobileprovision",
481 )
482 shutil.copy2(source_path, target_path)
483 substitutions = self._GetSubstitutions(bundle_identifier, team_id + ".")
484 return substitutions, provisioning_data["Entitlements"]
485
486 def _FindProvisioningProfile(self, profile, bundle_identifier):
487 """Finds the .mobileprovision file to use for signing the bundle.
488
489 Checks all the installed provisioning profiles (or if the user specified
490 the PROVISIONING_PROFILE variable, only consult it) and select the most
491 specific that correspond to the bundle identifier.
492
493 Args:
494 profile: string, optional, short name of the .mobileprovision file
495 to use, if empty or the file is missing, the best file installed
496 will be used
497 bundle_identifier: string, value of CFBundleIdentifier from Info.plist
498
499 Returns:
500 A tuple of the path to the selected provisioning profile, the data of
501 the embedded plist in the provisioning profile and the team identifier
502 to use for code signing.
503
504 Raises:
505 SystemExit: if no .mobileprovision can be used to sign the bundle.
506 """
507 profiles_dir = os.path.join(
508 os.environ["HOME"], "Library", "MobileDevice", "Provisioning Profiles"
509 )
510 if not os.path.isdir(profiles_dir):
511 print(
512 "cannot find mobile provisioning for %s" % (bundle_identifier),
513 file=sys.stderr,
514 )
515 sys.exit(1)
516 provisioning_profiles = None
517 if profile:
518 profile_path = os.path.join(profiles_dir, profile + ".mobileprovision")
519 if os.path.exists(profile_path):
520 provisioning_profiles = [profile_path]
521 if not provisioning_profiles:
522 provisioning_profiles = glob.glob(
523 os.path.join(profiles_dir, "*.mobileprovision")
524 )
525 valid_provisioning_profiles = {}
526 for profile_path in provisioning_profiles:
527 profile_data = self._LoadProvisioningProfile(profile_path)
528 app_id_pattern = profile_data.get("Entitlements", {}).get(
529 "application-identifier", ""
530 )
531 for team_identifier in profile_data.get("TeamIdentifier", []):
532 app_id = f"{team_identifier}.{bundle_identifier}"
533 if fnmatch.fnmatch(app_id, app_id_pattern):
534 valid_provisioning_profiles[app_id_pattern] = (
535 profile_path,
536 profile_data,
537 team_identifier,
538 )
539 if not valid_provisioning_profiles:
540 print(
541 "cannot find mobile provisioning for %s" % (bundle_identifier),
542 file=sys.stderr,
543 )
544 sys.exit(1)
545 # If the user has multiple provisioning profiles installed that can be
546 # used for ${bundle_identifier}, pick the most specific one (ie. the
547 # provisioning profile whose pattern is the longest).
548 selected_key = max(valid_provisioning_profiles, key=lambda v: len(v))
549 return valid_provisioning_profiles[selected_key]
550
551 def _LoadProvisioningProfile(self, profile_path):
552 """Extracts the plist embedded in a provisioning profile.
553
554 Args:
555 profile_path: string, path to the .mobileprovision file
556
557 Returns:
558 Content of the plist embedded in the provisioning profile as a dictionary.
559 """
560 with tempfile.NamedTemporaryFile() as temp:
561 subprocess.check_call(
562 ["security", "cms", "-D", "-i", profile_path, "-o", temp.name]
563 )
564 return self._LoadPlistMaybeBinary(temp.name)
565
566 def _MergePlist(self, merged_plist, plist):
567 """Merge |plist| into |merged_plist|."""
568 for key, value in plist.items():
569 if isinstance(value, dict):
570 merged_value = merged_plist.get(key, {})
571 if isinstance(merged_value, dict):
572 self._MergePlist(merged_value, value)
573 merged_plist[key] = merged_value
574 else:
575 merged_plist[key] = value
576 else:
577 merged_plist[key] = value
578
579 def _LoadPlistMaybeBinary(self, plist_path):
580 """Loads into a memory a plist possibly encoded in binary format.
581
582 This is a wrapper around plistlib.readPlist that tries to convert the
583 plist to the XML format if it can't be parsed (assuming that it is in
584 the binary format).
585
586 Args:
587 plist_path: string, path to a plist file, in XML or binary format
588
589 Returns:
590 Content of the plist as a dictionary.
591 """
592 try:
593 # First, try to read the file using plistlib that only supports XML,
594 # and if an exception is raised, convert a temporary copy to XML and
595 # load that copy.
596 return plistlib.readPlist(plist_path)
597 except Exception:
598 pass
599 with tempfile.NamedTemporaryFile() as temp:
600 shutil.copy2(plist_path, temp.name)
601 subprocess.check_call(["plutil", "-convert", "xml1", temp.name])
602 return plistlib.readPlist(temp.name)
603
604 def _GetSubstitutions(self, bundle_identifier, app_identifier_prefix):
605 """Constructs a dictionary of variable substitutions for Entitlements.plist.
606
607 Args:
608 bundle_identifier: string, value of CFBundleIdentifier from Info.plist
609 app_identifier_prefix: string, value for AppIdentifierPrefix
610
611 Returns:
612 Dictionary of substitutions to apply when generating Entitlements.plist.
613 """
614 return {
615 "CFBundleIdentifier": bundle_identifier,
616 "AppIdentifierPrefix": app_identifier_prefix,
617 }
618
619 def _GetCFBundleIdentifier(self):
620 """Extracts CFBundleIdentifier value from Info.plist in the bundle.
621
622 Returns:
623 Value of CFBundleIdentifier in the Info.plist located in the bundle.
624 """
625 info_plist_path = os.path.join(
626 os.environ["TARGET_BUILD_DIR"], os.environ["INFOPLIST_PATH"]
627 )
628 info_plist_data = self._LoadPlistMaybeBinary(info_plist_path)
629 return info_plist_data["CFBundleIdentifier"]
630
631 def _InstallEntitlements(self, entitlements, substitutions, overrides):
632 """Generates and install the ${BundleName}.xcent entitlements file.
633
634 Expands variables "$(variable)" pattern in the source entitlements file,
635 add extra entitlements defined in the .mobileprovision file and the copy
636 the generated plist to "${BundlePath}.xcent".
637
638 Args:
639 entitlements: string, optional, path to the Entitlements.plist template
640 to use, defaults to "${SDKROOT}/Entitlements.plist"
641 substitutions: dictionary, variable substitutions
642 overrides: dictionary, values to add to the entitlements
643
644 Returns:
645 Path to the generated entitlements file.
646 """
647 source_path = entitlements
648 target_path = os.path.join(
649 os.environ["BUILT_PRODUCTS_DIR"], os.environ["PRODUCT_NAME"] + ".xcent"
650 )
651 if not source_path:
652 source_path = os.path.join(os.environ["SDKROOT"], "Entitlements.plist")
653 shutil.copy2(source_path, target_path)
654 data = self._LoadPlistMaybeBinary(target_path)
655 data = self._ExpandVariables(data, substitutions)
656 if overrides:
657 for key in overrides:
658 if key not in data:
659 data[key] = overrides[key]
660 plistlib.writePlist(data, target_path)
661 return target_path
662
663 def _ExpandVariables(self, data, substitutions):
664 """Expands variables "$(variable)" in data.
665
666 Args:
667 data: object, can be either string, list or dictionary
668 substitutions: dictionary, variable substitutions to perform
669
670 Returns:
671 Copy of data where each references to "$(variable)" has been replaced
672 by the corresponding value found in substitutions, or left intact if
673 the key was not found.
674 """
675 if isinstance(data, str):
676 for key, value in substitutions.items():
677 data = data.replace("$(%s)" % key, value)
678 return data
679 if isinstance(data, list):
680 return [self._ExpandVariables(v, substitutions) for v in data]
681 if isinstance(data, dict):
682 return {k: self._ExpandVariables(data[k], substitutions) for k in data}
683 return data
684
685
686def NextGreaterPowerOf2(x):
687 return 2 ** (x).bit_length()
688
689
690def WriteHmap(output_name, filelist):
691 """Generates a header map based on |filelist|.
692
693 Per Mark Mentovai:
694 A header map is structured essentially as a hash table, keyed by names used
695 in #includes, and providing pathnames to the actual files.
696
697 The implementation below and the comment above comes from inspecting:
698 http://www.opensource.apple.com/source/distcc/distcc-2503/distcc_dist/include_server/headermap.py?txt
699 while also looking at the implementation in clang in:
700 https://llvm.org/svn/llvm-project/cfe/trunk/lib/Lex/HeaderMap.cpp
701 """
702 magic = 1751998832
703 version = 1
704 _reserved = 0
705 count = len(filelist)
706 capacity = NextGreaterPowerOf2(count)
707 strings_offset = 24 + (12 * capacity)
708 max_value_length = max(len(value) for value in filelist.values())
709
710 out = open(output_name, "wb")
711 out.write(
712 struct.pack(
713 "<LHHLLLL",
714 magic,
715 version,
716 _reserved,
717 strings_offset,
718 count,
719 capacity,
720 max_value_length,
721 )
722 )
723
724 # Create empty hashmap buckets.
725 buckets = [None] * capacity
726 for file, path in filelist.items():
727 key = 0
728 for c in file:
729 key += ord(c.lower()) * 13
730
731 # Fill next empty bucket.
732 while buckets[key & capacity - 1] is not None:
733 key = key + 1
734 buckets[key & capacity - 1] = (file, path)
735
736 next_offset = 1
737 for bucket in buckets:
738 if bucket is None:
739 out.write(struct.pack("<LLL", 0, 0, 0))
740 else:
741 (file, path) = bucket
742 key_offset = next_offset
743 prefix_offset = key_offset + len(file) + 1
744 suffix_offset = prefix_offset + len(os.path.dirname(path) + os.sep) + 1
745 next_offset = suffix_offset + len(os.path.basename(path)) + 1
746 out.write(struct.pack("<LLL", key_offset, prefix_offset, suffix_offset))
747
748 # Pad byte since next offset starts at 1.
749 out.write(struct.pack("<x"))
750
751 for bucket in buckets:
752 if bucket is not None:
753 (file, path) = bucket
754 out.write(struct.pack("<%ds" % len(file), file))
755 out.write(struct.pack("<s", "\0"))
756 base = os.path.dirname(path) + os.sep
757 out.write(struct.pack("<%ds" % len(base), base))
758 out.write(struct.pack("<s", "\0"))
759 path = os.path.basename(path)
760 out.write(struct.pack("<%ds" % len(path), path))
761 out.write(struct.pack("<s", "\0"))
762
763
764if __name__ == "__main__":
765 sys.exit(main(sys.argv[1:]))
766 