FreeCAD

Форк
0
/
MakeMacBundleRelocatable.py 
424 строки · 12.7 Кб
1
import os
2
import sys
3
from subprocess import check_call, check_output
4
import re
5
import logging
6

7
# This script is intended to help copy dynamic libraries used by FreeCAD into
8
# a Mac application bundle and change dyld commands as appropriate.  There are
9
# two key items that this currently does differently from other similar tools:
10
#
11
#  * @rpath is used rather than @executable_path because the libraries need to
12
#    be loadable through a Python interpreter and the FreeCAD binaries.
13
#  * We need to be able to add multiple rpaths in some libraries.
14

15
# Assume any libraries in these paths don't need to be bundled
16
systemPaths = [
17
    "/System/",
18
    "/usr/lib/",
19
    "/Library/Frameworks/3DconnexionClient.framework/",
20
]
21

22
# If a library is in these paths, but not systemPaths, a warning will be
23
# issued and it will NOT be bundled.  Generally, libraries installed by
24
# MacPorts or Homebrew won't end up in /Library/Frameworks, so we assume
25
# that libraries found there aren't meant to be bundled.
26
warnPaths = ["/Library/Frameworks/"]
27

28

29
class LibraryNotFound(Exception):
30
    pass
31

32

33
class Node:
34
    """
35
    self.path should be an absolute path to self.name
36
    """
37

38
    def __init__(self, name, path="", children=None):
39
        self.name = name
40
        self.path = path
41
        if not children:
42
            children = list()
43
        self.children = children
44
        self._marked = False
45

46
    def __eq__(self, other):
47
        if not isinstance(other, Node):
48
            return False
49
        return self.name == other.name
50

51
    def __ne__(self, other):
52
        return not self.__eq__(other)
53

54
    def __hash__(self):
55
        return hash(self.name)
56

57
    def __str__(self):
58
        return self.name + " path: " + self.path + " num children: " + str(len(self.children))
59

60

61
class DepsGraph:
62
    graph = {}
63

64
    def in_graph(self, node):
65
        return node.name in list(self.graph)
66

67
    def add_node(self, node):
68
        self.graph[node.name] = node
69

70
    def get_node(self, name):
71
        if name in self.graph:
72
            return self.graph[name]
73
        return None
74

75
    def visit(self, operation, op_args=[]):
76
        """ "
77
        Perform a depth first visit of the graph, calling operation
78
        on each node.
79
        """
80
        stack = []
81

82
        for k in list(self.graph):
83
            self.graph[k]._marked = False
84

85
        for k in list(self.graph):
86
            if not self.graph[k]._marked:
87
                stack.append(k)
88
                while stack:
89
                    node_key = stack.pop()
90
                    self.graph[node_key]._marked = True
91
                    for ck in self.graph[node_key].children:
92
                        if not self.graph[ck]._marked:
93
                            stack.append(ck)
94
                    operation(self, self.graph[node_key], *op_args)
95

96

97
def is_macho(path):
98
    return b"Mach-O" in check_output(["file", path])
99

100

101
def get_token(txt, delimiter=" (", first=True):
102
    result = txt.decode().split(delimiter)
103
    if first:
104
        return result[0]
105
    else:
106
        return result
107

108

109
def is_system_lib(lib):
110
    for p in systemPaths:
111
        if lib.startswith(p):
112
            return True
113
    for p in warnPaths:
114
        if lib.startswith(p):
115
            logging.warning("WARNING: library %s will not be bundled!" % lib)
116
            logging.warning("See MakeMacRelocatable.py for more information.")
117
            return True
118
    return False
119

120

121
def get_path(name, search_paths):
122
    for path in search_paths:
123
        if os.path.isfile(os.path.join(path, name)):
124
            return path
125
    return None
126

127

128
def list_install_names(path_macho):
129
    output = check_output(["otool", "-L", path_macho])
130
    lines = output.split(b"\t")
131
    libs = []
132

133
    # first line is the filename, and if it is a library, the second line
134
    # is the install name of it
135
    if path_macho.endswith(os.path.basename(get_token(lines[1]))):
136
        lines = lines[2:]
137
    else:
138
        lines = lines[1:]
139

140
    for line in lines:
141
        lib = get_token(line)
142
        if not is_system_lib(lib):
143
            libs.append(lib)
144
    return libs
145

146

147
def library_paths(install_names, search_paths):
148
    paths = []
149
    for name in install_names:
150
        path = os.path.dirname(name)
