aboutsummaryrefslogtreecommitdiff
path: root/contrib/macdeploy/gen-sdk
blob: 6efaaccb8e16b0d05994b9411aa29d20133b3079 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env python3
import argparse
import plistlib
import pathlib
import sys
import tarfile
import gzip
import os
import contextlib

# monkey-patch Python 3.8 and older to fix wrong TAR header handling
# see https://github.com/bitcoin/bitcoin/pull/24534
# and https://github.com/python/cpython/pull/18080 for more info
if sys.version_info < (3, 9):
    _old_create_header = tarfile.TarInfo._create_header
    def _create_header(info, format, encoding, errors):
        buf = _old_create_header(info, format, encoding, errors)
        # replace devmajor/devminor with binary zeroes
        buf = buf[:329] + bytes(16) + buf[345:]
        # recompute checksum
        chksum = tarfile.calc_chksums(buf)[0]
        buf = buf[:-364] + bytes("%06o\0" % chksum, "ascii") + buf[-357:]
        return buf
    tarfile.TarInfo._create_header = staticmethod(_create_header)

@contextlib.contextmanager
def cd(path):
    """Context manager that restores PWD even if an exception was raised."""
    old_pwd = os.getcwd()
    os.chdir(str(path))
    try:
        yield
    finally:
        os.chdir(old_pwd)

def run():
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawTextHelpFormatter)

    parser.add_argument('xcode_app', metavar='XCODEAPP', nargs=1)
    parser.add_argument("-o", metavar='OUTSDKTGZ', nargs=1, dest='out_sdktgz', required=False)

    args = parser.parse_args()

    xcode_app = pathlib.Path(args.xcode_app[0]).resolve()
    assert xcode_app.is_dir(), "The supplied Xcode.app path '{}' either does not exist or is not a directory".format(xcode_app)

    xcode_app_plist = xcode_app.joinpath("Contents/version.plist")
    with xcode_app_plist.open('rb') as fp:
        pl = plistlib.load(fp)
        xcode_version = pl['CFBundleShortVersionString']
        xcode_build_id = pl['ProductBuildVersion']
    print("Found Xcode (version: {xcode_version}, build id: {xcode_build_id})".format(xcode_version=xcode_version, xcode_build_id=xcode_build_id))

    sdk_dir = xcode_app.joinpath("Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk")
    sdk_plist = sdk_dir.joinpath("System/Library/CoreServices/SystemVersion.plist")
    with sdk_plist.open('rb') as fp:
        pl = plistlib.load(fp)
        sdk_version = pl['ProductVersion']
        sdk_build_id = pl['ProductBuildVersion']
    print("Found MacOSX SDK (version: {sdk_version}, build id: {sdk_build_id})".format(sdk_version=sdk_version, sdk_build_id=sdk_build_id))

    out_name = "Xcode-{xcode_version}-{xcode_build_id}-extracted-SDK-with-libcxx-headers".format(xcode_version=xcode_version, xcode_build_id=xcode_build_id)

    xcode_libcxx_dir = xcode_app.joinpath("Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1")
    assert xcode_libcxx_dir.is_dir()

    if args.out_sdktgz:
        out_sdktgz_path = pathlib.Path(args.out_sdktgz_path)
    else:
        # Construct our own out_sdktgz if not specified on the command line
        out_sdktgz_path = pathlib.Path("./{}.tar.gz".format(out_name))

    def tarfp_add_with_base_change(tarfp, dir_to_add, alt_base_dir):
        """Add all files in dir_to_add to tarfp, but prepent MEMBERPREFIX to the files'
        names

        e.g. if the only file under /root/bazdir is /root/bazdir/qux, invoking:

            tarfp_add_with_base_change(tarfp, "foo/bar", "/root/bazdir")

        would result in the following members being added to tarfp:

            foo/bar/             -> corresponding to /root/bazdir
            foo/bar/qux          -> corresponding to /root/bazdir/qux

        """
        def change_tarinfo_base(tarinfo):
            if tarinfo.name and tarinfo.name.startswith("./"):
                tarinfo.name = str(pathlib.Path(alt_base_dir, tarinfo.name))
            if tarinfo.linkname and tarinfo.linkname.startswith("./"):
                tarinfo.linkname = str(pathlib.Path(alt_base_dir, tarinfo.linkname))
            # make metadata deterministic
            tarinfo.mtime = 0
            tarinfo.uid, tarinfo.uname =  0, ''
            tarinfo.gid, tarinfo.gname = 0, ''
            # don't use isdir() as there are also executable files present
            tarinfo.mode = 0o0755 if tarinfo.mode & 0o0100 else 0o0644
            return tarinfo
        with cd(dir_to_add):
            # recursion already adds entries in sorted order
            tarfp.add(".", recursive=True, filter=change_tarinfo_base)

    print("Creating output .tar.gz file...")
    with out_sdktgz_path.open("wb") as fp:
        with gzip.GzipFile(fileobj=fp, mode='wb', compresslevel=9, mtime=0) as gzf:
            with tarfile.open(mode="w", fileobj=gzf, format=tarfile.GNU_FORMAT) as tarfp:
                print("Adding MacOSX SDK {} files...".format(sdk_version))
                tarfp_add_with_base_change(tarfp, sdk_dir, out_name)
                print("Adding libc++ headers...")
                tarfp_add_with_base_change(tarfp, xcode_libcxx_dir, "{}/usr/include/c++/v1".format(out_name))
    print("Done! Find the resulting gzipped tarball at:")
    print(out_sdktgz_path.resolve())

if __name__ == '__main__':
    run()