"""
copy_runtime_dependencies.py
This script recursively copies a given shared library (or binary) and its runtime
dependencies into a specified destination directory. It resolves and rewrites
paths as needed to ensure the binary and its dependencies can run standalone
from the destination directory.
Supported platforms:
- macOS: Uses otool and install_name_tool to resolve and fix @rpath dependencies.
- Linux: Uses ldd and patchelf to rewrite rpaths.
- Windows: Uses dumpbin (requires Visual Studio tools) to find DLL dependencies.
Usage:
python copy_runtime_dependencies.py <target_binary> <destination_directory>
Arguments:
<target_binary> Path to the binary or shared library to process.
<destination_directory> Directory where the binary and dependencies will be copied.
Note:
- On macOS, this script ensures all libraries are rewritten with `@loader_path`.
- On Linux, the rpath of copied libraries is set to `$ORIGIN`.
- On Windows, it attempts to find dependent DLLs in the PATH or next to the binary.
Dependencies:
- macOS: install_name_tool, otool
- Linux: ldd, patchelf
- Windows: dumpbin (from Visual Studio command line tools)
"""
import os
import sys
import shutil
import subprocess
def get_dependencies(lib_path):
"""Returns a list of dependent shared libraries for a given library."""
if sys.platform == "darwin":
result = subprocess.run(["otool", "-L", lib_path], capture_output=True, text=True)
lines = result.stdout.split("\n")[1:]
dependencies = []
for line in lines:
parts = line.strip().split(" ")
if parts:
dep_path = parts[0]
if not dep_path.startswith("/usr/lib") and not dep_path.startswith("/System/"):
dependencies.append(dep_path)
return dependencies
elif sys.platform.startswith("linux"):
result = subprocess.run(["ldd", lib_path], capture_output=True, text=True)
dependencies = []
for line in result.stdout.splitlines():
if "=>" in line:
parts = line.strip().split("=>")
if len(parts) == 2:
dep = parts[1].split()[0]
dependencies.append(dep)
return dependencies
elif sys.platform.startswith("win"):
result = subprocess.run(["dumpbin", "/DEPENDENTS", lib_path], capture_output=True, text=True)
dependencies = []
for line in result.stdout.splitlines():
line = line.strip()
if line.lower().endswith(".dll"):
if not line.lower().startswith("c:\\windows"):
dependencies.append(line)
return dependencies
else:
print("Unsupported platform for dependency resolution.")
return []
def resolve_rpath(lib_path, dep):
"""Resolves dependency paths for @rpath-like references or similar."""
if sys.platform == "darwin":
result = subprocess.run(["otool", "-l", lib_path], capture_output=True, text=True)
lines = result.stdout.split("\n")
rpaths = []
for i in range(len(lines)):
if "LC_RPATH" in lines[i]:
path_line = lines[i + 2].strip().split("path ")[-1].split(" (offset")[0]
rpaths.append(path_line)
for rpath in rpaths:
possible_path = os.path.join(rpath, os.path.basename(dep))
if os.path.exists(possible_path):
return possible_path
return None
elif sys.platform.startswith("linux"):
return dep
elif sys.platform.startswith("win"):
lib_dir = os.path.dirname(lib_path)
possible_path = os.path.join(lib_dir, dep)
if os.path.exists(possible_path):
return possible_path
else:
search_paths = os.environ['PATH'].split(os.pathsep)
for path in search_paths:
possible_path = os.path.join(path, dep)
if (possible_path.lower().startswith("c:\\windows") or
possible_path.lower().startswith("c:\\windows\\system32") or
"api-ms-" in possible_path.lower() or
"vcruntime" in possible_path.lower() or
"msvcp" in possible_path.lower()):
continue
if os.path.exists(possible_path):
return possible_path
return None
else:
return None
def copy_and_fix_dependencies(source_lib, dest_dir):
"""Recursively copies the library and its dependencies to dest_dir and fixes paths."""
if sys.platform == "darwin":
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
lib_dir = os.path.dirname(source_lib)
copied_libs = {}
queue = [source_lib]
processed_libs = set()
copied_lib_paths = set()
resolved_deps = set()
while queue:
lib_path = queue.pop()
if lib_path in processed_libs or lib_path in resolved_deps or not os.path.isfile(lib_path):
continue
lib_name = os.path.basename(lib_path)
dest_path = os.path.join(dest_dir, lib_name)
if not os.path.exists(lib_path):
alternative_path = os.path.join(lib_dir, lib_name)
if os.path.exists(alternative_path) and os.path.isfile(alternative_path):
print(f"🔍 Found missing @rpath library: {lib_path} -> {alternative_path}")
lib_path = alternative_path
else:
print(f"⚠️ Warning: {lib_path} not found. Trying to resolve @rpath.")
resolved_path = resolve_rpath(source_lib, lib_path)
if resolved_path:
print(f"✅ Resolved {lib_path} -> {resolved_path}")
lib_path = resolved_path
else:
print(f"❌ Could not resolve {lib_path}. Skipping.")
continue
try:
shutil.copy2(lib_path, dest_path)
os.chmod(dest_path, 0o755)
copied_lib_paths.add(lib_path)
except PermissionError:
print(f"⚠️ Permission denied when copying {lib_path}. Try running with sudo.")
continue
copied_libs[lib_path] = dest_path
processed_libs.add(lib_path)
resolved_deps.add(lib_path)
dependencies = get_dependencies(lib_path)
for dep in dependencies:
if dep.startswith("@rpath"):
resolved_dep = resolve_rpath(lib_path, dep)
if resolved_dep:
print(f"🔄 Resolved @rpath dependency: {dep} -> {resolved_dep}")
if resolved_dep not in resolved_deps and resolved_dep not in processed_libs and resolved_dep not in copied_lib_paths:
queue.append(resolved_dep)
else:
print(f"⚠️ Could not resolve @rpath dependency: {dep}")
else:
if dep not in resolved_deps and dep not in processed_libs and dep not in copied_lib_paths:
queue.append(dep)
for original_path, new_path in copied_libs.items():
new_name = os.path.basename(new_path)
subprocess.run(["install_name_tool", "-id", f"@loader_path/{new_name}", new_path])
for dep in get_dependencies(new_path):
dep_name = os.path.basename(dep)
if dep in copied_libs:
new_dep_path = f"@loader_path/{dep_name}"
print(f"🔄 Updating dependency: {new_path} -> {new_dep_path}")
subprocess.run(["install_name_tool", "-change", dep, new_dep_path, new_path])
elif dep.startswith("@rpath"):
resolved_dep = resolve_rpath(original_path, dep)
if resolved_dep and os.path.exists(resolved_dep) and os.path.isfile(resolved_dep):
print(f"🔄 Replacing @rpath: {new_path} -> {resolved_dep}")
subprocess.run(["install_name_tool", "-change", dep, f"@loader_path/{os.path.basename(resolved_dep)}", new_path])
print("✅ All dependencies copied and fixed successfully.")
elif sys.platform.startswith("linux"):
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
lib_dir = os.path.dirname(source_lib)
copied_libs = {}
queue = [source_lib]
processed_libs = set()
while queue:
lib_path = queue.pop()
if lib_path in processed_libs or not os.path.isfile(lib_path):
continue
lib_name = os.path.basename(lib_path)
dest_path = os.path.join(dest_dir, lib_name)
if not os.path.exists(lib_path):
alternative_path = os.path.join(lib_dir, lib_name)
if os.path.exists(alternative_path) and os.path.isfile(alternative_path):
print(f"Found missing library: {lib_path} -> {alternative_path}")
lib_path = alternative_path
else:
print(f"Warning: {lib_path} not found. Skipping.")
continue
try:
shutil.copy2(lib_path, dest_path)
os.chmod(dest_path, 0o755)
except PermissionError:
print(f"Permission denied when copying {lib_path}.")
continue
copied_libs[lib_path] = dest_path
processed_libs.add(lib_path)
dependencies = get_dependencies(lib_path)
for dep in dependencies:
if dep not in processed_libs:
queue.append(dep)
for original_path, new_path in copied_libs.items():
subprocess.run(["patchelf", "--set-rpath", "$ORIGIN", new_path])
print("✅ All dependencies copied and fixed successfully on Linux.")
elif sys.platform.startswith("win"):
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
lib_dir = os.path.dirname(source_lib)
copied_libs = {}
queue = [source_lib]
processed_libs = set()
while queue:
lib_path = queue.pop()
if lib_path in processed_libs or not os.path.isfile(lib_path):
continue
lib_name = os.path.basename(lib_path)
dest_path = os.path.join(dest_dir, lib_name)
if not os.path.exists(lib_path):
alternative_path = os.path.join(lib_dir, lib_name)
if os.path.exists(alternative_path) and os.path.isfile(alternative_path):
print(f"Found missing DLL: {lib_path} -> {alternative_path}")
lib_path = alternative_path
else:
print(f"Warning: {lib_path} not found. Skipping.")
continue
try:
shutil.copy2(lib_path, dest_path)
except PermissionError:
print(f"Permission denied when copying {lib_path}.")
continue
copied_libs[lib_path] = dest_path
processed_libs.add(lib_path)
dependencies = get_dependencies(lib_path)
for dep in dependencies:
resolved_dep = resolve_rpath(lib_path, dep)
if resolved_dep and resolved_dep not in processed_libs:
queue.append(resolved_dep)
print("All dependencies copied successfully on Windows. You may need to add the destination directory to your PATH.")
else:
print("Unsupported platform.")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python copy_runtime_dependencies.py <target_binary> <destination_directory>", file=sys.stderr)
sys.exit(1)
target_binary = sys.argv[1]
destination_directory = sys.argv[2]
copy_and_fix_dependencies(target_binary, destination_directory)