117 lines
5 KiB
Text
117 lines
5 KiB
Text
|
#!/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/moneyrocket/moneyrocket/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()
|