#!/usr/bin/env nix-shell #!nix-shell -i python -p python3 swiftPackages.swift-unwrapped """ Generate a frameworks.nix for a macOS SDK. You may point this tool at an Xcode bundled SDK, but more ideal is using the SDK from Nixpkgs. For example: SDK_PATH="$(nix-build --no-link -A darwin.apple_sdk_11_0.MacOSX-SDK)" ./gen-frameworks.py "$SDK_PATH" > ./new-frameworks.nix """ import json import os import subprocess import sys ALLOWED_LIBS = ["simd"] HEADER = """\ # This file is generated by gen-frameworks.nix. # Do not edit, put overrides in apple_sdk.nix instead. { libs, frameworks }: with libs; with frameworks; { """ FOOTER = """\ } """ def eprint(*args): print(*args, file=sys.stderr) def name_from_ident(ident): return ident.get("swift", ident.get("clang")) def scan_sdk(sdk): # Find frameworks by scanning the SDK frameworks directory. frameworks = [ framework.removesuffix(".framework") for framework in os.listdir(f"{sdk}/System/Library/Frameworks") if not framework.startswith("_") ] frameworks.sort() # Determine the longest name for padding output. width = len(max(frameworks, key=len)) output = HEADER for framework in frameworks: deps = [] # Use Swift to scan dependencies, because a module may have both Clang # and Swift parts. Using Clang only imports the Clang module, whereas # using Swift will usually import both Clang + Swift overlay. # # TODO: The above is an assumption. Not sure if it's possible a Swift # module completely shadows a Clang module. (Seems unlikely) # # TODO: Handle "module 'Foobar' is incompatible with feature 'swift'" # # If there were a similar Clang invocation for scanning, we could fix # the above todos, but that doesn't appear to exist. eprint(f"# scanning {framework}") result = subprocess.run( [ "swiftc", "-scan-dependencies", # We provide a source snippet via stdin. "-", # Use the provided SDK. "-sdk", sdk, # This search path is normally added automatically by the # compiler based on the SDK, but we have a patch in place that # removes that for SDKs in /nix/store, because our xcbuild stub # SDK doesn't have the directory. # (swift-prevent-sdk-dirs-warning.patch) "-I", f"{sdk}/usr/lib/swift", # For some reason, 'lib/swift/shims' from both the SDK and # Swift compiler are picked up, causing redefinition errors. # This eliminates the latter. "-resource-dir", f"{sdk}/usr/lib/swift", ], input=f"import {framework}".encode(), stdout=subprocess.PIPE, ) if result.returncode != 0: eprint(f"# Scanning {framework} failed (exit code {result.returncode})") result.stdout = b"" # Parse JSON output. if len(result.stdout) != 0: data = json.loads(result.stdout) # Entries in the modules list come in pairs. The first is an # identifier (`{ swift: "foobar" }` or `{ clang: "foobar" }`), and # the second metadata for that module. Here we look for the pair # that matches the framework we're scanning (and ignore the rest). modules = data["modules"] for i in range(0, len(modules), 2): ident, meta = modules[i : i + 2] # NOTE: We may match twice, for a Swift module _and_ for a # Clang module. So matching here doesn't break from the loop, # and deps is appended to. if name_from_ident(ident) == framework: dep_idents = meta["directDependencies"] deps += [name_from_ident(ident) for ident in dep_idents] # List unfiltered deps in progress output. eprint(ident, "->", dep_idents) # Filter out modules that are not separate derivations. # Also filter out duplicates (when a Swift overlay imports the Clang module) allowed = frameworks + ALLOWED_LIBS deps = set([dep for dep in deps if dep in allowed]) # Filter out self-references. (Swift overlay importing Clang module.) if framework in deps: deps.remove(framework) # Generate a Nix attribute line. if len(deps) != 0: deps = list(deps) deps.sort() deps = " ".join(deps) output += f" {framework.ljust(width)} = {{ inherit {deps}; }};\n" else: output += f" {framework.ljust(width)} = {{}};\n" output += FOOTER sys.stdout.write(output) if __name__ == "__main__": if len(sys.argv) != 2: eprint(f"Usage: {sys.argv[0]} ") sys.exit(64) scan_sdk(sys.argv[1])