151
        lib_name = os.path.basename(name)
152

153
        if path == "" or name[0] == "@":
154
            # not absolute -- we need to find the path of this lib
155
            path = get_path(lib_name, search_paths)
156

157
        paths.append(os.path.join(path, lib_name))
158

159
    return paths
160

161

162
def create_dep_nodes(install_names, search_paths):
163
    """
164
    Return a list of Node objects from the provided install names.
165
    """
166
    nodes = []
167
    for lib in install_names:
168
        install_path = os.path.dirname(lib)
169
        lib_name = os.path.basename(lib)
170

171
        # even if install_path is absolute, see if library can be found by
172
        # searching search_paths, so that we have control over what library
173
        # location to use
174
        path = get_path(lib_name, search_paths)
175

176
        if install_path != "" and lib[0] != "@":
177
            # we have an absolute path install name
178
            if not path:
179
                path = install_path
180

181
        if not path:
182
            logging.error("Unable to find LC_DYLD_LOAD entry: " + lib)
183
            raise LibraryNotFound(lib_name + " not found in given search paths")
184

185
        nodes.append(Node(lib_name, path))
186

187
    return nodes
188

189

190
def paths_at_depth(prefix, paths, depth):
191
    filtered = []
192
    for p in paths:
193
        dirs = os.path.join(prefix, p).strip("/").split("/")
194
        if len(dirs) == depth:
195
            filtered.append(p)
196
    return filtered
197

198

199
def should_visit(prefix, path_filters, path):
200
    s_path = path.strip("/").split("/")
201
    filters = []
202
    # we only want to use filters if they have the same parent as path
203
    for rel_pf in path_filters:
204
        pf = os.path.join(prefix, rel_pf)
205
        if os.path.split(pf)[0] == os.path.split(path)[0]:
206
            filters.append(pf)
207
    if not filters:
208
        # no filter that applies to this path
209
        return True
210

211
    for pf in filters:
212
        s_filter = pf.strip("/").split("/")
213
        length = len(s_filter)
214
        matched = 0
215
        for i in range(len(s_path)):
216
            if s_path[i] == s_filter[i]:
217
                matched += 1
218
            if matched == length or matched == len(s_path):
219
                return True
220

221
    return False
222

223

224
def build_deps_graph(graph, bundle_path, dirs_filter=None, search_paths=[]):
225
    """
226
    Walk bundle_path and build a graph of the encountered Mach-O binaries
227
    and there dependencies
228
    """
229
    # make a local copy since we add to it
230
    s_paths = list(search_paths)
231

232
    visited = {}
233

234
    for root, dirs, files in os.walk(bundle_path):
235
        if dirs_filter is not None:
236
            dirs[:] = [
237
                d for d in dirs if should_visit(bundle_path, dirs_filter, os.path.join(root, d))
238
            ]
239

240
        s_paths.insert(0, root)
241

242
        for f in files:
243
            fpath = os.path.join(root, f)
244
            ext = os.path.splitext(f)[1]
245
            if (ext == "" and is_macho(fpath)) or ext == ".so" or ext == ".dylib":
246
                visited[fpath] = False
247

248
    stack = []
249
    for k in list(visited):
250
        if not visited[k]:
251
            stack.append(k)
252
            while stack:
253
                k2 = stack.pop()
254
                visited[k2] = True
255

256
                node = Node(os.path.basename(k2), os.path.dirname(k2))
257
                if not graph.in_graph(node):
258
                    graph.add_node(node)
259

260
                try:
261
                    deps = create_dep_nodes(list_install_names(k2), s_paths)
262
                except Exception:
263
                    logging.error("Failed to resolve dependency in " + k2)
264
                    raise
265

266
                for d in deps:
267
                    if d.name not in node.children:
268
                        node.children.append(d.name)
269

270
                    dk = os.path.join(d.path, d.name)
271
                    if dk not in list(visited):
272
                        visited[dk] = False
273
                    if not visited[dk]:
274
                        stack.append(dk)
275

276

277
def in_bundle(lib, bundle_path):
278
    if lib.startswith(bundle_path):
279
        return True
280
    return False
281

282

283
def copy_into_bundle(graph, node, bundle_path):
284
    if not in_bundle(node.path, bundle_path):
285
        source = os.path.join(node.path, node.name)
286
        target = os.path.join(bundle_path, "lib", node.name)
287
        logging.info("Bundling {}".format(source))
288

289
        check_call(["cp", "-L", source, target])
290

