forked from NeoGeographyToolkit/BinaryBuilder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BinaryDist.py
1000 lines (857 loc) · 37.3 KB
/
BinaryDist.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
import os.path as P
import logging
import itertools, shutil, re, errno, sys, os, stat, subprocess, platform
from os import makedirs, remove, listdir, chmod, symlink, readlink, link
from collections import namedtuple
from tempfile import mkdtemp, NamedTemporaryFile
from glob import glob
from functools import partial, wraps
from hashlib import sha1
'''
Code for creating the downloadable binary distribution of ASP.
'''
global logger
logger = logging.getLogger()
def binary_builder_prefix():
return 'BinaryBuilder'
def hash_file(filename):
with open(filename, 'rb') as f:
return sha1(f.read()).hexdigest()
def run(*args, **kw):
'''Try to execute a command line command'''
need_output = kw.pop('output', False)
raise_on_failure = kw.pop('raise_on_failure', True)
want_stderr = kw.pop('want_stderr', False)
kw['stdout'] = kw.get('stdout', subprocess.PIPE)
kw['stderr'] = kw.get('stderr', subprocess.PIPE)
logger.debug('run: [%s] (wd=%s)' % (' '.join(args), kw.get('cwd', os.getcwd())))
p = subprocess.Popen(args, **kw)
out, err = p.communicate()
if out is not None:
out = out.decode('utf-8')
if err is not None:
err = err.decode('utf-8')
msg = None
if p.returncode != 0:
msg = '%s: return code: %d (output: %s) (error: %s)' % (args, p.returncode, out, err)
elif need_output and len(out) == 0:
msg = '%s: failed (no output). (%s)' % (args,err)
if msg is not None:
if raise_on_failure: raise Exception(msg)
logger.warn(msg)
return False, msg
if want_stderr:
return out, err
return out
def get_platform(pkg=None):
system = platform.system()
machine = platform.machine()
p = namedtuple('Platform', 'os bits osbits system machine prettyos dist_name dist_version')
if system == 'Linux':
dist = platform.linux_distribution(full_distribution_name=0)
name = str(dist[0]).replace("/", "_") # bugfix for Ubuntu, replace slashes
ver = str(dist[1]).replace("/", "_")
elif system == 'Darwin':
name = 'Darwin'
ver = platform.mac_ver()[0]
if system == 'Linux' and machine == 'x86_64':
return p('linux', 64, 'linux64', system, machine, 'Linux', name, ver)
elif system == 'Linux' and machine == 'i686':
return p('linux', 32, 'linux32', system, machine, 'Linux', name, ver)
elif system == 'Darwin' and machine == 'i386':
# Force 64 bit no matter what
return p('osx', 64, 'osx64', system, 'x86_64', 'OSX', name, ver)
elif system == 'Darwin' and machine == 'x86_64':
return p('osx', 64, 'osx64', system, machine, 'OSX', name, ver)
else:
message = 'Cannot match system to known platform'
if pkg is None:
raise Exception(message)
else:
raise PackageError(pkg, message)
# List resursively all files in given directory
def list_recursively(dir):
matches = []
for root, dirnames, filenames in os.walk(dir):
for filename in filenames:
matches.append(os.path.join(root, filename))
return matches
def make_list_unique(in_list):
'''Remove repetitions from a list.'''
vals_dict = {}
out_list = []
for val in in_list:
if val in vals_dict:
continue
out_list.append(val)
vals_dict[val] = 1
return out_list
def lib_ext(arch):
if arch == 'osx':
lib_ext = '.dylib'
else:
lib_ext = '.so'
return lib_ext
# What does this one do?
def strip_flag(flag, key, env):
ret = []
hit = None
if not key in env:
return
for test in env[key].split():
m = re.search(flag, test)
if m:
hit = m
else:
ret.append(test)
if ret:
env[key] = ' '.join(ret).strip()
else:
del env[key]
return hit, env
def is_binary(filename):
'''Use the linux "file" tool to deterimen if a given file is a binary file'''
ret = run('file', filename, output=True)
return (ret.find('ELF') != -1) or (ret.find('Mach-O') != -1)
def doctest_on(os):
'''Set up a function wrapper with a warning __doc__ if the provided os does not match?'''
def outer(f):
@wraps(f)
def inner(*args, **kw): return f(*args, **kw)
if get_platform().os != os:
inner.__doc__ = '%s is only supported on %s' % (inner.__name__, os)
return inner
return outer
def default_baker(filename, distdir, searchpath):
'''Updates a files rpath to be relative to distdir and strips it of symbols'''
if not is_binary(filename):
return
set_rpath(filename, distdir, searchpath)
# On linux stripping causes the conda libraries to crash
if get_platform().os != 'linux':
strip(filename)
def which(program):
'''Find if a program is in the PATH'''
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
def mkdir_f(dirname):
''' A mkdir -p that does not care if the dir is there '''
try:
makedirs(dirname)
except OSError as o:
if o.errno == errno.EEXIST and P.isdir(dirname):
return
raise
class DistManager(object):
'''Main class for creating a StereoPipeline binary distribution'''
def __init__(self, tarname, exec_wrapper_file, asp_install_dir, asp_deps_dir):
self.wrapper_file = exec_wrapper_file
self.tarname = tarname
self.tempdir = mkdtemp(prefix='dist')
self.distdir = DistPrefix(P.join(self.tempdir, self.tarname))
self.asp_install_dir = asp_install_dir
self.asp_deps_dir = asp_deps_dir
self.distlist = set() # List of files to be distributed
self.deplist = dict() # List of file dependencies
self.parentlib = dict() # library k is used by parentlib[k]
self.dst_to_src = dict()
mkdir_f(self.distdir)
def remove_tempdir(self):
shutil.rmtree(self.tempdir, True)
def add_executable(self, inpath, keep_symlink=True):
''' 'inpath' should be a file. This will add the executable to libexec/
and the wrapper script to bin/ (with the basename of the exe) '''
logger.debug('attempting to add %s' % inpath)
base = P.basename(inpath)
# When adding a symlink, also add the file it points to.
if P.islink(inpath):
paths = snap_symlinks(inpath)
else:
paths = [inpath]
# TODO: Why .py files are extempted from having a wrapper
# file? This can cause issues, as .py tools may end up
# searching for libraries outside of our lib directory. Fix
# and test this.
if base.endswith(".py"):
self._add_file(inpath, self.distdir.bin(base))
else:
for path in paths:
base = P.basename(path)
self._add_file(path, self.distdir.libexec(base))
self._add_file(self.wrapper_file, self.distdir.bin(base))
def add_library(self, inpath, symlinks_too=True, add_deps=True, is_plugin = False):
''' 'symlinks_too' means follow all symlinks, and add what they point
to. 'add_deps' means scan the library and add its required dependencies
to deplist.'''
logger.debug('attempting to add %s' % inpath)
if symlinks_too:
paths = snap_symlinks(inpath)
# This is a bugfix, sometimes not all symbolic links are added
all_paths = []
for path in paths:
for newpath in glob(path + '*'):
all_paths.append(newpath)
paths = make_list_unique(all_paths)
else:
paths = [inpath]
for p in paths:
# This pulls out only the filename for the library. We
# don't preserve the subdirs underneath 'lib'. This make
# later rpath code easier to understand.
lib = P.normpath(p).split('/')[-1]
if not is_plugin:
self._add_file(p, self.distdir.lib(lib), add_deps=add_deps)
else:
self._add_file(p, usgscsm_plugin_path(self.distdir, lib), add_deps=add_deps)
def add_glob(self, pattern, prefixes):
''' Add a pattern to the tree. pattern must be relative to an
installroot, provided in one of the prefixes.'''
if pattern == "":
raise Exception("Tried to add a glob with an empty pattern.")
inpaths = []
for prefix in prefixes:
pat = P.join(prefix, pattern)
inpaths = glob(pat)
[self.add_smart(i, prefix) for i in inpaths]
def add_smart(self, inpath, prefix):
''' Looks at the relative path, and calls the correct add_* function '''
if not P.isabs(inpath):
inpath = P.abspath(P.join(prefix, inpath))
assert P.commonprefix([inpath, prefix]) == prefix, \
'path[%s] must be within prefix[%s]' % (inpath, prefix)
relpath = P.relpath(inpath, prefix)
if P.isdir(inpath):
for d in listdir(P.join(prefix, inpath)):
self.add_smart(P.relpath(P.join(prefix, inpath, d), prefix), prefix)
elif relpath.startswith('bin/'):
self.add_executable(inpath)
elif relpath.startswith('lib/'):
self.add_library(inpath)
else:
self._add_file(inpath, self.distdir.base(relpath))
def add_file(self, src, dst=None, hardlink=False):
'''Copy a file to a destination given as a directory relative to distdir.
That is, cp src distdir/dst/src.'''
if dst is None:
dst = self.distdir
else:
dst = P.join(self.distdir, dst)
dst = P.join(dst, src)
self._add_file(src, dst)
def add_directory(self, src, dst = None, hardlink = False, subdirs = []):
'''
Recursively copy the files and dirs in src, and make them relative to directory dst.
That is:
cp -rf src/* dst/
Can also add only selected subdirectories.
'''
if dst is None: dst = self.distdir
if not P.exists(src):
raise Exception("Failed to find directory: " + src)
if len(subdirs) == 0:
# Copy everything in src
mergetree(src, dst, partial(self._add_file, hardlink=hardlink, add_deps=False))
else:
# Copy only selected subdirectories
for subdir in subdirs:
mergetree(src + "/" + subdir, dst + "/" + subdir,
partial(self._add_file, hardlink=hardlink, add_deps=False))
def remove_deps(self, seq):
''' Filter deps out of the deplist '''
for k in seq:
self.deplist.pop(k, None)
def remove_already_added(self, seq):
'''It seems easier to first copy all dependences,
then wipe the ones we do not want to ship.
We assume any entry is in distdir/*/entry'''
for k in seq:
files = glob(P.join(self.distdir, '*', k + '*'))
for f in files:
if os.path.exists(f):
try:
print("Removing: " + f)
os.remove(f)
except:
pass
def resolve_deps(self, nocopy, copy, search = None):
''' Find as many of the currently-listed deps as possible. If the dep
is found in one of the 'copy' dirs, copy it (without deps) to the dist.'''
if search is None:
search = list(itertools.chain(nocopy, copy))
logger.debug('Searching: %s' % (search,))
logger.debug('Dependency list--------------------------------------')
for lib in self.deplist:
logger.debug(' %s' % lib)
found = set()
for lib in self.deplist:
for searchdir in search:
checklib = P.join(searchdir, lib)
if P.exists(checklib):
found.add(lib)
logger.debug('\tFound: %s' % checklib)
if searchdir in copy:
self.add_library(checklib, add_deps=False)
break
self.remove_deps(found)
def create_file(self, relpath, mode='w'):
'''Create a new file in self.distdir and open it'''
return open(self.distdir.base(relpath), mode)
def bake(self, searchpath, baker = default_baker):
'''Updates the rpath of all files to be relative to distdir and strips it of symbols.
Also cleans up some junk in self.distdir and sets file permissions.'''
logger.debug('Baking list')
for filename in self.distlist:
logger.debug(' %s' % filename)
for filename in self.distlist:
baker(filename, self.distdir, searchpath)
# Delete all hidden files from the self.distdir folder
for i in run('find', self.distdir, '-name', '.*', '-print0').split('\0'):
if len(i) > 0 and i != '.' and i != '..':
try:
remove(i)
except Exception as e:
print(e)
def make_tarball(self, include = (), exclude = (), name = None):
'''Tar up all the files we have written to self.distdir.
exclude takes priority over include '''
if name is None: name = '%s.tar.bz2' % self.tarname
if isinstance(include, str):
include = [include]
if isinstance(exclude, str):
exclude = [exclude]
# Ensure all the files are readable
cmd = ['chmod', '-R', 'a+r', P.dirname(self.distdir)]
run(*cmd)
# Enable read/execute on all files in libexec, bin, and stereo plugins.
# Also for all subdirectories.
rwx_list = glob(self.distdir.libexec('*')) + glob(self.distdir.bin('*')) + \
glob(self.distdir + "/plugins/stereo/*/bin/*")
for dir_name in glob(self.distdir + "/*"):
if os.path.isdir(dir_name):
rwx_list.append(dir_name)
for path in rwx_list:
os.chmod(path, 0o755) # note we use the octal value of 755
# Use the current modification time. This is not working by
# default or some reason.
cmd = ['touch', self.distdir]
run(*cmd)
cmd = ['tar', 'cf', name, '--use-compress-prog=pbzip2']
cmd += ['-C', P.dirname(self.distdir)]
if include:
cmd += ['--no-recursion']
for i in include:
cmd += ['-T', i]
for e in exclude:
if os.path.exists(e):
cmd += ['-X', e]
cmd.append(self.tarname)
logger.info('Creating tarball %s' % name)
run(*cmd)
def find_filter(self, *filter, **kw):
'''Call "find" with a filter argument and write results to an opened temporary file'''
dir = kw.get('dir', self.tarname)
cwd = kw.get('cwd', P.dirname(self.distdir))
cmd = ['find', dir] + list(filter)
out = run(*cmd, cwd=cwd).encode()
files = NamedTemporaryFile()
files.write(out)
files.flush()
return files
def sym_link_lib(self, src, dst):
'''In the lib directory, symlink src to dst.'''
logger.debug('attempting to symlink ' + src + ' to ' + dst)
base_src = P.normpath(src).split('/')[-1]
base_dst = P.normpath(dst).split('/')[-1]
lib_dir = P.dirname(self.distdir.lib(base_src))
mkdir_f(lib_dir)
# Go to the lib dir and make the link
cmd = ['ln', '-s', base_src, base_dst]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=lib_dir)
out, err = p.communicate()
if out is not None:
out = out.decode('utf-8')
if err is not None:
err = err.decode('utf-8')
def _add_file(self, src, dst, hardlink=False, keep_symlink=True, add_deps=True):
'''Add a file to the list of distribution files'''
dst = P.abspath(dst)
assert not P.relpath(dst, self.distdir).startswith('..'), \
'destination %s must be within distdir[%s]' % (dst, self.distdir)
mkdir_f(P.dirname(dst))
# If a file to copy shows up in multiple places, prefer the
# one from the ASP install dir. Then the one from
# asp_deps_dir. Those are portable, unlike potentially the
# files in the current system.
if dst in self.dst_to_src:
if self.asp_install_dir in self.dst_to_src[dst] and (not self.asp_install_dir in src):
print("Will copy " + self.dst_to_src[dst] + " and not " + src)
return
if self.asp_deps_dir in self.dst_to_src[dst] and (not self.asp_deps_dir in src):
print("Will copy " + self.dst_to_src[dst] + " and not " + src)
return
self.dst_to_src[dst] = src
try:
copy(src, dst, keep_symlink=keep_symlink, hardlink=hardlink)
self.distlist.add(dst)
except Exception as e:
# Bail out if the copying failed.
# TODO(oalexan1): This may need finer-grained treatment
print("Warning: " + str(e))
return
if add_deps and is_binary(dst):
# Search for dependencies in our preferred locations first
search_path = self.asp_install_dir + "/lib" + ":" + self.asp_deps_dir + "/lib"
req = required_libs(dst, search_path)
self.deplist.update(req)
# Keep track for later which library needs the current library
for lib in req.keys():
if not lib in self.parentlib.keys():
self.parentlib[lib] = [dst]
else:
self.parentlib[lib].append(dst)
def copy(src, dst, hardlink=False, keep_symlink=True):
'''Copy a file to another location with a bunch of link handling'''
assert not P.isdir(src), 'Source path must not be a dir'
assert not P.isdir(dst), 'Destination path must not be a dir'
# There is nothing we can do about absolute sym links in system dirs. We just
# trace those to the source and copy.
if keep_symlink and P.islink(src):
if P.isabs(readlink(src)):
m = re.match('^/usr', readlink(src))
if m:
print("Resolving absolute link: ", src)
while P.islink(src) and \
P.isabs(readlink(src)) \
and os.path.basename(src) == os.path.basename(readlink(src)):
src = readlink(src)
print("Resolved to: ", src)
if keep_symlink and P.islink(src):
assert not P.isabs(readlink(src)), \
'Cannot copy symlink that points to an absolute path (%s)' % src
logger.debug('%8s %s -> %s' % ('symlink', src, dst))
# Some of the libraries are both in our install dir and in USGS conda's package.
# That is because they do not provide headers for cspice for example.
# So below we will run into trouble. Just overwrite any library we built
# with the one from conda.
if P.exists(dst):
is_soft_link = True
try:
link_val = readlink(dst)
except:
is_soft_link = False
if (not is_soft_link) or (readlink(dst) != readlink(src)):
print("Will overwrite " + dst + " with " + src)
os.remove(dst)
if not P.exists(dst):
try:
symlink(readlink(src), dst)
except:
pass
return
if P.exists(dst):
# This should happen rarely, normally the problem of which
# instance of a given file to copy should be solved by now.
if hash_file(src) != hash_file(dst):
print("Will overwrite " + dst + " with " + src + " having a different hash.")
if hardlink:
try:
link(src, dst)
logger.debug('%8s %s -> %s' % ('hardlink', src, dst))
return
except OSError as o:
# Invalid cross-device link, not an error, fall back to copy
if o.errno != errno.EXDEV:
raise
logger.debug('%8s %s -> %s' % ('copy', src, dst))
shutil.copyfile(src, dst)
# Bugfix, make it writeable
mode = os.stat(dst)[stat.ST_MODE]
os.chmod(dst, mode | stat.S_IWUSR)
@doctest_on('linux')
def readelf(filename):
''' Run readelf on a file
>>> readelf('/lib/libc.so.6') # doctest:+ELLIPSIS
readelf(needed=['ld-linux-...'], soname='libc.so.6', rpath=[])
>>> readelf('/bin/ls') # doctest:+ELLIPSIS
readelf(needed=[..., 'libc.so.6'], soname=None, rpath=[])
'''
Ret = namedtuple('readelf', 'needed soname rpath')
r = re.compile(' \((.*?)\).*\[(.*?)\]')
needed = []
soname = None
rpath = []
for line in run('readelf', '-d', filename, output=True).split('\n'):
m = r.search(line)
if m:
if m.group(1) == 'NEEDED': needed.append(m.group(2))
elif m.group(1) == 'SONAME': soname = m.group(2)
elif m.group(1) == 'RPATH' : rpath = m.group(2).split(':')
return Ret(needed, soname, rpath)
@doctest_on('linux')
def ldd(filename, search_path):
''' Run ldd on a file
>>> ldd('/lib/libc.so.6')
{}
>>> ldd('/bin/ls') # doctest:+ELLIPSIS
{..., 'libc.so.6': '/lib/libc.so.6'}
'''
libs = {}
r = re.compile('^\s*(\S+) => (\S+)')
# Ensure this is initalized
if "LD_LIBRARY_PATH" not in os.environ:
os.environ["LD_LIBRARY_PATH"] = ""
# Help ldd find the libraries in the desired location
orig_path = os.environ["LD_LIBRARY_PATH"]
os.environ["LD_LIBRARY_PATH"] = search_path
for line in run('ldd', filename, output=True).split('\n'):
m = r.search(line)
if m:
libs[m.group(1)] = (None if m.group(2) == 'not' else m.group(2))
# Restore the orginal environment
os.environ["LD_LIBRARY_PATH"] = orig_path
return libs
@doctest_on('osx')
def otool(filename):
''' Run otool on a binary
>>> otool('/usr/lib/libSystem.B.dylib')
otool(soname='libSystem.B.dylib', sopath='/usr/lib/libSystem.B.dylib', libs={'libmathCommon.A.dylib': '/usr/lib/system/libmathCommon.A.dylib'})
>>> otool('/bin/ls')
otool(soname=None, sopath=None, libs={'libSystem.B.dylib': '/usr/lib/libSystem.B.dylib', 'libncurses.5.4.dylib': '/usr/lib/libncurses.5.4.dylib'})
'''
Ret = namedtuple('otool', 'soname sopath libs abs_rpaths rel_rpaths')
r = re.compile('^\s*(\S+)')
lines = run('otool', '-L', filename, output=True).split('\n')
libs = {}
# Run otool -D and keep the non-empty lines
vals = run('otool', '-D', filename, output=True).split('\n')
out = []
for val in vals:
if len(val.strip()) > 0:
out.append(val.strip())
assert len(out) > 0, 'Empty output for otool -D %s' % filename
assert len(out) < 3, 'Unexpected otool output: %s' % out
this_soname = None
this_sopath = None
if len(out) == 2:
this_sopath = out[1]
this_soname = P.basename(this_sopath)
for i in range(1, len(lines)):
m = r.search(lines[i])
if m:
sopath = m.group(1)
if this_sopath is not None and this_sopath == sopath:
continue
fidx = sopath.rfind('.framework')
if fidx >= 0:
soname = sopath[sopath.rfind('/', 0, fidx)+1:]
else:
soname = P.basename(sopath)
libs[soname] = sopath
# Identify the absolute RPATH dirs in the current install dir.
# We'll wipe those later. Record the relative ones separately.
abs_rpaths = []
rel_rpaths = []
lines = run('otool', '-l', filename, output=True).split('\n')
for i in range(0, len(lines)):
if re.search('cmd LC_RPATH', lines[i]):
if i+2 >= len(lines):
continue
#print('found LC_RPATH: ' + lines[i+2])
m = re.match('^.*?path\s+([^\s]+)', lines[i+2])
if m:
rpath_val = m.group(1)
if re.search(os.getcwd(), rpath_val):
# Keep only those in current dir, not system
# ones. Not sure about this, but it works.
abs_rpaths.append(rpath_val)
else:
rel_rpaths.append(rpath_val)
return Ret(soname=this_soname, sopath=this_sopath, libs=libs, abs_rpaths=abs_rpaths, rel_rpaths=rel_rpaths)
def required_libs(filename, search_path):
''' Returns a dict where the keys are required SONAMEs and the values are proposed full paths. '''
def linux():
soname = set(readelf(filename).needed)
return dict((k,v) for k,v in ldd(filename, search_path).items() if k in soname)
def osx():
return otool(filename).libs
# What an obfuscated way of saying things where an if statement could
# do the same thing and would also do error checking.
return locals()[get_platform().os]()
def grep(regex, filename):
'''Run a regular expression search inside a file'''
ret = []
rx = re.compile(regex)
with open(filename, 'r') as f:
for line in f:
m = rx.search(line)
if m:
ret.append(m)
return ret
class DistPrefix(str):
'''A class so that, for example, if myobj is an instance of
DistPrefix, myobj.libexec(tool_name) would return
<myobj base path>/libexec/tool_name.
What an obfuscated piece of code. One could as well simply implement
member functions like libexec(toolname) manually rather than doing
the __getattr__ mumbo-jumbo.'''
def __new__(cls, directory):
return str.__new__(cls, P.normpath(directory))
def base(self, *args):
return P.join(self, *args)
def __getattr__(self, name):
def f(*args):
args = [name] + list(args)
return self.base(*args)
return f
def usgscsm_plugin_path(distdir, base):
'''Return the full path to a given USGS CSM plugin.'''
out_path = P.join(distdir, 'plugins', 'usgscsm', base)
return out_path
def rm_f(filename):
''' An rm that does not care if the file is not there '''
try:
remove(filename)
except OSError as o:
if o.errno != errno.ENOENT: # Don't care if it wasn't there
raise
def mergetree(src, dst, copyfunc):
"""Merge one directory into another.
The destination directory may already exist.
If exception(s) occur, an error is raised with a list of reasons.
"""
if not P.exists(dst):
makedirs(dst)
errors = []
for name in listdir(src):
srcname = P.join(src, name)
dstname = P.join(dst, name)
try:
if P.isdir(srcname):
mergetree(srcname, dstname, copyfunc)
else:
copyfunc(srcname, dstname)
except shutil.Error as err:
errors.extend(err.args[0])
except EnvironmentError as why:
errors.append((srcname, dstname, str(why)))
try:
shutil.copystat(src, dst)
except OSError as why:
errors.extend((src, dst, str(why)))
if errors:
raise shutil.Error
def strip(filename):
'''Discard all symbols from this object file with OS specific flags'''
flags = None
def linux_flags():
typ = run('file', filename, output=True)
if typ.find('current ar archive') != -1:
return ['-g']
elif typ.find('SB executable') != -1 or typ.find('SB shared object') != -1:
save_elf_debug(filename)
return ['--strip-unneeded', '-R', '.comment']
elif typ.find('SB relocatable') != -1:
return ['--strip-unneeded']
return []
def osx_flags():
return ['-S']
# Get flags from one of the two functions above then run the strip command.
flags = []
os_type = get_platform().os
if os_type == 'linux':
flags = linux_flags()
elif os_type == 'osx':
flags = osx_flags()
else:
raise Exception('Unknown platform: ' + os_type)
flags.append(filename)
try:
run('strip', *flags)
except Exception as e:
print("Failed running strip with flags: ", flags, ". Got the error: ", e)
def save_elf_debug(filename):
'''Copy the debug information from an ELF file'''
debug = '%s.debug' % filename
try:
run('objcopy', '--only-keep-debug', filename, debug)
run('objcopy', '--add-gnu-debuglink=%s' % debug, filename)
except Exception:
logger.warning('Failed to split debug info for %s' % filename)
if P.exists(debug):
remove(debug)
def set_rpath(filename, toplevel, searchpath, relative_name=True):
'''For each input file, set the rpath to contain all the input
search paths to be relative to the top level.'''
assert not any(map(P.isabs, searchpath)), 'set_rpath: searchpaths must be relative to distdir (was given %s)' % (searchpath,)
def linux():
rel_to_top = P.relpath(toplevel, P.dirname(filename))
#small_path = searchpath[0:1] # truncate this as it can't fit
rpath = '$ORIGIN/../lib'
if run('chrpath', '-r', rpath, filename, raise_on_failure = False) is None:
# TODO: Apparently patchelf is better than chrpath when the
# latter fails. Here, can use instead:
# patchelf --set-rpath ':'.join(rpath) filename
pass
# This warning is too verbose.
#logger.warn('Failed to set_rpath on %s' % filename)
def osx():
info = otool(filename)
# soname is None for an executable
if info.soname is not None:
info.libs[info.soname] = info.sopath
# If we are not using relative paths .. always fix the install name.
if not relative_name:
run('install_name_tool', '-id',
filename, filename)
logger.debug("Trying to Bake %s" % filename)
logger.debug("Info sopath %s" % info.sopath)
logger.debug("Toplevel var %s" % toplevel)
logger.debug("Possible search path %s" % searchpath)
for soname, sopath in info.libs.items():
logger.debug("Soname %s Sopath %s" % (soname, sopath))
# /tmp/build/install/lib/libvwCore.5.dylib
# base = libvwCore.5.dylib
# looks for @executable_path/../lib/libvwCore.5.dylib
# /opt/local/libexec/qt4-mac/lib/QtXml.framework/Versions/4/QtXml
# base = QtXml.framework/Versions/4/QtXml
# looks for @executable_path/../lib/QtXml.framework/Versions/4/QtXml
# OSX rpath points to one specific file, not anything that matches the
# library SONAME. We've already done a whitelist check earlier, so
# ignore it if we can't find the lib we want
# XXX: This code carries an implicit assumption that all
# executables are one level below the root (because
# @executable_path is always the exe path, not the path of the
# current binary like $ORIGIN in linux)
for rpath in searchpath:
if P.exists(P.join(toplevel, rpath, soname)):
new_path = P.join('@rpath', soname)
# If the entry is the "self" one, it has to be
# changed differently
if info.sopath == sopath:
if relative_name:
run('install_name_tool', '-id', new_path, filename)
break
else:
run('install_name_tool', '-change', sopath, new_path, filename)
break
if len(info.libs):
for rpath in searchpath:
exec_rpath = P.join('@executable_path', '..', rpath)
load_rpath = P.join('@loader_path', '..', rpath)
if exec_rpath not in info.rel_rpaths:
if run('install_name_tool', '-add_rpath', exec_rpath, filename, raise_on_failure = False) is None:
logger.warn('Failed to add rpath on %s' % filename)
if load_rpath not in info.rel_rpaths:
if run('install_name_tool', '-add_rpath', load_rpath, filename, raise_on_failure = False) is None:
logger.warn('Failed to add rpath on %s' % filename)
# We'd like to wipe the hard-coded RPATH pointing to the
# original install directory. The user won't have it, and it
# causes problems on the build machine, as libraries are
# loaded from both the new and original locations which
# results in a subtle crash.
for abs_rpath in info.abs_rpaths:
run('install_name_tool', '-delete_rpath', abs_rpath, filename)
# Call one of the two functions above depending on the OS
locals()[get_platform().os]()
def snap_symlinks(src):
'''Build a list of chained symlink files until we reach a non-link file.'''
assert src, 'Cannot snap symlink which is NONE'
assert not P.isdir(src), 'Cannot chase symlinks on a directory'
if not P.islink(src):
return [src]
dst = snap_symlinks(P.join(P.dirname(src), readlink(src)))
return [src] + dst
def fix_install_paths(installdir, arch):
''' After unpacking a set of pre-built binaries, in given directory,
fix any paths to point to the current directory. '''
print('Fixing paths in libtool control files, etc.')
control_files = glob(P.join(installdir,'include','*config.h')) + \
glob(P.join(installdir,'lib','*.la')) + \
glob(P.join(installdir,'lib','*.prl')) + \
glob(P.join(installdir,'lib','*', '*.pc')) + \
glob(P.join(installdir,'bin','*')) + \
glob(P.join(installdir,'mkspecs','*.pri')) + \
list_recursively(P.join(installdir,'share'))
for control in control_files:
# Skip folders and binaries
if os.path.isdir(control): continue
if is_binary(control): continue
print(' %s' % P.basename(control))
# ensure we can read and write (some files have odd permissions)
st = os.stat(control)
os.chmod(control, st.st_mode | stat.S_IREAD | stat.S_IWRITE)
# replace the temporary install directory with the one we're
# deploying to. (Modify file in-place)
lines = []
with open(control,'r') as f:
lines = f.readlines()
with open(control,'w') as f:
for line in lines:
line = re.sub('[\/\.]+[\w\/\.\-]*?' + binary_builder_prefix() + '\w*[\w\/\.]*?/install', installdir, line)
f.write( line )
# Create libblas.la (out of existing libsuperlu.la). We need
# libblas.la to force blas to show up before superlu when linking
# on Linux to avoid a bug with corruption when invoking lapack in
# a multi-threaded environment. A better long-term solution is needed.
superlu_la = installdir + '/lib/libsuperlu.la'
blas_la = installdir + '/lib/libblas.la'
if arch.os == 'linux' and os.path.exists(superlu_la):
lines = []
with open(superlu_la,'r') as f:
lines = f.readlines()
with open(blas_la,'w') as f:
for line in lines:
line = re.sub('libsuperlu', 'libblas', line)
line = re.sub('dlname=\'.*?\'',
'dlname=\'libblas.so\'', line)
line = re.sub('library_names=\'.+?\'',
'library_names=\'libblas.so\'', line)
# Force blas to depend on superlu
line = re.sub('dependency_libs=\'.*?\'',
'dependency_libs=\' -L' + installdir
+ '/lib -lsuperlu -lm\'', line)
f.write( line )
library_ext = ["so"]
if arch.os == 'osx':
library_ext.append("dylib")
# Ensure installdir/bin is in the path, to be able to find chrpath, etc.
if "PATH" not in os.environ: os.environ["PATH"] = ""
os.environ["PATH"] = P.join(installdir, 'bin') + \
os.pathsep + os.environ["PATH"]
SEARCHPATH = [P.join(installdir,'lib'),
P.join(installdir,'lib64')]
print('Fixing RPATHs')
for curr_path in SEARCHPATH:
for extension in library_ext:
for library in glob(P.join(curr_path,'*.'+extension+'*')):
if not is_binary(library):
continue
print(' %s' % P.basename(library))
try:
set_rpath(library, installdir, map(lambda path: P.relpath(path, installdir), SEARCHPATH), False)
except:
print(' Failed %s' % P.basename(library))
print('Fixing binaries')
for binary in glob(P.join(installdir,'bin','*')):
if not is_binary(binary):
continue
print(' %s' % P.basename(binary))
try:
set_rpath(binary, installdir, map(lambda path: P.relpath(path, installdir), SEARCHPATH), False)
except:
print(' Failed %s' % P.basename(binary))
if __name__ == '__main__':
import doctest
doctest.testmod()