#! /usr/bin/env python3

import sys
import re
import shutil

from pathlib import Path
from subprocess import check_output, check_call


def parse_ldd_line(line: str):
    line = line.strip()
    line = re.sub(r" *\(0x[0-9a-fA-F]*\)", "", line)
    parts = line.split("=>")
    name = Path(parts[0].strip()).name
    if name not in exlude_list and len(parts) == 2:
        return name, Path(parts[1].strip()).resolve()


def collect_dependencies(target):
    target = Path(target)

    def collect():
        for line in check_output(["ldd", target]).decode("utf-8").splitlines():
            dep = parse_ldd_line(line)
            if dep is not None:
                yield dep

    return list(collect())

def make_python_portable(interpreter):
    interpreter = Path(interpreter)
    bin_dir = interpreter.parent
    root_dir = bin_dir.parent
    lib_dir = root_dir / "lib"

    # Patch interpreter to find all libraries
    check_call(["patchelf", "--set-rpath", "$ORIGIN/../lib", interpreter])

    # Install all dependencies
    targets = [
        interpreter,
        *list(Path(lib_dir / (interpreter.resolve().name)).glob("**/*.so"))
    ]

    deps = []
    for target in targets:
        deps += collect_dependencies(target)
    for name, path in deps:
        if not root_dir in path.parents:
            shutil.copy2(str(path), str(lib_dir / name))

    # Patch targets and dependencies to find each other
    for target in targets[1:]:
        num_parents = len(target.relative_to(lib_dir).parts) - 1
        rpath = Path("$ORIGIN")
        for _ in range(num_parents):
            rpath /= ".."
        check_call(["patchelf", "--set-rpath", str(rpath), target])
    for name, path in deps:
        check_call(["patchelf", "--set-rpath", "$ORIGIN", str(lib_dir / name)])

# Retrieved: 2021-11-18
appimage_exclude_list = [
    "ld-linux.so.2",
    "ld-linux-x86-64.so.2",
    "libanl.so.1",
    "libBrokenLocale.so.1",
    "libcidn.so.1",
    "libc.so.6",
    "libdl.so.2",
    "libm.so.6",
    "libmvec.so.1",
    "libnss_compat.so.2",
    "libnss_dns.so.2",
    "libnss_files.so.2",
    "libnss_hesiod.so.2",
    "libnss_nisplus.so.2",
    "libnss_nis.so.2",
    "libpthread.so.0",
    "libresolv.so.2",
    "librt.so.1",
    "libthread_db.so.1",
    "libutil.so.1",
    "libstdc++.so.6",
    "libGL.so.1",
    "libEGL.so.1",
    "libGLdispatch.so.0",
    "libGLX.so.0",
    "libOpenGL.so.0",
    "libdrm.so.2",
    "libglapi.so.0",
    "libgbm.so.1",
    "libxcb.so.1",
    "libX11.so.6",
    "libgio-2.0.so.0",
    "libasound.so.2",
    "libfontconfig.so.1",
    "libthai.so.0",
    "libfreetype.so.6",
    "libharfbuzz.so.0",
    "libcom_err.so.2",
    "libexpat.so.1",
    "libgcc_s.so.1",
    "libglib-2.0.so.0",
    "libgpg-error.so.0",
    "libICE.so.6",
    "libp11-kit.so.0",
    "libSM.so.6",
    "libusb-1.0.so.0",
    "libuuid.so.1",
    "libz.so.1",
    "libgobject-2.0.so.0",
    "libpangoft2-1.0.so.0",
    "libpangocairo-1.0.so.0",
    "libpango-1.0.so.0",
    "libgpg-error.so.0",
    "libjack.so.0",
    "libxcb-dri3.so.0",
    "libxcb-dri2.so.0",
    "libfribidi.so.0",
    "libgmp.so.10",
]

exlude_list = [
    *appimage_exclude_list,
    "linux-vdso.so.1",
]

if __name__ == "__main__":
    if len(sys.argv) >= 2:
        make_python_portable(sys.argv[1])
    else:
        raise SystemExit(1)