291
        node.path = os.path.dirname(target)
292

293
        # fix permissions
294
        check_call(["chmod", "a+w", target])
295

296

297
def get_rpaths(library):
298
    "Returns a list of rpaths specified within library"
299

300
    out = check_output(["otool", "-l", library])
301

302
    pathRegex = r"^path (.*) \(offset \d+\)$"
303
    expectingRpath = False
304
    rpaths = []
305
    for line in get_token(out, "\n", False):
306
        line = line.strip()
307

308
        if "cmd LC_RPATH" in line:
309
            expectingRpath = True
310
        elif "Load command" in line:
311
            expectingRpath = False
312
        elif expectingRpath:
313
            m = re.match(pathRegex, line)
314
            if m:
315
                rpaths.append(m.group(1))
316
                expectingRpath = False
317

318
    return rpaths
319

320

321
def add_rpaths(graph, node, bundle_path):
322
    lib = os.path.join(node.path, node.name)
323

324
    if in_bundle(lib, bundle_path):
325
        logging.debug(lib)
326

327
        # Remove existing rpaths that could take precedence
328
        for rpath in get_rpaths(lib):
329
            logging.debug(" - rpath: " + rpath)
330
            check_call(["install_name_tool", "-delete_rpath", rpath, lib])
331

332
        if node.children:
333
            install_names = list_install_names(lib)
334
            rpaths = []
335

336
            for install_name in install_names:
337
                name = os.path.basename(install_name)
338
                # change install names to use rpaths
339
                logging.debug(" ~ rpath: " + name + " => @rpath/" + name)
340
                check_call(
341
                    [
342
                        "install_name_tool",
343
                        "-change",
344
                        install_name,
345
                        "@rpath/" + name,
346
                        lib,
347
                    ]
348
                )
349

350
                dep_node = node.children[node.children.index(name)]
351
                rel_path = os.path.relpath(graph.get_node(dep_node).path, node.path)
352
                rpath = ""
353
                if rel_path == ".":
354
                    rpath = "@loader_path/"
355
                else:
356
                    rpath = "@loader_path/" + rel_path + "/"
357
                if rpath not in rpaths:
358
                    rpaths.append(rpath)
359

360
            for rpath in rpaths:
361
                # Ensure that lib has rpath set
362
                if not rpath in get_rpaths(lib):
363
                    logging.debug(" + rpath: " + rpath)
364
                    check_call(["install_name_tool", "-add_rpath", rpath, lib])
365

366

367
def change_libid(graph, node, bundle_path):
368
    lib = os.path.join(node.path, node.name)
369

370
    logging.debug(lib)
371

372
    if in_bundle(lib, bundle_path):
373
        logging.debug(" ~ id: " + node.name)
374
        try:
375
            check_call(["install_name_tool", "-id", node.name, lib])
376
        except Exception:
377
            logging.warning("Failed to change bundle id {} in lib {}".format(node.name, lib))
378

379

380
def print_child(graph, node, path):
381
    logging.debug("  >" + str(node))
382

383

384
def print_node(graph, node, path):
385
    logging.debug(node)
386
    graph.visit(print_child, [node])
387

388

389
def main():
390
    if len(sys.argv) < 2:
391
        print("Usage " + sys.argv[0] + " path [additional search paths]")
392
        quit()
393

394
    path = sys.argv[1]
395
    bundle_path = os.path.abspath(os.path.join(path, "Contents"))
396
    graph = DepsGraph()
397
    dir_filter = ["MacOS", "lib", "Mod"]
398
    search_paths = [bundle_path + "/lib"] + sys.argv[2:]
399

400
    # change to level to logging.DEBUG for diagnostic messages
401
    logging.basicConfig(
402
        stream=sys.stdout, level=logging.INFO, format="-- %(levelname)s: %(message)s"
403
    )
404

405
    logging.info("Analyzing bundle dependencies...")
406
    build_deps_graph(graph, bundle_path, dir_filter, search_paths)
407

408
    if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
409
        graph.visit(print_node, [bundle_path])
410

411
    logging.info("Copying external dependencies to bundle...")
412
    graph.visit(copy_into_bundle, [bundle_path])
413

414
    logging.info("Updating dynamic loader paths...")
415
    graph.visit(add_rpaths, [bundle_path])
416

417
    logging.info("Setting bundled library IDs...")
418
    graph.visit(change_libid, [bundle_path])
419

420
    logging.info("Done.")
421

422

423
if __name__ == "__main__":
424
    main()
425

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.