spack/lib/spack/external/macholib/MachOStandalone.py
2022-01-28 10:55:12 -08:00

174 lines
5.7 KiB
Python

import os
from collections import deque
from macholib.dyld import framework_info
from macholib.MachOGraph import MachOGraph, MissingMachO
from macholib.util import (
flipwritable,
has_filename_filter,
in_system_path,
iter_platform_files,
mergecopy,
mergetree,
)
class ExcludedMachO(MissingMachO):
pass
class FilteredMachOGraph(MachOGraph):
def __init__(self, delegate, *args, **kwargs):
super(FilteredMachOGraph, self).__init__(*args, **kwargs)
self.delegate = delegate
def createNode(self, cls, name):
cls = self.delegate.getClass(name, cls)
res = super(FilteredMachOGraph, self).createNode(cls, name)
return self.delegate.update_node(res)
def locate(self, filename, loader=None):
newname = super(FilteredMachOGraph, self).locate(filename, loader)
if newname is None:
return None
return self.delegate.locate(newname, loader=loader)
class MachOStandalone(object):
def __init__(self, base, dest=None, graph=None, env=None, executable_path=None):
self.base = os.path.join(os.path.abspath(base), "")
if dest is None:
dest = os.path.join(self.base, "Contents", "Frameworks")
self.dest = dest
self.mm = FilteredMachOGraph(
self, graph=graph, env=env, executable_path=executable_path
)
self.changemap = {}
self.excludes = []
self.pending = deque()
def update_node(self, m):
return m
def getClass(self, name, cls):
if in_system_path(name):
return ExcludedMachO
for base in self.excludes:
if name.startswith(base):
return ExcludedMachO
return cls
def locate(self, filename, loader=None):
if in_system_path(filename):
return filename
if filename.startswith(self.base):
return filename
for base in self.excludes:
if filename.startswith(base):
return filename
if filename in self.changemap:
return self.changemap[filename]
info = framework_info(filename)
if info is None:
res = self.copy_dylib(filename)
self.changemap[filename] = res
return res
else:
res = self.copy_framework(info)
self.changemap[filename] = res
return res
def copy_dylib(self, filename):
# When the filename is a symlink use the basename of the target of
# the link as the name in standalone bundle. This avoids problems
# when two libraries link to the same dylib but using different
# symlinks.
if os.path.islink(filename):
dest = os.path.join(self.dest, os.path.basename(os.path.realpath(filename)))
else:
dest = os.path.join(self.dest, os.path.basename(filename))
if not os.path.exists(dest):
self.mergecopy(filename, dest)
return dest
def mergecopy(self, src, dest):
return mergecopy(src, dest)
def mergetree(self, src, dest):
return mergetree(src, dest)
def copy_framework(self, info):
dest = os.path.join(self.dest, info["shortname"] + ".framework")
destfn = os.path.join(self.dest, info["name"])
src = os.path.join(info["location"], info["shortname"] + ".framework")
if not os.path.exists(dest):
self.mergetree(src, dest)
self.pending.append((destfn, iter_platform_files(dest)))
return destfn
def run(self, platfiles=None, contents=None):
mm = self.mm
if contents is None:
contents = "@executable_path/.."
if platfiles is None:
platfiles = iter_platform_files(self.base)
for fn in platfiles:
mm.run_file(fn)
while self.pending:
fmwk, files = self.pending.popleft()
ref = mm.findNode(fmwk)
for fn in files:
mm.run_file(fn, caller=ref)
changemap = {}
skipcontents = os.path.join(os.path.dirname(self.dest), "")
machfiles = []
for node in mm.flatten(has_filename_filter):
machfiles.append(node)
dest = os.path.join(
contents,
os.path.normpath(node.filename[len(skipcontents) :]), # noqa: E203
)
changemap[node.filename] = dest
def changefunc(path):
if path.startswith("@loader_path/"):
# This is a quick hack for py2app: In that
# usecase paths like this are found in the load
# commands of relocatable wheels. Those don't
# need rewriting.
return path
res = mm.locate(path)
rv = changemap.get(res)
if rv is None and path.startswith("@loader_path/"):
rv = changemap.get(mm.locate(mm.trans_table.get((node.filename, path))))
return rv
for node in machfiles:
fn = mm.locate(node.filename)
if fn is None:
continue
rewroteAny = False
for _header in node.headers:
if node.rewriteLoadCommands(changefunc):
rewroteAny = True
if rewroteAny:
old_mode = flipwritable(fn)
try:
with open(fn, "rb+") as f:
for _header in node.headers:
f.seek(0)
node.write(f)
f.seek(0, 2)
f.flush()
finally:
flipwritable(fn, old_mode)
allfiles = [mm.locate(node.filename) for node in machfiles]
return set(filter(None, allfiles))