fw4spl
osx_install_name_tool.py
1 # -*- coding: UTF8 -*-
2 
3 
4 import fnmatch
5 import os
6 import re
7 import sys
8 
9 from collections import defaultdict
10 from functools import partial
11 from subprocess import Popen, PIPE
12 
13 def p(x): print "==",x.name,"=="
14 
15 usually_ignored = [
16  'AGL',
17  'AppKit',
18  'ApplicationServices',
19  'AudioToolbox',
20  'AudioUnit',
21  'Carbon',
22  'Cocoa',
23  'CoreAudio',
24  'CoreFoundation',
25  'CoreServices',
26  'CoreVideo',
27  'CoreWLAN',
28  'Foundation',
29  'IOKit',
30  'OpenGL',
31  'OpenCL',
32  'QTKit',
33  'QuartzCore',
34  'QuickTime',
35  'Security',
36  'SystemConfiguration',
37  'WebKit',
38  'libSystem.B.dylib',
39  'libgcc_s.1.dylib',
40  'libiodbc',
41  'libobjc.A.dylib',
42  'libresolv',
43  'libstdc++.6.dylib',
44  ]
45 
46 
47 def cached_property(func):
48  from functools import wraps
49  name = func.__name__
50  @wraps(func)
51  def _get(self):
52  try :
53  return self.__dict__[name]
54  except KeyError :
55  value = func(self)
56  self.__dict__[name]= value
57  return value
58  def _del(self):
59  self.__dict__.pop(name, None)
60  return property(_get, None, _del)
61 
62 def try_remove_item(list_object):
63  def remove(item):
64  try:
65  list_object.remove(item)
66  except:
67  pass
68  return remove
69 
70 class DepFilter(object):
71 
72  @staticmethod
73  def dep_filter(dep):
74  return all([
75  dep,
76  not '.o):' in dep,
77  ])
78 
79  @staticmethod
80  def no_executable_path_filter(dep):
81  return (
82  DepFilter.dep_filter(dep)
83  and not dep.startswith('@executable_path')
84  )
85 
86 
87 class BinaryFile(object):
88 
89  db = dict()
90  ignored = defaultdict(set)
91  fw_re = re.compile('.*?(?P<fw>[^/]+\.framework.+)')
92 
93  def __init__(self, path, dep_filter):
94  self.path = path
95  m = BinaryFile.fw_re.match(path)
96  if m:
97  self.name = m.groupdict()['fw']
98  else:
99  self.name = os.path.basename(path)
100  self.dep_filter = dep_filter
101 
102  @cached_property
103  def deps(self):
104  otool = Popen(['otool', '-L', self.path], stdout=PIPE)
105  output = otool.communicate()[0]
106  binfiles = []
107  if not otool.returncode:
108  binfiles = [l.strip().split()[0] for l in output.splitlines()[1:]]
109  binfiles = [BinaryFile(binfile, self.dep_filter)
110  for binfile in filter(self.dep_filter, binfiles) ]
111  return dict((l.name, l) for l in binfiles)
112  else:
113  return {}
114 
115  def __repr__(self):
116  return ''.join(['BinaryFile(path=', repr(self.path), ')'])
117 
118  def __str__(self):
119  return ': '.join((self.name, self.path))
120 
121  def __eq__(self,other):
122  return self.name == other.name
123 
124  def __hash__(self):
125  return hash(self.name)
126 
127  @staticmethod
128  def find(path, pattern, dep_filter):
129  reg = fnmatch.translate(pattern)
130  reg = re.compile(reg + '|[^.]+\Z')
131  match = reg.match
132  binfiles = []
133  if os.path.isfile(path):
134  binfiles += [BinaryFile(path, dep_filter)]
135  else:
136  for (root, dirs, files) in os.walk(path):
137  map(try_remove_item(dirs), ['Headers','Resources'])
138  files = [BinaryFile(os.path.join(root,f), dep_filter) for f in files]
139  binfiles += [f for f in files if match(f.name)]
140  return dict((l.name, l) for l in binfiles)
141 
142 
143  def install_name_id(self, path_func):
144  return [['-id', path_func(self.path)]]
145 
146  def install_name_changes(self, lib_set, path_func, ignored = None):
147  deps = self.deps
148  deps_set = set(deps.values())
149  inter = lib_set.intersection(deps_set)
150  if ignored is not None:
151  ignored_libs = [lib.name for lib in deps_set.difference(lib_set)]
152  ignored.update(ignored_libs)
153  for ig in ignored_libs:
154  BinaryFile.ignored[ig].add(self)
155  res = [
156  [ '-change', deps[lib.name].path,
157  path_func(self.db[lib.name].path) ] for lib in inter
158  ]
159  return res
160 
161 
162 
163 
164 def get_options(args):
165  from optparse import OptionParser
166 
167  usage = "usage: %prog [options] binary_or_dir ..."
168  epilog = """Updates the install names of executable or shared libraries
169  found in argument list (binary_or_dir).
170  """
171  parser = OptionParser(usage=usage, epilog="")
172 
173  parser.add_option("-a", "--absolute-path",
174  action="store_true", dest="abs_path", default=False,
175  help=("Uses absolute path for install names that will be "
176  "relocated"),
177  )
178 
179  parser.add_option("-e", "--executable-path", dest="exec_path",
180  help=("specifies the executable path to which the install "
181  "names will be relocated. disabled if '-a' is used."),
182  metavar="DIR")
183 
184  #parser.add_option("-L", "--library-path",
185  # action="append", dest="libpath",
186  # help="libraries path", metavar="DIR")
187 
188  parser.add_option("-f", "--force",
189  action="store_true", dest="force", default=False,
190  help=("Updates dependency even if it already has an "
191  "@executable_path "),
192  )
193 
194 
195  parser.add_option("-p", "--progress",
196  action="store_true", dest="progress", default=False,
197  help="show progress")
198 
199  parser.add_option("-i", "--show-ignored",
200  action="store_true", dest="show_ignored", default=False,
201  help="show libraries that have not been relocated")
202 
203 
204  parser.add_option("-u", "--show-suspiciously-ignored",
205  action="count", dest="show_suspiciously_ignored",
206  default=False,
207  help="show libraries that have not been relocated "
208  "and should have. Set it twice to have more details on "
209  "wich libralibvtkVisuManagement.dylibries was requirering one of these.")
210 
211 
212  parser.add_option("-v", "--verbose",
213  action="store_true", dest="verbose", default=False,
214  help="verbose output")
215 
216  parser.add_option("-P", "--search-pattern", dest="search_pattern",
217  default="*.dylib", help= ("specifies the executable path to "
218  "which the install names will be relocated. "
219  "disabled if '-a' is used."), metavar="PATTERN")
220 
221  parser.add_option("-s", "--search",
222  action="append", dest="search_dirs", default=[],
223  help="this directory will be searched with pattern "
224  "provided by -P", metavar="DIR")
225 
226 
227 
228  (options, args) = parser.parse_args(args)
229  return (options, args)
230 
231 
232 def main(options, args):
233  def print_msg(*a):
234  print ' '.join(map(str,a))
235  def print_msg_n(*a):
236  print ' '.join(map(str,a)),
237 
238  def mute(*a):
239  pass
240 
241  verbose = mute
242  verbose_n = mute
243  progress_print = mute
244  progress_print_n = mute
245 
246  if options.verbose:
247  verbose = print_msg
248  verbose_n = print_msg_n
249 
250  if options.progress:
251  progress_print = print_msg
252  progress_print_n = print_msg_n
253 
254 
255  if options.force:
256  dep_filter = DepFilter.dep_filter
257  else:
258  dep_filter = DepFilter.no_executable_path_filter
259 
260  find_binary_files = partial(BinaryFile.find, dep_filter = dep_filter)
261  update_db = lambda pattern : lambda path : BinaryFile.db.update(
262  find_binary_files(path, pattern)
263  )
264 
265  map(update_db('*.dylib'), args)
266  map(update_db('*.framework/Versions/*'), args)
267  map(update_db(options.search_pattern), options.search_dirs)
268 
269  relpath = os.path.relpath
270  librelpath = lambda path : os.path.join( '@executable_path', relpath( path, options.exec_path ) )
271 
272  abspath = os.path.abspath
273  libabspath = lambda path : abspath( path )
274 
275  if options.abs_path:
276  install_name_func = libabspath
277  else:
278  install_name_func = librelpath
279 
280 
281  ignored = set()
282  L = len(BinaryFile.db)
283  files = set(BinaryFile.db.values())
284 
285  for n,lib in enumerate(files):
286  lib_file = lib.path
287  progress_print_n(' ', '{0}/{1}'.format(n+1,L),'\r')
288  sys.stdout.flush()
289  if not os.path.islink(lib_file):
290  changes = lib.install_name_id(install_name_func)
291  changes += lib.install_name_changes(files, install_name_func, ignored)
292  verbose(lib_file)
293  for change in changes:
294  os.chmod(lib_file, 0775)
295  cmd = ['install_name_tool',] + change + [lib_file,]
296  res = Popen(cmd, stdout=PIPE).communicate()[0]
297  if res:
298  verbose(res)
299  progress_print('')
300 
301  if any([
302  options.show_ignored,
303  options.show_suspiciously_ignored,
304  ]):
305  ignored = sorted( map(str,ignored))
306 
307  if options.show_ignored:
308  print 'ignored libraries:', (os.linesep+' ').join(ignored)
309 
310  if options.show_suspiciously_ignored:
311  #suspicious = list(set(ignored) - set(usually_ignored))
312  match = lambda x : not any( ig in x for ig in usually_ignored )
313  suspicious = [ig for ig in ignored if match(ig)]
314  libsep = (os.linesep+' ')
315  depsep = (os.linesep+' - ')
316  if options.show_suspiciously_ignored == 1:
317  print 'suspiciously ignored :', libsep + libsep.join(suspicious)
318  else:
319  si = [["<%s>"%s + ', required by :']
320  + [l.name for l in BinaryFile.ignored[s]] for s in suspicious]
321  print 'suspiciously ignored :', (libsep
322  + libsep.join(depsep.join(d) for d in si))
323 
324 
325 if __name__ == '__main__':
326  (options, args) = get_options(sys.argv[1:])
327  main(options, args)
328