Files
gnatstudio/osx_bundle/scripts/resolver.py
2024-06-06 12:20:22 +00:00

160 lines
5.4 KiB
Python

import os
import os.path
import re
import subprocess
import sys
OTOOL_LINE_FILTER = re.compile("(.*\.dylib\.?.*)\s\(compatibility.*$")
class Resolver(object):
"""Class used to track and resolve an executable's dependencies"""
inputs = []
to_copy = {}
ignore = []
to_analyze = []
def __init__(self, prefix, mains, library_path):
"""Takes a list of libraries or executable as input, and creates self.to_copy
containing the dependencies"""
self.prefix = prefix
self.library_path = library_path
self._resolve_library_dependencies(mains)
@staticmethod
def filterlines(lines):
"""Filters lines with regexp and returns the group 1 from matching lines"""
for line in lines:
match = OTOOL_LINE_FILTER.match(line)
if match:
yield match.group(1).strip().split()[0]
def prefix_filter(self, line):
if line in self.ignore:
return False
if line in list(self.to_copy.keys()):
return True
if not os.path.isabs(line):
return False
for prefix in self.library_path:
if line.startswith(prefix):
return True
if (
not line.startswith("/usr/lib")
and not line.startswith("/System/Library")
and not line.strip().split()[0].endswith("libgcc_s.1.dylib")
):
basename = os.path.basename(line)
for prefix in self.library_path:
tentative = os.path.join(prefix, "lib", basename)
if os.path.isfile(tentative):
print(
"Warning, using %s instead of %s: may have been moved"
% (tentative, line)
)
return True
# Ignore the library from the OS, and raise a warning for the others
print("!!! ERROR, library not available in any prefix: %s" % line)
self.ignore.append(line)
return False
def to_bundle(self, filename, prefix):
"""Returns the bundle's filename for library filename"""
return filename.replace(prefix, self.prefix)
def _get_deps(self, filename):
"""Retrieve the list of filename's dynamic library in use"""
cmd = "xcrun otool -L %s" % filename
with os.popen(cmd) as f:
libs = Resolver.filterlines([line.strip() for line in f])
libs = list(filter(self.prefix_filter, libs))
return set(libs)
def _add_deps(self, f_list):
"""Filters libraries based on their location"""
for lib in f_list:
libs = self._get_deps(lib)
for lib in libs:
if lib in list(self.to_copy.keys()):
continue
if lib in self.ignore:
continue
found = False
for prefix in self.library_path:
if lib.startswith(prefix):
# From authorized prefixes
self.to_copy[lib] = (lib, self.to_bundle(lib, prefix))
self.inputs.append(lib)
found = True
break
if found:
continue
# lib not found in prefixes, so it's been moved at some point
basename = os.path.basename(lib)
found = False
for prefix in self.library_path:
tentative = os.path.join(prefix, "lib", basename)
if os.path.isfile(tentative):
self.to_copy[lib] = (
tentative,
self.to_bundle(tentative, prefix),
)
self.inputs.append(tentative)
found = True
break
def to_relative(self, filename):
"""Rebase the library dependencies relative to filename location"""
lines = self._get_deps(filename)
for lib in lines:
libname = os.path.basename(lib)
cmd = "xcrun install_name_tool -change %s %s %s" % (lib, libname, filename)
subprocess.call(cmd, shell=True)
cmd = "xcrun install_name_tool -id %s %s" % (
os.path.basename(filename),
filename,
)
subprocess.call(cmd, shell=True)
def _resolve_library_dependencies(self, mains):
"""Recursively gather all library dependencies of executable mainpath"""
self.inputs = mains[:]
n_iterations = 0
n_paths = -1
for main in mains:
do_copy = False
# First check if we need to copy the files given as input
for prefix in self.library_path:
if main.startswith(prefix):
self.to_copy[main] = (main, main.replace(prefix, self.prefix))
do_copy = True
break
if not do_copy:
self.to_copy[main] = ("", main)
# we iterate until we don't have any new library
while n_paths != len(self.to_copy):
n_paths = len(self.to_copy)
# run otool on the input files
self._add_deps(self.inputs[:])
n_iterations += 1
if n_iterations > 10:
print("Too many tries to resolve library dependencies")
sys.exit(1)