import os import subprocess import shutil import sys import glob def _lib_name(component): return 'libost_%s.dylib' % component def _deps_for_lib(lib, pool, recursive=True): if lib in pool: return otool=subprocess.Popen(['otool', '-L', lib], stdout=subprocess.PIPE) output=otool.communicate()[0] lines=output.split('\n')[1:] for line in lines: d=line.split(' ')[0].strip() if len(d)>0: if d==lib: continue if d not in pool: if d.startswith('/System') or d.startswith('/usr/lib'): continue if recursive: _deps_for_lib(d, pool) pool.add(d) return def collect_deps(stage_dir, components, binaries, site_packages, site_packages_dir): """ Collect the dependencies for the given components and returns a list of frameworks/libraries that the component depends on. """ pool=set() for component in components: lib_name=os.path.abspath(os.path.join(stage_dir, 'lib', _lib_name(component))) if not os.path.exists(lib_name): print 'WARNING:', lib_name, 'does not exist' if lib_name not in pool: _deps_for_lib(lib_name, pool) pool.add(lib_name) for bin in binaries: bin_name=os.path.abspath(os.path.join(stage_dir, 'bin', bin)) if not os.path.exists(bin_name): print 'WARNING:', bin_name, 'does not exist' continue if bin_name not in pool: _deps_for_lib(bin_name, pool) for site_package in site_packages: full_path=get_python_module_path(site_package) print full_path if not os.path.exists(full_path): print 'WARNING:', site_package, 'does not exists' continue if os.path.isdir(full_path): for so_file in glob.glob(os.path.join(full_path, '*.so')): _deps_for_lib(so_file, pool) return pool BINARIES=['gosty', 'chemdict_tool'] COMPONENTS=['mol', 'geom', 'conop', 'gfx', 'gui', 'seq_alg', 'seq', 'img', 'img_alg', 'info', 'io', 'db', 'base'] SCRIPTS=['dng', 'ost'] CHANGE_ID_RPATH='install_name_tool -id @rpath/%s %s' CHANGE_ID='install_name_tool -id @executable_path/%s %s' CHANGE_LOAD_CMD_RPATH='install_name_tool -change %s @rpath/%s %s' CHANGE_LOAD_CMD='install_name_tool -change %s @executable_path/%s %s' ADD_RPATH='install_name_tool -add_rpath %s %s 2> /dev/null' SITE_PACKAGES=['sip.so', 'sipconfig.py', 'sipdistutils.py', 'PyQt4'] REMOVE_HEADERS='rm -rf `find %s/lib -type d -name Headers`' REMOVE_CURRENT='rm -rf `find %s/lib -type d -name Current`' # collect libs of non-standard libraries/frameworks we depend on def copy_binaries(stage_dir, outdir, binary_names, scripts, use_rpath, append_bin=True): exe_path=os.path.abspath(os.path.join(outdir, 'bin')) for binary_name in binary_names: if append_bin: bin_name=os.path.join(stage_dir, 'bin', binary_name) else: bin_name=os.path.join(stage_dir, binary_name) if not os.path.exists(bin_name): print 'WARNING:', binary_name, 'does not exist' continue dst_name=os.path.join(outdir, 'bin', os.path.basename(bin_name)) shutil.copy(bin_name, dst_name) update_load_commands(dst_name, True, use_rpath, exe_path) os.system(ADD_RPATH % ('../lib', dst_name)) for script in scripts: shutil.copy(os.path.join(stage_dir, 'bin', script), os.path.join(outdir,'bin', script)) def split_framework_components(abs_path): """ Splits the path pointing to a dynamic library within a framework '/System/Frameworks/A.framework/Versions/4/A' => ['/System/Frameworks/A.framework', 'Versions/4/A'] """ parts=abs_path.split('/') for i, s in enumerate(parts): if s.endswith('.framework'): lead=os.path.join('/', *parts[:i+1]) trail=os.path.join(*parts[i+1:]) return lead, trail def change_id(id, lib, use_rpath): os.chmod(lib, 0666) if use_rpath: os.system(CHANGE_ID_RPATH % (id,lib)) else: os.system(CHANGE_ID % (id,lib)) os.chmod(lib, 0444) def update_load_commands(lib, exe, use_rpath, exe_path): direct_deps=set() _deps_for_lib(lib, direct_deps, recursive=False) os.chmod(lib, 0666) for direct_dep in direct_deps: if direct_dep.endswith('.dylib'): if use_rpath: new_name=os.path.basename(direct_dep) os.system(CHANGE_LOAD_CMD_RPATH % (direct_dep, new_name, lib)) else: new_name=os.path.join('../lib', os.path.basename(direct_dep)) os.system(CHANGE_LOAD_CMD % (direct_dep, new_name, lib)) else: assert direct_dep.find('.framework/')>=0 if use_rpath: framework_path, rel_path=split_framework_components(direct_dep) framework_name=os.path.basename(framework_path) new_name=os.path.join(framework_name, rel_path) os.system(CHANGE_LOAD_CMD_RPATH % (direct_dep, new_name, lib)) else: framework_path, rel_path=split_framework_components(direct_dep) framework_name=os.path.basename(framework_path) new_name=os.path.join('../lib', framework_name, rel_path) os.system(CHANGE_LOAD_CMD % (direct_dep, new_name, lib)) if use_rpath: os.system(ADD_RPATH % ('.', lib)) if exe: os.chmod(lib, 0555) else: os.chmod(lib, 0444) def copy_deps(dependencies, outdir, use_rpath): exe_path=os.path.join(outdir, 'bin') for dep in dependencies: if dep.endswith('.dylib'): dst_name=os.path.join(outdir, 'lib', os.path.basename(dep)) if not os.path.exists(dep): continue shutil.copy(dep, dst_name) if use_rpath: change_id(os.path.basename(dep), dst_name, use_rpath) else: change_id('../lib/%s' % os.path.basename(dep), dst_name, use_rpath) update_load_commands(dst_name, False, use_rpath, exe_path) else: assert dep.find('.framework/')>=0 framework_path, rel_path=split_framework_components(dep) framework_name=os.path.basename(framework_path) dst_name=os.path.join(outdir, 'lib', framework_name) shutil.copytree(framework_path, dst_name) if use_rpath: change_id(os.path.join(dst_name, rel_path), os.path.join(dst_name, rel_path), use_rpath) else: change_id(os.path.join('../lib', framework_name, rel_path), os.path.join(dst_name, rel_path), use_rpath) os.unlink(os.path.join(dst_name, os.path.splitext(framework_name)[0])) update_load_commands(os.path.join(dst_name, rel_path), False, use_rpath, exe_path) def update_pymod_shared_objects(args, path, files): lib_path, use_rpath=args exe_path=os.path.abspath(os.path.join(lib_path, '../bin')) for f in files: if not os.path.exists(os.path.join(path, f)): continue base, ext=os.path.splitext(f) if ext=='.so': path_to_lib_path=os.path.relpath(lib_path, path) abs_name=os.path.join(path, f) os.system(ADD_RPATH % (path_to_lib_path, abs_name)) update_load_commands(abs_name, False, use_rpath, exe_path) elif ext in ('.pyc', '.pyo'): os.unlink(os.path.join(path, f)) def get_site_package_dir(): """ Get site-package directory of this python installation. This assumes that ost was linked against the same version of Python """ for p in sys.path: pattern='/site-packages' index=p.find(pattern) if index>=0: return p[:index+len(pattern)] raise RuntimeError("Couldn't determine site-packages location") def get_python_module_path(module): for path in sys.path: full_path=os.path.join(path, module) if os.path.exists(full_path): return full_path return None def get_python_home(): """ Derive Python home by looking at the location of the os module """ return os.path.dirname(sys.modules['os'].__file__) def check_install_name_tool_capabilities(): """ Find out whether install_name_tool supports the add_rpath option. """ inst_name_tool=subprocess.Popen('install_name_tool', shell=True, stderr=subprocess.PIPE) output=inst_name_tool.communicate()[1] return output.find('-add_rpath')!=-1 def make_standalone(stage_dir, outdir, no_includes, force_no_rpath=False, macports_workaround=False): site_packages=get_site_package_dir() # figure out if install_name_tool supports the -add_rpath option. use_rpath=True if not check_install_name_tool_capabilities(): print "install_name_tool doesn't support the -add_rpath option." print "I will fallback to the more arcane @executable_path" use_rpath=False elif force_no_rpath: print "I will use the arcane @executable_path" use_rpath=False if os.path.exists(outdir): shutil.rmtree(outdir) os.system('mkdir -p "%s"' % outdir) os.system('mkdir -p "%s/lib"' % outdir) os.system('mkdir -p "%s/bin"' % outdir) print 'copying shared datafiles' shutil.copytree(os.path.join(stage_dir, 'share'), os.path.join(outdir, 'share')) print 'collecting dependencies' deps=collect_deps(stage_dir, COMPONENTS, BINARIES, SITE_PACKAGES, site_packages) print 'copying dependencies' copy_deps(deps, outdir, use_rpath) print 'copying binaries' copy_binaries(stage_dir, outdir, BINARIES, SCRIPTS, use_rpath) print 'copying pymod' shutil.copytree(os.path.join(stage_dir, 'lib/openstructure'), os.path.join(outdir, 'lib/openstructure')) copied_py_framework=False non_std_python=False if os.path.exists(os.path.join(outdir, 'lib/Python.framework')): framework_path=os.path.join(outdir, 'lib/Python.framework') nonstd_python=True copied_py_framework=True if len(glob.glob(os.path.join(outdir, 'lib', 'libpython*')))>0: non_std_python=True if non_std_python: print 'looks like we are using a non-standard python.' python_home=get_python_home() if not copied_py_framework: print 'also copying python modules from %s' % python_home modules_dst=os.path.join(outdir, 'lib', os.path.basename(python_home)) shutil.copytree(python_home, modules_dst) if os.path.exists(os.path.join(modules_dst, 'site-packages')): shutil.rmtree(os.path.join(modules_dst, 'site-packages')) copy_binaries(os.path.join(python_home, '../..'), outdir, ['python'], [], use_rpath) python_bin=os.path.abspath(os.path.join(python_home, '../../bin/python')) else: # For MacPorts it's even more involved. Python is not executed directly # but rather uses a wrapper executable that calls the actual python exe. # We have to include that one into the bundle. if macports_workaround: path_to_app='../../Resources/Python.app/Contents/MacOS/' exe_path=os.path.join(python_home, path_to_app) copy_binaries(exe_path, outdir, ['python'], [], use_rpath, append_bin=False) python_bin=os.path.join('/opt/local/bin/python') else: copy_binaries(os.path.join(python_home, '../..'), outdir, ['python'], [], use_rpath) python_bin=os.path.abspath(os.path.join(python_home, '../../bin/python')) # remove all versions but the one we are using version_string=sys.version[0:3] prefix, postfix=split_framework_components(python_home) site_packages_dir=os.path.join(outdir, 'lib', 'Python.framework', postfix, 'site-packages') if os.path.exists(site_packages_dir): shutil.rmtree(site_packages_dir) for directory in glob.glob(os.path.join(framework_path, 'Versions/*')): if os.path.basename(directory)!=version_string: shutil.rmtree(directory) # replace the python executable ost_script=os.path.join(outdir, 'bin', 'ost') os.chmod(ost_script, 0666) script=''.join(open(ost_script, 'r').readlines()) script=script.replace(python_bin, '$BIN_DIR/python') open(ost_script, 'w').write(script) os.chmod(ost_script, 0555) elif use_rpath==False: print 'BIG FAT WARNING: Creating bundle with @executable_path and default' print 'Python might not work. Test carefully before deploying.' if no_includes: os.system(REMOVE_HEADERS % outdir) os.system(REMOVE_CURRENT % outdir) print 'copying site-packages' for sp in SITE_PACKAGES: src=get_python_module_path(sp) if os.path.isdir(src): shutil.copytree(src, os.path.join(outdir, 'lib/openstructure', sp)) else: shutil.copy(src, os.path.join(outdir, 'lib/openstructure', sp)) print 'updating link commands of python shared objects' os.path.walk(os.path.join(outdir, 'lib'), update_pymod_shared_objects, (os.path.join(outdir, 'lib'), use_rpath))