| #!/usr/bin/env python |
| |
| from __future__ import print_function |
| |
| import argparse |
| import errno |
| import itertools |
| import json |
| import multiprocessing |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import traceback |
| from shutil import copyfile |
| |
| BARE_INTERFACE_SEARCH_PATHS = [ |
| "usr/lib/swift", |
| "System/iOSSupport/usr/lib/swift" |
| ] |
| DEFAULT_FRAMEWORK_INTERFACE_SEARCH_PATHS = [ |
| "System/Library/Frameworks", |
| "System/iOSSupport/System/Library/Frameworks" |
| ] |
| STDLIB_NAME = 'Swift' |
| |
| MONOTONIC_VERSION = 1 |
| |
| |
| def create_parser(): |
| parser = argparse.ArgumentParser( |
| description="Builds an SDK's swiftinterfaces into swiftmodules. " |
| "Always searches usr/lib/swift in addition to whichever " |
| "framework directories are passed on the command line.", |
| prog=os.path.basename(__file__), |
| usage='%(prog)s -o output/ [INTERFACE_SEARCH_DIRS]', |
| epilog='Environment variables: SDKROOT, SWIFT_EXEC, ' |
| 'SWIFT_FORCE_MODULE_LOADING') |
| parser.add_argument('interface_framework_dirs', nargs='*', |
| metavar='INTERFACE_SEARCH_DIRS', |
| help='Relative paths to search for frameworks with ' |
| 'interfaces (default: System/Library/Frameworks)') |
| parser.add_argument('-o', dest='output_dir', |
| help='Directory to which the output will be emitted ' |
| '(required)') |
| parser.add_argument('-j', dest='jobs', type=int, |
| help='The number of parallel jobs to execute ' |
| '(default: # of cores)') |
| parser.add_argument('-v', dest='verbose', action='store_true', |
| help='Print command invocations and progress info') |
| parser.add_argument('-n', dest='dry_run', action='store_true', |
| help='Dry run: don\'t actually run anything') |
| parser.add_argument('-sdk', default=os.getenv('SDKROOT'), |
| help='SDK to find frameworks and interfaces in ' |
| '(default: $SDKROOT)') |
| parser.add_argument('-F', dest='framework_dirs', metavar='DIR', |
| action='append', default=[], |
| help='Add additional framework search paths') |
| parser.add_argument('-Fsystem', '-iframework', |
| dest='system_framework_dirs', metavar='DIR', |
| action='append', default=[], |
| help='Add additional system framework search paths') |
| parser.add_argument('-Fsystem-iosmac', |
| dest='iosmac_system_framework_dirs', metavar='DIR', |
| action='append', default=[], |
| help='Add system framework search paths ' |
| 'for iOSMac only') |
| parser.add_argument('-I', dest='include_dirs', metavar='DIR', |
| action='append', default=[], |
| help='Add additional header/module search paths') |
| parser.add_argument('-module-cache-path', |
| help='Temporary directory to store intermediate info') |
| parser.add_argument('-log-path', |
| help='Directory to write stdout/stderr output to') |
| parser.add_argument('-skip-stdlib', action='store_true', |
| help='Don\'t build the standard library interface') |
| parser.add_argument('-disable-modules-validate-system-headers', |
| action='store_true', |
| help='Disable modules verification for system headers') |
| parser.add_argument('-xfails', metavar='PATH', |
| help='JSON file containing an array of the modules ' |
| 'expected to fail') |
| parser.add_argument('-check-only', action='store_true', |
| help='Assume the resulting modules will be thrown ' |
| 'away (may be faster)') |
| parser.add_argument('-ignore-non-stdlib-failures', action='store_true', |
| help='Treat all modules but the stdlib as XFAILed') |
| parser.add_argument('-debug-crash-compiler', action='store_true', |
| help='Have the compiler crash (for testing purposes)') |
| parser.add_argument('-machine-parseable-monotonic-version', |
| action='store_true', |
| help='For comparing versions of this tool') |
| return parser |
| |
| |
| def fatal(msg): |
| print(msg, file=sys.stderr) |
| sys.exit(1) |
| |
| |
| def run_command(args, dry_run): |
| if dry_run: |
| return (0, "", "") |
| proc = subprocess.Popen(args, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| try: |
| out, err = proc.communicate() |
| exitcode = proc.returncode |
| return (exitcode, out.decode('utf-8'), err.decode('utf-8')) |
| except KeyboardInterrupt: |
| proc.terminate() |
| raise |
| |
| |
| def make_dirs_if_needed(path): |
| try: |
| os.makedirs(path) |
| except OSError as e: |
| if e.errno != errno.EEXIST: |
| raise |
| |
| |
| class NegatedSet: |
| def __init__(self, contents): |
| self._contents = frozenset(contents) |
| |
| def __contains__(self, item): |
| return item not in self._contents |
| |
| |
| class ModuleFile: |
| def __init__(self, name, path, is_expected_to_fail): |
| self.name = name |
| self.path = path |
| self.is_expected_to_fail = is_expected_to_fail |
| |
| |
| def collect_slices(xfails, swiftmodule_dir): |
| if not os.path.isdir(swiftmodule_dir): |
| return |
| module_name, extension = \ |
| os.path.splitext(os.path.basename(swiftmodule_dir)) |
| assert extension == ".swiftmodule" |
| |
| is_xfail = module_name in xfails |
| for entry in os.listdir(swiftmodule_dir): |
| _, extension = os.path.splitext(entry) |
| if extension == ".swiftinterface": |
| yield ModuleFile(module_name, os.path.join(swiftmodule_dir, entry), |
| is_xfail) |
| |
| |
| def collect_framework_modules(sdk, xfails, sdk_relative_framework_dirs): |
| for sdk_relative_framework_dir in sdk_relative_framework_dirs: |
| framework_dir = os.path.join(sdk, sdk_relative_framework_dir) |
| if not os.access(framework_dir, os.R_OK): |
| continue |
| for entry in os.listdir(framework_dir): |
| path_without_extension, extension = os.path.splitext(entry) |
| if extension != ".framework": |
| continue |
| module_name = os.path.basename(path_without_extension) |
| swiftmodule = os.path.join(framework_dir, entry, "Modules", |
| module_name + ".swiftmodule") |
| if os.access(swiftmodule, os.R_OK): |
| for x in collect_slices(xfails, swiftmodule): |
| yield x |
| |
| |
| def collect_non_framework_modules(sdk, xfails, sdk_relative_search_dirs): |
| for sdk_relative_search_dir in sdk_relative_search_dirs: |
| search_dir = os.path.join(sdk, sdk_relative_search_dir) |
| for dir_path, _, file_names in os.walk(search_dir, followlinks=True): |
| if os.path.splitext(dir_path)[1] == ".swiftmodule": |
| for x in collect_slices(xfails, dir_path): |
| yield x |
| else: |
| for interface in file_names: |
| module_name, extension = os.path.splitext(interface) |
| if extension == ".swiftinterface": |
| is_xfail = module_name in xfails |
| yield ModuleFile(module_name, |
| os.path.join(dir_path, interface), |
| is_xfail) |
| |
| |
| def should_retry_compilation(stderr): |
| if "has been modified since the module file" in stderr: |
| return True |
| if "mismatched umbrella headers in submodule" in stderr: |
| return True |
| if "is out of date and needs to be rebuilt: signature mismatch" in stderr: |
| return True |
| if "current parser token 'include'" in stderr: |
| return True |
| if "current parser token 'import'" in stderr: |
| return True |
| return False |
| |
| |
| def run_with_module_cache_retry(command_args, module_cache_path, dry_run): |
| """Hack: runs a command several times, clearing the module cache if we get |
| an error about header files being modified during the run. |
| |
| This shouldn't be necessary (the cached PCM files should automatically be |
| regenerated) but there seems to still be a bug in Clang that we haven't |
| tracked down yet. |
| """ |
| RETRIES = 3 |
| attempts_stderr = "" |
| for r in range(RETRIES): |
| status, stdout, stderr = run_command(command_args, dry_run) |
| if status == 0: |
| break |
| if not should_retry_compilation(stderr): |
| break |
| if module_cache_path: |
| shutil.rmtree(module_cache_path, ignore_errors=True) |
| # If all retries fail, output information for each instance. |
| attempts_stderr += ( |
| "\n*** Compilation attempt {}/{} failed with modules bugs. " |
| "Error output:\n".format(r + 1, RETRIES)) |
| attempts_stderr += stderr |
| stderr = attempts_stderr |
| return (status, stdout, stderr) |
| |
| |
| def log_output_to_file(content, module_name, interface_base, label, log_path): |
| if not log_path: |
| return |
| if not content: |
| return |
| make_dirs_if_needed(log_path) |
| log_name = module_name + "-" + interface_base + "-" + label + ".txt" |
| with open(os.path.join(log_path, log_name), "w") as output_file: |
| output_file.write(content) |
| |
| |
| def looks_like_iosmac(interface_base): |
| return 'ios-macabi' in interface_base |
| |
| |
| def process_module(module_file): |
| global args, shared_output_lock |
| try: |
| interface_base, _ = \ |
| os.path.splitext(os.path.basename(module_file.path)) |
| |
| swiftc = os.getenv('SWIFT_EXEC', |
| os.path.join(os.path.dirname(__file__), 'swiftc')) |
| command_args = [ |
| swiftc, '-frontend', |
| '-build-module-from-parseable-interface', |
| '-sdk', args.sdk, |
| '-prebuilt-module-cache-path', args.output_dir, |
| ] |
| module_cache_path = "" |
| if args.module_cache_path: |
| module_cache_path = os.path.join(args.module_cache_path, |
| str(os.getpid())) |
| command_args += ('-module-cache-path', module_cache_path) |
| if args.debug_crash_compiler: |
| command_args += ('-debug-crash-immediately',) |
| if not args.check_only: |
| command_args += ( |
| '-serialize-parseable-module-interface-dependency-hashes',) |
| if args.disable_modules_validate_system_headers: |
| command_args += ( |
| '-disable-modules-validate-system-headers',) |
| |
| # FIXME: This shouldn't be necessary, but the module name is checked |
| # before the frontend action is. |
| if module_file.name == STDLIB_NAME: |
| command_args += ('-parse-stdlib',) |
| |
| if looks_like_iosmac(interface_base): |
| for system_framework_path in args.iosmac_system_framework_dirs: |
| command_args += ('-Fsystem', system_framework_path) |
| command_args += ('-Fsystem', os.path.join(args.sdk, "System", |
| "iOSSupport", "System", |
| "Library", "Frameworks")) |
| |
| for include_path in args.include_dirs: |
| command_args += ('-I', include_path) |
| for system_framework_path in args.system_framework_dirs: |
| command_args += ('-Fsystem', system_framework_path) |
| for framework_path in args.framework_dirs: |
| command_args += ('-F', framework_path) |
| |
| command_args += ('-module-name', module_file.name, module_file.path) |
| |
| output_path = os.path.join(args.output_dir, |
| module_file.name + ".swiftmodule") |
| |
| if interface_base != module_file.name: |
| make_dirs_if_needed(output_path) |
| output_path = os.path.join(output_path, |
| interface_base + ".swiftmodule") |
| |
| command_args += ('-o', output_path) |
| |
| if args.verbose: |
| with shared_output_lock: |
| print("# Starting " + module_file.path) |
| print(' '.join(command_args)) |
| sys.stdout.flush() |
| status, stdout, stderr = run_with_module_cache_retry( |
| command_args, module_cache_path=module_cache_path, |
| dry_run=args.dry_run) |
| log_output_to_file(stdout, module_file.name, interface_base, "out", |
| log_path=args.log_path) |
| log_output_to_file(stderr, module_file.name, interface_base, "err", |
| log_path=args.log_path) |
| return (module_file, status, stdout, stderr) |
| except BaseException: |
| # We're catching everything here because we don't want to take down the |
| # other jobs. |
| return (module_file, 1, "", |
| "".join(traceback.format_exception(*sys.exc_info()))) |
| |
| |
| def set_up_child(parent_args, lock): |
| global args, shared_output_lock |
| args = parent_args |
| shared_output_lock = lock |
| |
| |
| def process_module_files(pool, module_files): |
| results = pool.imap_unordered(process_module, module_files) |
| |
| overall_exit_status = 0 |
| for (module_file, exit_status, stdout, stderr) in results: |
| with shared_output_lock: |
| if exit_status != 0: |
| print("# ", end="") |
| if module_file.is_expected_to_fail: |
| print("(XFAIL) ", end="") |
| else: |
| print("(FAIL) ", end="") |
| print(module_file.path) |
| if (not module_file.is_expected_to_fail) or args.verbose: |
| print(stdout, end="") |
| print(stderr, end="", file=sys.stderr) |
| elif module_file.is_expected_to_fail: |
| print("# (UPASS) " + module_file.path) |
| elif args.verbose: |
| print("# (PASS) " + module_file.path) |
| sys.stdout.flush() |
| if overall_exit_status == 0 and \ |
| not module_file.is_expected_to_fail: |
| overall_exit_status = exit_status |
| return overall_exit_status |
| |
| |
| def getSDKVersion(sdkroot): |
| settingPath = os.path.join(sdkroot, 'SDKSettings.json') |
| with open(settingPath) as json_file: |
| data = json.load(json_file) |
| return data['Version'] |
| fatal("Failed to get SDK version from: " + settingPath) |
| |
| |
| def copySystemVersionFile(sdkroot, output): |
| sysInfoPath = os.path.join(sdkroot, |
| 'System/Library/CoreServices/SystemVersion.plist') |
| destInfoPath = os.path.join(output, 'SystemVersion.plist') |
| try: |
| copyfile(sysInfoPath, destInfoPath) |
| except BaseException as e: |
| print("cannot copy from " + sysInfoPath + " to " + destInfoPath + ": " + str(e)) |
| |
| |
| def main(): |
| global args, shared_output_lock |
| parser = create_parser() |
| args = parser.parse_args() |
| |
| if args.machine_parseable_monotonic_version: |
| print(MONOTONIC_VERSION) |
| sys.exit(0) |
| |
| if 'SWIFT_FORCE_MODULE_LOADING' not in os.environ: |
| os.environ['SWIFT_FORCE_MODULE_LOADING'] = 'prefer-serialized' |
| |
| if not args.output_dir: |
| fatal("argument -o is required") |
| if not args.sdk: |
| fatal("SDKROOT must be set in the environment") |
| if not os.path.isdir(args.sdk): |
| fatal("invalid SDK: " + args.sdk) |
| |
| # if the given output dir ends with 'prebuilt-modules', we should |
| # append the SDK version number so all modules will built into |
| # the SDK-versioned sub-directory. |
| if os.path.basename(args.output_dir) == 'prebuilt-modules': |
| args.output_dir = os.path.join(args.output_dir, getSDKVersion(args.sdk)) |
| |
| xfails = () |
| if args.ignore_non_stdlib_failures: |
| if args.xfails: |
| print("warning: ignoring -xfails because " |
| "-ignore-non-stdlib-failures was provided", file=sys.stderr) |
| xfails = NegatedSet((STDLIB_NAME,)) |
| elif args.xfails: |
| with open(args.xfails) as xfails_file: |
| xfails = json.load(xfails_file) |
| |
| make_dirs_if_needed(args.output_dir) |
| |
| # Copy a file containing SDK build version into the prebuilt module dir, |
| # so we can keep track of the SDK version we built from. |
| copySystemVersionFile(args.sdk, args.output_dir) |
| if 'ANDROID_DATA' not in os.environ: |
| shared_output_lock = multiprocessing.Lock() |
| pool = multiprocessing.Pool(args.jobs, set_up_child, |
| (args, shared_output_lock)) |
| else: |
| # Android doesn't support Python's multiprocessing as it doesn't have |
| # sem_open, so switch to a ThreadPool instead. |
| import threading |
| shared_output_lock = threading.Lock() |
| from multiprocessing.pool import ThreadPool |
| pool = ThreadPool(args.jobs, set_up_child, |
| (args, shared_output_lock)) |
| |
| interface_framework_dirs = (args.interface_framework_dirs or |
| DEFAULT_FRAMEWORK_INTERFACE_SEARCH_PATHS) |
| |
| module_files = list(itertools.chain( |
| collect_non_framework_modules(args.sdk, xfails, |
| BARE_INTERFACE_SEARCH_PATHS), |
| collect_framework_modules(args.sdk, xfails, interface_framework_dirs))) |
| |
| if not args.skip_stdlib: |
| # Always do the stdlib first, so that we can use it in later steps |
| stdlib_module_files = ( |
| x for x in module_files if x.name == STDLIB_NAME) |
| status = process_module_files(pool, stdlib_module_files) |
| if status != 0: |
| sys.exit(status) |
| |
| non_stdlib_module_files = ( |
| x for x in module_files if x.name != STDLIB_NAME) |
| status = process_module_files(pool, non_stdlib_module_files) |
| if os.name == 'nt': |
| import ctypes |
| Kernel32 = ctypes.cdll.LoadLibrary("Kernel32.dll") |
| Kernel32.ExitProcess(ctypes.c_ulong(status)) |
| sys.exit(status) |
| |
| |
| if __name__ == '__main__': |
| main() |