def get_dependencies( dylib_path, executable_path=sys.executable ): """ Return the list of dependencies for the given dylib, as listed by `otool -L`. All returned paths will be absolute, and the list may include symbolic links. """ try: otool_output = subprocess.check_output("otool -L " + dylib_path, shell=True) except subprocess.CalledProcessError as ex: sys.stderr.write("Error {} while calling otool -L {}\n".format( ex.returncode, dylib_path ) ) raise dylib_id = read_dylib_id( dylib_path ) raw_rpaths = read_rpaths( dylib_path ) abs_rpaths = map( lambda rpath: rpath.replace( '@loader_path', os.path.split(dylib_path)[0] ), raw_rpaths ) dependencies = [] for line in otool_output.split('\n')[1:]: if not line: continue match = install_name_rgx.match(line) assert match, "Can't parse line: {}".format( line ) dylib_install_name = match.group(1) if dylib_id and dylib_id in dylib_install_name: # Skip the id line for the dylib itself. continue abs_dylib_path = None if dylib_install_name.startswith('@loader_path'): abs_dylib_path = dylib_install_name.replace('@loader_path', os.path.split(dylib_path)[0]) elif dylib_install_name.startswith('@executable_path'): abs_dylib_path = dylib_install_name.replace('@executable_path', os.path.split(executable_path)[0]) elif dylib_install_name.startswith("@rpath"): for abs_rpath in abs_rpaths: possible_abspath = dylib_install_name.replace("@rpath", abs_rpath) if os.path.exists(possible_abspath): abs_dylib_path = possible_abspath break elif os.path.isabs(dylib_install_name): abs_dylib_path = dylib_install_name else: # TODO: We don't yet handle relative paths that don't use @loader_path, @rpath, etc. # For non-absolute install names, we would have to check DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, etc. # This is probably where we should be using the macholib package instead of this custom hack. sys.stderr.write("*** Can't handle relative install-name in {}: {}\n".format( dylib_path, dylib_install_name )) #raise Exception("Can't handle relative install-name in {}: {}\n".format( dylib_path, dylib_install_name )) if not abs_dylib_path or not os.path.exists(abs_dylib_path): sys.stderr.write("*** Dependency of {} does not exist: {}\n".format( dylib_path, dylib_install_name )) continue abs_dylib_path = os.path.normpath(abs_dylib_path) dependencies.append( abs_dylib_path ) return dependencies
def remove_rpath(dylib_path, make_relative_to="loader_path", executable_path=sys.executable): """ For the given dylib, inspect the output of "otool -L" and use install_name_tool to replace all references to @rpath with a new relative path beginning with either @loader_path or @executable_path, as specified by the 'relative_to' parameter. (The correct replacement strings are found by inspecting the LC_RPATH entries in "otool -l" and searching those paths for the referenced libraries.) Lastly, the LC_RPATH entries are deleted. Motivation: What's wrong with keeping @rpath? Nothing, in principle, except for the following minor points: - It isn't supported in old versions of OSX - It adds a level of indirection that can make it tricky to debug linking problems. - I'm not 100% sure that py2app handles @rpath correctly when building a bundle. """ assert make_relative_to in ["loader_path", "executable_path"] try: otool_output = subprocess.check_output("otool -L " + dylib_path, shell=True) except subprocess.CalledProcessError as ex: sys.stderr.write("Error {} while calling otool -L {}".format(ex.returncode, dylib_path)) raise else: dylib_id = read_dylib_id(dylib_path) raw_rpaths = read_rpaths(dylib_path) if raw_rpaths: print("*** Removing rpath from: {}".format(dylib_path)) abs_rpaths = map(lambda rpath: rpath.replace("@loader_path", os.path.split(dylib_path)[0]), raw_rpaths) if make_relative_to == "executable_path": if not os.path.isdir(executable_path): executable_path = os.path.split(executable_path)[0] relative_rpaths = map(lambda rpath: os.path.relpath(rpath, executable_path), abs_rpaths) rpath_replacements = map(lambda rpath: "@executable_path/" + rpath, relative_rpaths) else: relative_rpaths = map(lambda rpath: os.path.relpath(rpath, os.path.split(dylib_path)[0]), abs_rpaths) rpath_replacements = map(lambda rpath: "@loader_path/" + rpath, relative_rpaths) for line in otool_output.split("\n")[1:]: if not line: continue match = install_name_rgx.match(line) assert match, "Can't parse line: {}".format(line) old_install_name = match.group(1) if old_install_name.startswith("@rpath"): if dylib_id and dylib_id in old_install_name: cmd = "install_name_tool -id {} {}".format(os.path.split(dylib_id)[1], dylib_path) print(cmd) subprocess.check_call(cmd, shell=True) continue found_file = False for abs_rpath, rpath_replacement in zip(abs_rpaths, rpath_replacements): new_install_name = old_install_name.replace("@rpath", rpath_replacement) if os.path.exists(abs_rpath): cmd = "install_name_tool -change {} {} {}".format( old_install_name, new_install_name, dylib_path ) print(cmd) subprocess.check_call(cmd, shell=True) found_file = True break if not found_file: raise Exception( "{}, linked from {} does not exist on rpaths: {}".format( old_install_name, dylib_path, raw_rpaths ) ) # Lastly remove the LC_RPATH commands for rpath in raw_rpaths: cmd = "install_name_tool -delete_rpath {} {}".format(rpath, dylib_path) print(cmd) subprocess.check_call(cmd, shell=True)
def remove_rpath(dylib_path, make_relative_to='loader_path', executable_path=sys.executable): """ For the given dylib, inspect the output of "otool -L" and use install_name_tool to replace all references to @rpath with a new relative path beginning with either @loader_path or @executable_path, as specified by the 'relative_to' parameter. (The correct replacement strings are found by inspecting the LC_RPATH entries in "otool -l" and searching those paths for the referenced libraries.) Lastly, the LC_RPATH entries are deleted. Motivation: What's wrong with keeping @rpath? Nothing, in principle, except for the following minor points: - It isn't supported in old versions of OSX - It adds a level of indirection that can make it tricky to debug linking problems. - I'm not 100% sure that py2app handles @rpath correctly when building a bundle. """ assert make_relative_to in ['loader_path', 'executable_path'] try: otool_output = subprocess.check_output("otool -L " + dylib_path, shell=True) except subprocess.CalledProcessError as ex: sys.stderr.write("Error {} while calling otool -L {}".format( ex.returncode, dylib_path)) raise else: dylib_id = read_dylib_id(dylib_path) raw_rpaths = read_rpaths(dylib_path) if raw_rpaths: print("*** Removing rpath from: {}".format(dylib_path)) abs_rpaths = map( lambda rpath: rpath.replace('@loader_path', os.path.split(dylib_path)[0]), raw_rpaths) if make_relative_to == 'executable_path': if not os.path.isdir(executable_path): executable_path = os.path.split(executable_path)[0] relative_rpaths = map( lambda rpath: os.path.relpath(rpath, executable_path), abs_rpaths) rpath_replacements = map(lambda rpath: "@executable_path/" + rpath, relative_rpaths) else: relative_rpaths = map( lambda rpath: os.path.relpath(rpath, os.path.split(dylib_path)[0]), abs_rpaths) rpath_replacements = map(lambda rpath: "@loader_path/" + rpath, relative_rpaths) for line in otool_output.split('\n')[1:]: if not line: continue match = install_name_rgx.match(line) assert match, "Can't parse line: {}".format(line) old_install_name = match.group(1) if old_install_name.startswith("@rpath"): if dylib_id and dylib_id in old_install_name: cmd = "install_name_tool -id {} {}".format( os.path.split(dylib_id)[1], dylib_path) print(cmd) subprocess.check_call(cmd, shell=True) continue found_file = False for abs_rpath, rpath_replacement in zip( abs_rpaths, rpath_replacements): new_install_name = old_install_name.replace( "@rpath", rpath_replacement) if os.path.exists(abs_rpath): cmd = "install_name_tool -change {} {} {}".format( old_install_name, new_install_name, dylib_path) print(cmd) subprocess.check_call(cmd, shell=True) found_file = True break if not found_file: raise Exception( "{}, linked from {} does not exist on rpaths: {}". format(old_install_name, dylib_path, raw_rpaths)) # Lastly remove the LC_RPATH commands for rpath in raw_rpaths: cmd = "install_name_tool -delete_rpath {} {}".format( rpath, dylib_path) print(cmd) subprocess.check_call(cmd, shell=True)