gen-sdk.py
1 #!/usr/bin/env python3 2 import argparse 3 import plistlib 4 import pathlib 5 import tarfile 6 import os 7 import contextlib 8 9 @contextlib.contextmanager 10 def cd(path): 11 """Context manager that restores PWD even if an exception was raised.""" 12 old_pwd = os.getcwd() 13 os.chdir(str(path)) 14 try: 15 yield 16 finally: 17 os.chdir(old_pwd) 18 19 def run(): 20 parser = argparse.ArgumentParser( 21 description=__doc__, formatter_class=argparse.RawTextHelpFormatter) 22 23 parser.add_argument('xcode_app', metavar='XCODEAPP', type=pathlib.Path) 24 parser.add_argument("-o", metavar='OUTSDKTAR', dest='out_sdkt', type=pathlib.Path, required=False) 25 26 args = parser.parse_args() 27 28 xcode_app = args.xcode_app.resolve() 29 assert xcode_app.is_dir(), "The supplied Xcode.app path '{}' either does not exist or is not a directory".format(xcode_app) 30 31 xcode_app_plist = xcode_app.joinpath("Contents/version.plist") 32 with xcode_app_plist.open('rb') as fp: 33 pl = plistlib.load(fp) 34 xcode_version = pl['CFBundleShortVersionString'] 35 xcode_build_id = pl['ProductBuildVersion'] 36 print("Found Xcode (version: {xcode_version}, build id: {xcode_build_id})".format(xcode_version=xcode_version, xcode_build_id=xcode_build_id)) 37 38 sdk_dir = xcode_app.joinpath("Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk") 39 sdk_plist = sdk_dir.joinpath("System/Library/CoreServices/SystemVersion.plist") 40 with sdk_plist.open('rb') as fp: 41 pl = plistlib.load(fp) 42 sdk_version = pl['ProductVersion'] 43 sdk_build_id = pl['ProductBuildVersion'] 44 print("Found MacOSX SDK (version: {sdk_version}, build id: {sdk_build_id})".format(sdk_version=sdk_version, sdk_build_id=sdk_build_id)) 45 46 out_name = "Xcode-{xcode_version}-{xcode_build_id}-extracted-SDK-with-libcxx-headers".format(xcode_version=xcode_version, xcode_build_id=xcode_build_id) 47 48 out_sdkt_path = args.out_sdkt or pathlib.Path("./{}.tar".format(out_name)) 49 50 def tarfp_add_with_base_change(tarfp, dir_to_add, alt_base_dir): 51 """Add all files in dir_to_add to tarfp, but prepend alt_base_dir to the files' 52 names 53 54 e.g. if the only file under /root/bazdir is /root/bazdir/qux, invoking: 55 56 tarfp_add_with_base_change(tarfp, "foo/bar", "/root/bazdir") 57 58 would result in the following members being added to tarfp: 59 60 foo/bar/ -> corresponding to /root/bazdir 61 foo/bar/qux -> corresponding to /root/bazdir/qux 62 63 """ 64 def change_tarinfo_base(tarinfo): 65 if tarinfo.name and tarinfo.name.endswith((".swiftmodule", ".modulemap")): 66 return None 67 if tarinfo.name and tarinfo.name.startswith("./"): 68 tarinfo.name = str(pathlib.Path(alt_base_dir, tarinfo.name)) 69 if tarinfo.linkname and tarinfo.linkname.startswith("./"): 70 tarinfo.linkname = str(pathlib.Path(alt_base_dir, tarinfo.linkname)) 71 # make metadata deterministic 72 tarinfo.mtime = 0 73 tarinfo.uid, tarinfo.uname = 0, '' 74 tarinfo.gid, tarinfo.gname = 0, '' 75 # don't use isdir() as there are also executable files present 76 tarinfo.mode = 0o0755 if tarinfo.mode & 0o0100 else 0o0644 77 return tarinfo 78 with cd(dir_to_add): 79 # recursion already adds entries in sorted order 80 tarfp.add("./usr/include", recursive=True, filter=change_tarinfo_base) 81 tarfp.add("./usr/lib", recursive=True, filter=change_tarinfo_base) 82 tarfp.add("./System/Library/Frameworks", recursive=True, filter=change_tarinfo_base) 83 84 print("Creating output .tar file...") 85 with out_sdkt_path.open("wb") as fp: 86 with tarfile.open(mode="w", fileobj=fp, format=tarfile.PAX_FORMAT) as tarfp: 87 print("Adding MacOSX SDK {} files...".format(sdk_version)) 88 tarfp_add_with_base_change(tarfp, sdk_dir, out_name) 89 print("Done! Find the resulting tarball at:") 90 print(out_sdkt_path.resolve()) 91 92 if __name__ == '__main__': 93 run()