#!/usr/bin/env python3 import argparse import os import subprocess import sys import tempfile INFER_IMPORT_DIR = \ os.path.dirname(os.path.realpath(__file__)) + '/sdk-module-lists' INFER_IMPORT_PATH = INFER_IMPORT_DIR + '/infer-imports.py' def printerr(message): print(message, file=sys.stderr) def fatal_error(message): printerr(message) sys.exit(1) def escapeCmdArg(arg): if '"' in arg or ' ' in arg: return '"%s"' % arg.replace('"', '\\"') else: return arg def check_call(cmd, cwd=None, env=os.environ, verbose=False, output=None): if verbose: print(' '.join([escapeCmdArg(arg) for arg in cmd])) try: subprocess.check_call(cmd, cwd=cwd, env=env, stderr=None, stdout=output) return 0 except Exception as error: printerr(error) return 1 def check_output(cmd, verbose=False): if verbose: print(' '.join([escapeCmdArg(arg) for arg in cmd])) return subprocess.check_output(cmd).strip() def get_sdk_path(platform): if platform.startswith('iosmac'): platform = 'macosx' return check_output(['xcrun', '-sdk', platform, '-show-sdk-path']) def write_fixed_module(file, platform, infix, verbose): common_modules_path = os.path.join(INFER_IMPORT_DIR, 'fixed-' + infix + '-modules-common.txt') platform_modules_path = os.path.join(INFER_IMPORT_DIR, 'fixed-' + infix + '-modules-' + platform + '.txt') with open(common_modules_path, 'r') as extra: if verbose: print('Including modules in: ' + common_modules_path) file.write(extra.read()) with open(platform_modules_path, 'r') as extra: if verbose: print('Including modules in: ' + platform_modules_path) file.write(extra.read()) def prepare_module_list(platform, file, verbose, module_filter_flags, include_fixed_clang_modules): cmd = [INFER_IMPORT_PATH, '-s', get_sdk_path(platform)] cmd.extend(module_filter_flags) if platform.startswith('iosmac'): cmd.extend(['--catalyst']) if verbose: cmd.extend(['--v']) check_call(cmd, verbose=verbose, output=file) # Always include fixed swift modules write_fixed_module(file, platform, 'swift', verbose) # Check if we need fixed clang modules if include_fixed_clang_modules: write_fixed_module(file, platform, 'clang', verbose) def get_api_digester_path(tool_path): if tool_path: return tool_path return check_output(['xcrun', '--find', 'swift-api-digester']) def create_directory(path): if not os.path.isdir(path): os.makedirs(path) class DumpConfig: def __init__(self, tool_path, platform, platform_alias, abi, verbose): target_map = { 'iphoneos': 'arm64-apple-ios13.0', 'macosx': 'x86_64-apple-macosx10.15', 'appletvos': 'arm64-apple-tvos13.0', 'watchos': 'armv7k-apple-watchos6.0', 'iosmac': 'x86_64-apple-ios13.1-macabi', } self.tool_path = get_api_digester_path(tool_path) self.platform = platform self.target = target_map[platform] self.sdk = get_sdk_path(platform) self.inputs = [] self.platform_alias = platform_alias self.abi = abi if self.platform == 'macosx': # We need this input search path for CreateML self.inputs.extend([self.sdk + '/usr/lib/swift/']) # This is where XCTest is self.frameworks = [self.sdk + '/../../Library/Frameworks/'] if self.platform.startswith('iosmac'): # Catalyst modules need this extra framework dir iOSSupport = self.sdk + \ '/System/iOSSupport/System/Library/Frameworks' self.frameworks.extend([iOSSupport]) self._environ = dict(os.environ) self._environ['SWIFT_FORCE_MODULE_LOADING'] = 'prefer-interface' self.verbose = verbose def dumpZipperedContent(self, cmd, output, module): dir_path = os.path.realpath(output + '/' + module) if self.abi: dir_path = os.path.join(dir_path, 'ABI') else: dir_path = os.path.join(dir_path, 'API') file_path = os.path.realpath(dir_path + '/' + self.platform_alias + '.json') create_directory(dir_path) current_cmd = list(cmd) current_cmd.extend(['-module', module]) current_cmd.extend(['-o', file_path]) check_call(current_cmd, env=self._environ, verbose=self.verbose) def run(self, output, module, swift_ver, opts, module_filter_flags, include_fixed_clang_modules, separate_by_module, zippered): cmd = [self.tool_path, '-sdk', self.sdk, '-target', self.target, '-dump-sdk', '-module-cache-path', '/tmp/ModuleCache', '-swift-version', swift_ver, '-abort-on-module-fail'] for path in self.frameworks: cmd.extend(['-iframework', path]) for path in self.inputs: cmd.extend(['-I', path]) if self.abi: cmd.extend(['-abi', '-swift-only']) cmd.extend(['-' + o for o in opts]) if self.verbose: cmd.extend(['-v']) if module: if zippered: create_directory(output) self.dumpZipperedContent(cmd, output, module) else: cmd.extend(['-module', module]) cmd.extend(['-o', output]) check_call(cmd, env=self._environ, verbose=self.verbose) else: with tempfile.NamedTemporaryFile() as tmp: prepare_module_list(self.platform, tmp, self.verbose, module_filter_flags, include_fixed_clang_modules) if separate_by_module: tmp.seek(0) create_directory(output) for module in [name.strip() for name in tmp.readlines()]: # Skip comments if module.startswith('//'): continue self.dumpZipperedContent(cmd, output, module) else: cmd.extend(['-o', output]) cmd.extend(['-module-list-file', tmp.name]) check_call(cmd, env=self._environ, verbose=self.verbose) class DiagnoseConfig: def __init__(self, tool_path, abi): self.tool_path = get_api_digester_path(tool_path) self.abi = abi def run(self, opts, before, after, output, verbose): cmd = [self.tool_path, '-diagnose-sdk', '-input-paths', before, '-input-paths', after, '-print-module'] if output: cmd.extend(['-o', output]) if self.abi: cmd.extend(['-abi']) cmd.extend(['-' + o for o in opts]) if verbose: cmd.extend(['-v']) check_call(cmd, verbose=verbose) def main(): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=''' A convenient wrapper for swift-api-digester. ''') basic_group = parser.add_argument_group('Basic') basic_group.add_argument('--tool-path', default=None, help=''' the path to a swift-api-digester; if not specified, the script will use the one from the toolchain ''') basic_group.add_argument('--action', default='', help=''' the action to perform for swift-api-digester ''') basic_group.add_argument('--target', default=None, help=''' one of macosx, iphoneos, appletvos, and watchos ''') basic_group.add_argument('--output', default=None, help=''' the output file of the module baseline should end with .json ''') basic_group.add_argument('--swift-version', default='5', help=''' Swift version to use; default is 5 ''') basic_group.add_argument('--module', default=None, help=''' name of the module/framework to generate baseline, e.g. Foundation ''') basic_group.add_argument('--module-filter', default='', help=''' the action to perform for swift-api-digester ''') basic_group.add_argument('--opts', nargs='+', default=[], help=''' additional flags to pass to swift-api-digester ''') basic_group.add_argument('--v', action='store_true', help='Process verbosely') basic_group.add_argument('--dump-before', action=None, help=''' Path to the json file generated before change' ''') basic_group.add_argument('--dump-after', action=None, help=''' Path to the json file generated after change ''') basic_group.add_argument('--separate-by-module', action='store_true', help='When importing entire SDK, dump content ' 'separately by module names') basic_group.add_argument('--zippered', action='store_true', help='dump module content to a dir with files for' 'separately targets') basic_group.add_argument('--abi', action='store_true', help='Process verbosely') basic_group.add_argument('--platform-alias', default='', help=''' Specify a file name to use if using a platform name in json file isn't optimal ''') args = parser.parse_args(sys.argv[1:]) if args.action == 'dump': if not args.target: fatal_error("Need to specify --target") if not args.output: fatal_error("Need to specify --output") if args.module_filter == '': module_filter_flags = [] include_fixed_clang_modules = True elif args.module_filter == 'swift-frameworks-only': module_filter_flags = ['--swift-frameworks-only'] include_fixed_clang_modules = False elif args.module_filter == 'swift-overlay-only': module_filter_flags = ['--swift-overlay-only'] include_fixed_clang_modules = False else: fatal_error("cannot recognize --module-filter") if args.platform_alias == '': args.platform_alias = args.target runner = DumpConfig(tool_path=args.tool_path, platform=args.target, platform_alias=args.platform_alias, abi=args.abi, verbose=args.v) runner.run(output=args.output, module=args.module, swift_ver=args.swift_version, opts=args.opts, module_filter_flags=module_filter_flags, include_fixed_clang_modules=include_fixed_clang_modules, separate_by_module=args.separate_by_module, zippered=args.zippered) elif args.action == 'diagnose': if not args.dump_before: fatal_error("Need to specify --dump-before") if not args.dump_after: fatal_error("Need to specify --dump-after") runner = DiagnoseConfig(tool_path=args.tool_path, abi=args.abi) runner.run(opts=args.opts, before=args.dump_before, after=args.dump_after, output=args.output, verbose=args.v) else: fatal_error('Cannot recognize action: ' + args.action) if __name__ == '__main__': main()