1#!/usr/bin/env python3
2
3import os
4import sys
5import shutil
6import subprocess
7
8nocopy_list = [
9    "@rpath/QtCore.framework/Versions/5/QtCore",
10    "@rpath/QtDBus.framework/Versions/5/QtDBus",
11    "@rpath/QtGui.framework/Versions/5/QtGui",
12    "@rpath/QtOpenGL.framework/Versions/5/QtOpenGL",
13    "@rpath/QtPrintSupport.framework/Versions/5/QtPrintSupport",
14    "@rpath/QtWidgets.framework/Versions/5/QtWidgets",
15]
16
17
18def fix_qt_frameworks(app):
19    frameworks_dir = os.path.join(app, "Contents", "Frameworks")
20    for framework in os.listdir(frameworks_dir):
21        if not framework.startswith("Qt"):
22            continue
23        framework_dir = os.path.join(frameworks_dir, framework)
24        print(framework)
25        name, ext = framework.split(".")
26        v5 = os.path.join(framework_dir, "Versions", "5")
27        assert os.path.exists(v5)
28        current = os.path.join(framework_dir, "Versions", "Current")
29        if not os.path.exists(current):
30            os.symlink("5", current)
31        resources = os.path.join(v5, "Resources")
32        if not os.path.exists(resources):
33            os.makedirs(resources)
34        # library_link = os.path.join(framework_dir, name)
35        # if not os.path.exists(library_link):
36        #     os.symlink(os.path.join("Versions", "Current", name), library_link)
37        resources_link = os.path.join(framework_dir, "Resources")
38        if not os.path.exists(resources_link):
39            os.symlink("Versions/Current/Resources", resources_link)
40        plist = os.path.join(resources, "Info.plist")
41        with open(plist, "w", encoding="UTF-8") as f:
42            f.write("""<?xml version="1.0" encoding="UTF-8"?>
43<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
44<plist version="1.0">
45<dict>
46	<key>CFBundleExecutable</key>
47	<string>{name}</string>
48	<key>CFBundleGetInfoString</key>
49	<string>Created by standalone.py</string>
50	<key>CFBundleIdentifier</key>
51	<string>org.qt-project.{name}</string>
52	<key>CFBundlePackageType</key>
53	<string>FMWK</string>
54	<key>CFBundleShortVersionString</key>
55	<string>1.0</string>
56	<key>CFBundleSignature</key>
57	<string>????</string>
58	<key>CFBundleVersion</key>
59	<string>1.0</string>
60</dict>
61</plist>
62""".format(name=name))
63
64
65def fix_binary(path, macos_dir):
66    args = ["file", path]
67    p = subprocess.Popen(args, stdout=subprocess.PIPE)
68    data = p.stdout.read().decode("UTF-8")
69    p.wait()
70    if "Mach-O" not in data:
71        return 0
72
73    print("fixing", path)
74    changes = 0
75    if not os.path.exists(path):
76        raise Exception("could not find " + repr(path))
77
78    args = ["otool", "-l", path]
79    p = subprocess.Popen(args, stdout=subprocess.PIPE)
80    data = p.stdout.read().decode("UTF-8")
81    p.wait()
82
83    # Adding to rpath causes problems with signing
84    # "because larger updated load commands do not fit"
85    # if "@executable_path/../Frameworks" not in data:
86    #     args = ["install_name_tool", "-add_rpath",
87    #             "@executable_path/../Frameworks", path]
88    #     print(args)
89    #     p = subprocess.Popen(args)
90    #     p.wait()
91
92    # PyQt modules
93    if "@loader_path/Qt/lib" in data:
94        args = ["install_name_tool", "-rpath", "@loader_path/Qt/lib",
95                "@executable_path/../Frameworks", path]
96        print(args)
97        p = subprocess.Popen(args)
98        p.wait()
99    # Qt frameworks
100    if "@loader_path/../../lib" in data:
101        args = ["install_name_tool", "-rpath", "@loader_path/../../lib",
102                "@executable_path/../Frameworks", path]
103        print(args)
104        p = subprocess.Popen(args)
105        p.wait()
106
107    args = ["otool", "-L", path]
108    p = subprocess.Popen(args, stdout=subprocess.PIPE)
109    data = p.stdout.read().decode("UTF-8")
110    p.wait()
111    for line in data.split('\n'):
112        line = line.strip()
113        if not line:
114            continue
115        if line.startswith("/usr/lib") or line.startswith("/System"):
116            # old = line.split(' ')[0]
117            # print("ignoring", old)
118            continue
119        if line.startswith("@executable_path"):
120            continue
121
122        old = line.split(" ")[0]
123        # if old in ignore_list:
124        #     continue
125        if old == "@rpath/XCTest.framework/Versions/A/XCTest":
126            continue
127        if "Contents" in old:
128            continue
129        print(old)
130
131        if os.path.basename(path) == os.path.basename(old):
132            if "/" not in old:
133                continue
134            os.chmod(path, 0o755)
135            args = ["install_name_tool", "-id", os.path.basename(path), path]
136            print(args)
137            p = subprocess.Popen(args)
138            assert p.wait() == 0
139            changes += 1
140            continue
141
142        # if not os.path.isabs(old):
143        #     old = os.path.join(
144        #         os.environ["DYLD_FALLBACK_LIBRARY_PATH"].split(":")[0], old)
145
146        old_dir, name = os.path.split(old)
147        # new = old.replace(old, '@executable_path/../Frameworks/' + name)
148        new = old.replace(old, '@executable_path/' + name)
149        # dst = os.path.join(frameworks_dir, os.path.basename(old))
150        dst = os.path.join(macos_dir, os.path.basename(old))
151        if not os.path.exists(dst):
152            print("copying", old)
153            libs_f.write(old + "\n")
154            shutil.copy(old, dst)
155            os.chmod(dst, 0o755)
156            changes += 1
157        # print(os.path.basename(path), "vs", os.path.basename(old))
158        # if os.path.basename(path) == os.path.basename(old):
159        #     args = ["install_name_tool", "-id", new, path]
160        # else:
161        os.chmod(path, 0o755)
162        args = ["install_name_tool", "-change", old, new, path]
163        print(args)
164        p = subprocess.Popen(args)
165        assert p.wait() == 0
166
167    return changes
168
169
170def fix_iteration(app):
171    binaries = []
172    macos_dir = os.path.join(app, "Contents", "MacOS")
173    extra_paths = []
174    for dir_path, dir_names, file_names in os.walk(macos_dir):
175        for name in file_names:
176            p = os.path.join(dir_path, name)
177            if os.path.isdir(p):
178                pass
179            elif p.endswith("_debug.dylib"):
180                pass
181            else:
182                binaries.append(p)
183    # for name in os.listdir(macos_dir):
184    #     p = os.path.join(macos_dir, name)
185    #    if os.path.isdir(p):
186    #         extra_paths.append(p)
187    #     else:
188    #         binaries.append(os.path.join(macos_dir, name))
189    # for extra_dir in extra_paths:
190    #     for name in os.listdir(extra_dir):
191    #         p = os.path.join(extra_dir, name)
192    #         if os.path.isdir(p):
193    #             for name2 in os.listdir(p):
194    #                 if not name2.endswith("_debug.dylib"):
195    #                     binaries.append(os.path.join(p, name2))
196    #         else:
197    #             binaries.append(p)
198
199    frameworks_dir = os.path.join(app, "Contents", "Frameworks")
200    if os.path.exists(frameworks_dir):
201        for dir_path, dir_names, file_names in os.walk(frameworks_dir):
202            for name in file_names:
203                p = os.path.join(dir_path, name)
204                binaries.append(p)
205
206    # Qt plugins
207    plugins_dir = os.path.join(app, "Contents", "PlugIns")
208    if os.path.exists(plugins_dir):
209        for dir_path, dir_names, file_names in os.walk(plugins_dir):
210            for name in file_names:
211                p = os.path.join(dir_path, name)
212                binaries.append(p)
213
214    changes = 0
215    for binary in binaries:
216        changes += fix_binary(binary, macos_dir)
217    return changes
218
219
220def main():
221    global libs_f
222    app = sys.argv[1]
223    # fix_qt_frameworks(app)
224    with open("libs.txt", "w") as libs_f:
225        while True:
226            changes = fix_iteration(app)
227            if changes == 0:
228                break
229
230
231if __name__ == "__main__":
232    main()
233