Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
mac_tool.py766 linesDownload Raw Back to gyp
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 
codekingpro/portable-devtools · Team Ai