about summary refs log tree commit diff
path: root/nixpkgs/pkgs/os-specific/darwin/gen-frameworks.py
blob: ec2a6c7c16ecd1d82f6bdbf7c53b8f989a99d4c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/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]} <path to MacOSX.sdk>")
        sys.exit(64)

    scan_sdk(sys.argv[1])