#!/usr/bin/env python3
"""
Installation script generated from a Bazel `install` target.
"""
# Note(storypku):
#   Adapted from https://github.com/RobotLocomotion/drake/blob/master/tools/install/install.py.in

# N.B. This is designed to emulate CMake's install mechanism. Do not add
# unnecessary print statements.

import argparse
import collections
import filecmp
import itertools
import hashlib
import os
import re
import json
import shutil
import stat
import sys
import subprocess

import xml.etree.ElementTree as ET

from pathlib import Path
from subprocess import check_output, check_call

# Stores subdirectories that have already been created.
subdirs = set()
# Stored from command-line.
color = False
prefix = None
strip = True
strip_tool = None
install_lib = True
fix_rpath = True

# dbg = False
# gpu = False
# dev = True

deprecated_package_prefix = "packages"

# Mapping used to (a) check for unique shared library names and (b) provide a
# mapping from library name to paths for RPath fixes (where (a) is essential).
# Structure: Map[ basename (Str) => full_path ]
libraries_to_fix_rpath = {}
# These are binaries (or Python shared libraries) that require RPath fixes (and
# thus depend on `libraries_to_fix_rpath`), but by definition are not depended
# upon by other components, and thus need not be unique.
# Structure: List[ Tuple(basename, full_path) ]
binaries_to_fix_rpath = []
# Files that are not libraries, but may still require fixing.
# Structure: List[Str]
potential_binaries_to_fix_rpath = []
# Stores result of `--list` argument.
list_only = False
# Used for matching against libraries and extracting useful components.
# N.B. On linux, dynamic libraries may have their version number as a suffix
# (e.g. my_lib.so.x.y.z).
dylib_match = re.compile(r"(.*\.so)(\.\d+)*$")

workspace = os.getenv("PWD", "/apollo_workspace").split("/.cache/")[0]
lib_cache_prefix = os.path.join(workspace, "dev", "install", "apollo")

meta = []
export_library_dict = {}
packages = {}
plugins = {}
plugins_meta = {}
meta_prefix = "share/packages"
EMPTY_BUILD_TMP = '''
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_import")
cc_library(
    name = "{}",
    hdrs = glob(["include/{}/**/*.h"]) + glob(["include/{}/**/*.hpp"]),
    srcs = [],
    strip_include_prefix = "include",
    visibility = ["//visibility:public"],
)
'''
BUILD_TMP = '''
cc_library(
    name = "{}",
    srcs = ["{}"],
    linkopts = [{}],
    visibility = ["//visibility:public"],
    alwayslink = True,
)
'''

def generate_pack_file(meta_path):
    global plugins_meta
    cyberfile_path = os.path.join(meta_path, "cyberfile.xml")
    if not os.path.exists(cyberfile_path):
        return
    cyberfile_parser = ET.parse(cyberfile_path)
    root = cyberfile_parser.getroot()

    package_name = root.find("name").text
    version = "@REPLACE@"
    arch = check_output(["uname", "-m"]).decode("utf-8").replace("\n", "")
    if arch == "x86_64":
        arch = "amd64"
    elif arch == "aarch64":
        arch = "arm64"
    description = package_name
    cyberfile_deps = root.iterfind("depend")
    deps = []
    for dep in cyberfile_deps:
        deps.append(dep.text)

    bin_files = []
    with open(os.path.join(meta_path, "meta.txt")) as f:
        contents = f.read().split("\n")
        src_path = (contents[-1].split(":"))[-1]
        # process binary file
        for f in contents:
            c = f.split(":")[-1]
            if c.startswith("bin"):
                bin_files.append(c)

    config_path = os.path.join(prefix, "share", src_path)
    binaries = ["{}/{}".format(prefix, f) for f in bin_files]
    include_path = os.path.join(prefix, "include", src_path)
    library_path = os.path.join(prefix, "lib", src_path)
    python_path = os.path.join(prefix, "python", src_path)
    source_path = os.path.join(prefix, "src", src_path)
    meta_package_path = os.path.join(prefix, "share/packages", package_name)

    process_file_path = [
        config_path, include_path,
        library_path, python_path, source_path,
        meta_package_path] + binaries

    if package_name in plugins_meta:
        for plugin_info in plugins_meta[package_name]:
            plugin_lib_path = os.path.join(prefix,
                "lib", plugin_info["plugin_src_path"])
            cyber_plugin_index_file = os.path.join(
                prefix, "share/cyber_plugin_index",
                plugins[os.path.join(
                    "share", plugin_info["plugin_src_path"],
                    plugin_info["description_file_name"])])
            process_file_path.append(plugin_lib_path)
            process_file_path.append(cyber_plugin_index_file)

    preinst_extend_ops = []
    postinst_extend_ops = []
    for r, _, files in os.walk(config_path):
        for f in files:
            conf_src = os.path.join(r, f)
            conf_dst = os.path.join("/apollo", os.path.join(r, f).replace(
                            os.path.join(prefix, "share") + "/", ""))
            conf_base = os.path.abspath(os.path.dirname(conf_dst))

            shell_script = "if [ ! -e '{dst}' ]; then " \
                           "mkdir -p '{base}' && ln -snf '{src}' '{dst}'; " \
                           "fi".format(src=conf_src, dst=conf_dst, base=conf_base)
            postinst_extend_ops.append(shell_script)
    prerm_extend_ops = []
    for i in process_file_path:
        prerm_extend_ops.append("rm -rf {}".format(i))
    postrm_extend_ops = []
    data = []
    for i in process_file_path:
        if os.path.exists(i):
            data.append({"src": i, "des": i})

    content = {
        "name": package_name,
        "ver": version,
        "arch": arch,
        "description": "Apollo {} module.".format(package_name),
        "deps": deps,
        "preinst_extend_ops": preinst_extend_ops,
        "postinst_extend_ops": postinst_extend_ops,
        "prerm_extend_ops": prerm_extend_ops,
        "postrm_extend_ops": postrm_extend_ops,
        "data": data,
        "type": "neo"
    }

    with open(os.path.join(meta_path, "pack.json"), "w+") as f:
        f.write(json.dumps(content, indent=4))


# def get_pkg_real_name(name, dev=False, dbg=False, gpu=False):
#     """Get real package name by install parameters"""
#     new_name = name
#     if dev:
#         new_name += "-dev"
#     if dbg:
#         new_name += "-dbg"
#     if gpu:
#         new_name += "-gpu"
#     return new_name


def rename_package_name(dest):
    """Get packages name from file install destination."""
    # if not dev and not dbg and not gpu:
    #     return dest
    # if dest.startswith("lib/") or dest.startswith("share/"):
    #     return dest
    curr_pkg_name = dest.split("/")[0]
    # new_pkg_name = get_pkg_real_name(curr_pkg_name, dev, dbg, gpu)

    # Local build package version is fiexed `local`
    pkg_name_with_ver = deprecated_package_prefix + "/" + curr_pkg_name + "/local"
    new_dest = dest.replace(curr_pkg_name, pkg_name_with_ver, 1)

    # Install ${package_name}.BUILD to ${new_package_name}.BUILD
    # if dest == curr_pkg_name + "/" + curr_pkg_name + ".BUILD":
    #     new_dest = new_pkg_name + "/local/" + new_pkg_name + ".BUILD"

    return new_dest


def is_relative_link(filepath):
    """Find if a file is a relative link.

    Bazel paths are assumed to always be absolute. If path is not absolute,
    the file is a link we want to keep.

    If the given `filepath` is not a link, the function returns `None`. If the
    given `filepath` is a link, the result will depend if the link is absolute
    or relative. The function is called recursively. If the result is not a
    link, `None` is returned. If the link is relative, the relative link is
    returned.
    """
    if os.path.islink(filepath):
        link = os.readlink(filepath)
        if not os.path.isabs(link):
            return link
        else:
            return is_relative_link(link)
    else:
        return None


def find_binary_executables():
    """Finds installed files that are binary executables to fix them up later.

    Takes `potential_binaries_to_fix_rpath` as input list, and updates
    `binaries_to_fix_rpath` with executables that need to be fixed up.
    """
    if not potential_binaries_to_fix_rpath:
        return
    # Checking file type with command `file` is the safest way to find
    # executables. Files without an extension are likely to be executables, but
    # it is not always the case.
    file_output = check_output(
        ["file"] + potential_binaries_to_fix_rpath).decode("utf-8")
    # On Linux, executables can be ELF shared objects.
    executable_match = re.compile(
        r"(.*):.*(ELF.*executable|shared object.*)")
    for line in file_output.splitlines():
        re_result = executable_match.match(line)
        if re_result is not None:
            dst_full = re_result.group(1)
            basename = os.path.basename(dst_full)
            binaries_to_fix_rpath.append((basename, [dst_full]))


def may_be_binary(dst_full):
    # Try to minimize the amount of work that `find_binary_executables`
    # must do.
    extensions = [".h", ".py", ".obj", ".cmake", ".1", ".hpp", ".txt"]
    for extension in extensions:
        if dst_full.endswith(extension):
            return False
    return True

def create_cache(src):
    global lib_cache_prefix

    cache = os.path.join(lib_cache_prefix, src)
    if not os.path.exists(cache):
        os.makedirs(os.path.dirname(cache), exist_ok=True)
        with open(cache, "w+") as f:
            sha1 = hashlib.sha1()
            with open(src, "rb") as t:
                data = t.read()
                sha1.update(data)
            f.write(sha1.hexdigest())

def cachecmp(src, cache):
    with open(cache, "r+") as f:
        old_sha1 = f.read()
        new_sha1 = hashlib.sha1()
        with open(src, "rb") as t:
            data = t.read()
            new_sha1.update(data)
            new_sha1_text = new_sha1.hexdigest()
        if old_sha1 == new_sha1_text:
            return True
        f.seek(0)
        f.truncate(0)
        f.write(new_sha1_text)
        return False

def needs_install(src, dst):
    global lib_cache_prefix
    co_dev = os.getenv("CO_DEV", 0)

    if co_dev:
        # always return true in co_dev
        return True

    if os.path.basename(dst) == "cyberfile.xml":
        # data file -> installation needed.
        return True
    # Get canonical destination.
    dst_full = os.path.join(prefix, dst)

    # Check if destination exists.
    if not os.path.exists(dst_full):
        # Destination doesn't exist -> installation needed.
        create_cache(src)  
        return True
    
    cache = os.path.join(lib_cache_prefix, src)
    if not os.path.exists(cache):
        create_cache(src)
        return True

    # Check if files are different.
    if cachecmp(src, cache):
        # Files are the same -> no installation needed.
        return False

    # File needs to be installed.
    return True


def copy_or_link(src, dst):
    """Copy file if it is not a relative link or recreate the symlink in `dst`.

    Copy the input file to the destination if it is not a relative link. If the
    file is a relative link, create a similar link in the destination folder.
    """
    if not Path(src).exists():
        return
    relative_link = is_relative_link(src)
    if relative_link:
        if Path(dst).exists() or Path(dst).is_symlink():
            os.unlink(dst)
        os.symlink(relative_link, dst)
    else:
        shutil.copy2(src, dst)


def install(src, dst, action_type=None, package_path=None,
        shared_library_export=None, target_name=None):
    global subdirs
    global meta

    deprecated_flag = True

    # deprecated package install path
    if action_type is not None and package_path is not None:
        global packages
        global export_library_dict

        package_in_cache = False
        deprecated_flag = False

        for k in packages:
            if package_path.startswith(k):
                package_in_cache = True
                packages[k].append("{}:{}".format(src, dst))
                break

        if not package_in_cache:
            find_flag = False
            src_list = src.split("/")
            for i in range(0, len(src_list)):
                temp_path = "/".join(src_list[0: i])
                temp_cyberfile_path = os.path.join(temp_path, "cyberfile.xml")
                if os.path.exists(temp_cyberfile_path):
                    packages[temp_path] = ["{}:{}".format(src, dst)]
                    find_flag = True
                    break
            if not find_flag:
                print("\033[31m[ERROR]\033[0m orphan install file: {} -> {}".format(
                    src, dst), file=sys.stderr)
                exit(-1)

        if shared_library_export is not None and target_name is not None:
            if package_path not in export_library_dict:
                export_library_dict[package_path] = [{"target": target_name, "dst": dst}]
            else:
                export_library_dict[package_path].append({"target": target_name, "dst": dst})
    else:
        dst = rename_package_name(dst)

    if legacy:
        if not dst.startswith("lib/"):
            dst = src

    if not deprecated_flag and src.endswith("cyberfile.xml"):
        module_src_path = src.replace("/cyberfile.xml", "")
        cyberfile_parser = ET.parse(src)
        root = cyberfile_parser.getroot()

        package_name = root.find("name").text
        dst = "{}/{}/cyberfile.xml".format(meta_prefix, package_name)

    elif not deprecated_flag and dst.startswith("plugin_meta"):
        global plugins_meta
        path_list = src.replace("/plugins.xml", "").split("/")
        cyberfile_src_path = None
        for i in range(1, len(path_list)+1):
            temp_path = "/".join(path_list[: i])
            if os.path.exists(os.path.join(temp_path, "cyberfile.xml")):
                cyberfile_src_path = os.path.join(temp_path, "cyberfile.xml")
                break
        if cyberfile_src_path is None:
            print("\033[31m[ERROR]\033[0m missing package info of {}".format(src), file=sys.stderr)
            exit(-1)
        cyberfile_parser = ET.parse(cyberfile_src_path)
        root = cyberfile_parser.getroot()

        package_name = root.find("name").text
        plugin_src_path = dst.split("@")[1]
        description_file_name = dst.split("@")[-1]

        if package_name not in plugins_meta:
            plugins_meta[package_name] = [{
                "plugin_src_path": plugin_src_path,
                "description_file_name": description_file_name
            }]
        else:
            plugins_meta[package_name].append({
                "plugin_src_path": plugin_src_path,
                "description_file_name": description_file_name
            })

        return
        # dst = "{}/{}/{}/{}".format(
        #     meta_prefix, package_name, plugin_meta,
        #     "".format(plugin_src_path, description_file_name))

    # Do not install files in ${prefx}/lib dir
    # if not install_lib and dst.startswith("lib/"):
    #     return
    # In list-only mode, just display the filename, don't do any real work.
    if list_only:
        print(dst, action_type)
        return

    # Ensure destination subdirectory exists, creating it if necessary.
    subdir = os.path.dirname(dst)
    if subdir not in subdirs:
        subdir_full = os.path.join(prefix, subdir)
        if not os.path.exists(subdir_full):
            os.makedirs(subdir_full)
        subdirs.add(subdir)

    dst_full = os.path.join(prefix, dst)
    # Install file, if not up to date.
    if needs_install(src, dst):
        print("-- Installing: {}".format(dst_full))
        if os.path.exists(dst_full):
            os.remove(dst_full)
        copy_or_link(src, dst_full)
    else:
        # TODO(eric.cousineau): Unclear how RPath-patched file can be deemed
        # "up-to-date" by comparison?
        print("-- Up-to-date: {}".format(dst_full))
        # No need to check patching.
        return
    basename = os.path.basename(dst)
    if re.match(dylib_match, basename):  # It is a library.
        #TODO(lanyongshun): interim method
        if "python" in dst and not basename.startswith("lib"):
            # Assume this is a Python C extension.
            binaries_to_fix_rpath.append((basename, [dst_full]))
        else:
            # Check that dependency is only referenced once
            # in the library dictionary. If it is referenced multiple times,
            # we do not know which one to use, and fail fast.
            if basename in libraries_to_fix_rpath:
                # pre_full_dst = libraries_to_fix_rpath[basename]
                # # libxxxx.so produced by module is only installed once to it's module dir
                # if dst.startswith("lib/"):
                #     # remove it from lib/
                #     os.remove(dst_full)
                #     return
                # elif not pre_full_dst.startswith(os.path.join(prefix, "lib/")):
                #     sys.stderr.write("Multiple installation rules found for {}."
                #                      .format(basename))
                #     # sys.exit(1)
                #     return
                # else:
                #     # remove it from lib/
                #     os.remove(pre_full_dst)
                if dst_full not in libraries_to_fix_rpath[basename]:
                    libraries_to_fix_rpath[basename].append(dst_full)
            else:
                libraries_to_fix_rpath[basename] = [dst_full]
    elif may_be_binary(dst_full):  # May be an executable.
        potential_binaries_to_fix_rpath.append(dst_full)

def create_package_meta(prefix):
    global meta
    global packages
    global export_library_dict

    if list_only:
        return

    if len(packages) == 0:
        return

    for package_path in packages:
        cyberfile_parser = ET.parse(os.path.join(package_path, "cyberfile.xml"))
        root = cyberfile_parser.getroot()

        pkg_name = root.find("name").text
        package_index_path = os.path.join(prefix, meta_prefix, pkg_name)
        os.makedirs(package_index_path, exist_ok=True)

        package_install_files = []
        files_dst = set()
        for install_file in packages[package_path]:
            package_install_files.append(install_file)
            files_dst.add(install_file.split(":")[-1])
        package_install_files.append(
            "src_path:{}".format(package_path))
        if os.path.exists(os.path.join(package_index_path, "meta.txt")):
            with open(os.path.join(package_index_path, "meta.txt"), "r+") as f:
                # delete redundant files
                contents = f.read().split("\n")
                contents = contents[: len(contents)-1]
                for i in contents:
                    file_name = i.split(":")[-1]
                    file_full_name = os.path.join(prefix, file_name)
                    if file_name not in files_dst and \
                            os.path.exists(file_full_name):
                        os.remove(file_full_name)
                f.seek(0)
                f.truncate(0)
                f.write("\n".join(package_install_files))
        else:
            with open(os.path.join(package_index_path, "meta.txt"), "w+") as f:
                f.write("\n".join(package_install_files))

        so_files = []
        for i in package_install_files:
            info = i.split(":")
            if info[1].startswith("lib") and not info[1].startswith("lib/plugin"):
                if info[1].split("/")[-1].startswith("lib"):
                    so_files.append(info[1])

        package_build_content = []
        if package_path in export_library_dict:
            package_library_list = export_library_dict[package_path]
            for lib_info in package_library_list:
                target_name = lib_info["target"]
                dst = lib_info["dst"]
                path_list = dst.split("/")
                target_name_prefix = "/".join(
                    path_list[1: len(path_list)-1]).replace("/", "_S")

                ldd_query = "ldd {}".format(os.path.join(prefix, dst))
                elf_query = "patchelf --print-needed {}".format(os.path.join(prefix, dst))

                ld_result = subprocess.check_output(
                    ldd_query, shell=True).decode("utf-8").split("\n")

                ld_result_str = "".join(ld_result)
                if "not found" in ld_result_str:
                    print("[WARNNING] missing dynamic library in ld, try refreash ld cache")
                    update_sh = os.path.join(prefix, "update_dylib.sh")
                    ld_cache = os.path.join(prefix, "ld.cache") 
                    if os.path.exists(update_sh):
                        subprocess.check_output(
                            "rm -f {} && bash {}".format(ld_cache, update_sh), shell=True).decode("utf-8").split("\n")
                    else:
                        print("\033[31m[ERROR]\033[0m missing dynamic library when processing {}".format(dst))
                        exit(-1)    

                shared_obj_dict = {}
                ld_path = []
                for i in ld_result:
                    items = i.split(" ")
                    library_name = items[0].replace("\t", "")
                    if library_name.startswith("/"):
                        system_library_name = library_name.split("/")[-1]
                        shared_obj_dict[system_library_name] = None
                    if len(items) < 3:
                        continue
                    if library_name in shared_obj_dict:
                        print("\033[31m[ERROR]\033[0m duplicated apollo_cc_library found: {}".format(
                            dst), file=sys.stderr)
                        exit(-1)
                    shared_obj_dict[library_name] = items[2]

                needed_so = subprocess.check_output(
                    elf_query, shell=True).decode("utf-8").split("\n")
                needed_so = list(filter(None, needed_so))
                for i in needed_so:
                    if i not in shared_obj_dict:
                        continue
                    if i.startswith("lib") and i.endswith(".so"):
                        ld_path.append(shared_obj_dict[i])

                link_paths = []
                for i in ld_path:
                    if i.startswith("/opt/apollo/neo/lib") and not i.startswith("/opt/apollo/neo/lib/3rd-"):
                        i = "/".join(i.split("/")[0: len(i.split("/"))-1])
                        link_paths.append('"-L{}"'.format(i))
                        link_paths.append('"-Wl,-rpath,{}"'.format(i))

                link_opts = []
                for so_lib in needed_so:
                    if so_lib.startswith("lib") and so_lib.endswith(".so"):
                        link_opts.append('"-l{}"'.format(so_lib[3: len(so_lib)-3]))
                link_opts.sort()
                link_paths = list(set(link_paths))
                link_paths.sort()
                link_opts = link_paths + link_opts
                link_opts_str = ",".join(link_opts)

                package_build_content.append(
                    BUILD_TMP.format("{}_C{}".format(target_name_prefix, target_name), dst, link_opts_str)
                )

        build_content = EMPTY_BUILD_TMP.format(
            pkg_name, package_path, package_path)

        import_file = os.path.join(package_index_path,
                            "{}.BUILD".format(pkg_name))
        if os.path.exists(import_file):
            with open(import_file, "r") as f:
                content = f.read()
            if not content == "\n".join([build_content] + package_build_content):
                with open(import_file, "w+") as f:
                    f.write("\n".join([build_content] + package_build_content))
        else:
            with open(import_file, "w+") as f:
                f.write("\n".join([build_content] + package_build_content))

        generate_pack_file(package_index_path)


# TODO(liangjinping): create index at building phase instead of installing phase
def create_plugin_index(prefix, name, src, dst):
    """create plugin description file index
    """
    plugin_index_dir = os.path.join(prefix, "share/cyber_plugin_index")
    if not os.path.exists(plugin_index_dir):
        os.makedirs(plugin_index_dir)

    index_path = os.path.join(plugin_index_dir, name)
    with open(index_path, 'wb') as fout:
      fout.writelines([dst.encode('utf-8')])


def install_plugin_description(name, src, dst):
    global subdirs

    if list_only:
        print(dst)
        return

    subdir = os.path.dirname(dst)
    if subdir not in subdirs:
        subdir_full = os.path.join(prefix, subdir)
        if not os.path.exists(subdir_full):
            os.makedirs(subdir_full)
        subdirs.add(subdir)

    dst_full = os.path.join(prefix, dst)

    create_plugin_index(prefix, name, src, dst)

    plugins[dst] = name

    # Install file, if not up to date.
    if needs_install(src, dst):
        print("-- Installing: {}".format(dst_full))
        if os.path.exists(dst_full):
            os.remove(dst_full)
        copy_or_link(src, dst_full)
    else:
        # TODO(eric.cousineau): Unclear how RPath-patched file can be deemed
        # "up-to-date" by comparison?
        print("-- Up-to-date: {}".format(dst_full))
        # No need to check patching.
        return

    basename = os.path.basename(dst)
    if re.match(dylib_match, basename):  # It is a library.
        #TODO(lanyongshun): interim method
        if basename in libraries_to_fix_rpath:
            if dst_full not in libraries_to_fix_rpath[basename]:
                libraries_to_fix_rpath[basename].append(dst_full)
        else:
            libraries_to_fix_rpath[basename] = [dst_full]

def fix_rpaths_and_strip(compatible=False):
    # Add binary executables to list of files to be fixed up:
    find_binary_executables()
    # Only fix files that are installed now.
    lib_fix_items = [] 
    bin_fix_items = []
    for k in libraries_to_fix_rpath:
        lib_fix_items += libraries_to_fix_rpath[k]

    for i in range(len(binaries_to_fix_rpath)):
        bin_fix_items += binaries_to_fix_rpath[i][1] 

    for dst_full in lib_fix_items:
        if os.path.islink(dst_full):
            # Skip files that are links. However, they need to be in the
            # dictionary to fixup other library and executable paths.
            continue
        # Enable write permissions to allow modification.
        os.chmod(dst_full, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
                | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
        # Strip before running `patchelf`. Trying to strip after patching
        # the files is likely going to create the following error:
        # 'Not enough room for program headers, try linking with -N'
        # if strip:
        #     check_call([strip_tool, dst_full])
        py_bin_path = os.path.join(prefix, "bin")
        # skip py runfiles fix rpath
        if dst_full.startswith(py_bin_path) and ".runfiles" in dst_full:
            continue
        linux_fix_rpaths(dst_full, binary=False, compatible=compatible)

    for dst_full in bin_fix_items:
        if os.path.islink(dst_full):
            # Skip files that are links. However, they need to be in the
            # dictionary to fixup other library and executable paths.
            continue
        # Enable write permissions to allow modification.
        os.chmod(dst_full, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
                | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
        # Strip before running `patchelf`. Trying to strip after patching
        # the files is likely going to create the following error:
        # 'Not enough room for program headers, try linking with -N'
        # if strip:
        #     check_call([strip_tool, dst_full])
        py_bin_path = os.path.join(prefix, "bin")
        # skip py runfiles fix rpath
        if dst_full.startswith(py_bin_path) and ".runfiles" in dst_full:
            continue
        linux_fix_rpaths(dst_full, binary=True, compatible=compatible)


def linux_fix_rpaths_old(dst_full):
    # A conservative subset of the ld.so search path. These paths are added
    # to /etc/ld.so.conf by default or after the prerequisites install script
    # has been run. Query on a given system using `ldconfig -v`.
    # TODO(storypku): revisit this later for Aarch64
    ld_so_search_paths = [
        '/lib',
        '/lib/x86_64-linux-gnu',
        '/lib32',
        '/libx32',
        '/usr/lib',
        '/usr/lib/x86_64-linux-gnu',
        '/usr/lib/x86_64-linux-gnu/libfakeroot',
        '/usr/lib/x86_64-linux-gnu/mesa-egl',
        '/usr/lib/x86_64-linux-gnu/mesa',
        '/usr/lib/x86_64-linux-gnu/pulseaudio',
        '/usr/lib32',
        '/usr/libx32',
        '/usr/local/lib',
    ]
    file_output = check_output(["ldd", dst_full]).decode("utf-8")
    rpath = []
    for line in file_output.splitlines():
        ldd_result = line.strip().split(' => ')
        if len(ldd_result) < 2:
            continue
        # Library in install prefix.
        if ldd_result[1] == 'not found' or ldd_result[1].startswith(prefix):
            re_result = re.match(dylib_match, ldd_result[0])
            # Look for the absolute path in the dictionary of libraries using
            # the library name without its possible version number.
            soname, _ = re_result.groups()
            if soname not in libraries_to_fix_rpath:
                continue
            lib_dirname = os.path.dirname(dst_full)
            index = 0
            if len(libraries_to_fix_rpath[soname]) > 1:
                for i in range(len(libraries_to_fix_rpath[soname])):
                    if libraries_to_fix_rpath[soname][i].startswith(
                        os.path.join(prefix, "lib")):
                        index = i
            diff_path = os.path.dirname(
                os.path.relpath(libraries_to_fix_rpath[soname][index], lib_dirname)
            )
            rpath.append('$ORIGIN' + '/' + diff_path)
        # System library not in ld.so search path.
        else:
            # Remove (hexadecimal) address from output leaving (at most) the
            # path to the library.
            ldd_regex = r"(.*\.so(?:\.\d+)*) \(0x[0-9a-f]+\)$"
            re_result = re.match(ldd_regex, ldd_result[1])
            if re_result:
                lib_dirname = os.path.dirname(
                    os.path.realpath(re_result.group(1))
                )
                if lib_dirname not in ld_so_search_paths:
                    rpath.append(lib_dirname + '/')

    # The above may have duplicated some items into the list.  Uniquify it
    # here, preserving order.  Note that we do not just use a set() above,
    # since order matters.
    rpath = collections.OrderedDict.fromkeys(rpath).keys()

    # Replace build tree RPATH with computed install tree RPATH. Build tree
    # RPATH are automatically removed by this call. RPATH will contain the
    # necessary absolute and relative paths to find the libraries that are
    # needed. RPATH will typically be set to `$ORIGIN` or `$ORIGIN/../../..`,
    # possibly concatenated with directories under /opt.
    str_rpath = ":".join(x for x in rpath)
    check_output(
        ["patchelf",
         "--force-rpath",  # We need to override LD_LIBRARY_PATH.
         "--set-rpath", str_rpath,
         dst_full]
    )

def resolve_install_rpath(path, rpath, prefix):
    """resolve install rpath
    """
    lib_prefix = "/opt/apollo/neo/lib"
    bin_prefix = "/opt/apollo/neo/bin"
    if not rpath.startswith('$ORIGIN'):
        return rpath
    if not prefix:
        return rpath

    patt = re.compile(r'(.*)/_solib_.*?/(.*)')
    m = patt.match(rpath)
    if not m:
        return rpath

    # m.group(1)
    if prefix == lib_prefix:
        decoded_path = m.group(2).replace('_C', ':').replace(
            '_U', '_').replace('_S', '/').replace('_D', '.')
        if decoded_path.startswith('_@'):
            # external libs
            subpath = decoded_path.split('__')[1]
            ext_rpath = re.compile(r'^_lib/').sub(m.group(1) + '/', subpath)
            if os.path.exists(ext_rpath.replace('$ORIGIN', os.path.dirname(path))):
                # apollo packages
                return ext_rpath
            # external packages
            return None
        else:
            # package internal lib
            return decoded_path.split(':')[0].replace('_//', m.group(1) + '/')
    else:
        decoded_path = m.group(2).replace('_C', ':').replace(
            '_U', '_').replace('_S', '/').replace('_D', '.')
        if decoded_path.startswith('_@'):
            # external libs
            subpath = decoded_path.split('__')[1]
            ext_rpath = re.compile(r'^_lib/').sub(m.group(1) + '/', subpath)
            if '$ORIGIN' in ext_rpath:
                # apollo packages
                suffix = ext_rpath.split("../")[-1]
                return "$ORIGIN/../lib/{}".format(suffix)
            # external packages
            return None
        else:
            # package internal lib
            raw_decode_path = decoded_path.split(':')[0].replace('_//', m.group(1) + '/')
            suffix = raw_decode_path.split("../")[-1]
            return "$ORIGIN/../lib/{}".format(suffix)
            

        return decoded_path.split(':')[0].replace('_//', m.group(1) + '/') 

def linux_fix_rpaths(path, binary=False, compatible=False):
    """fix rpath
    """
    old_rpath = check_output(
        ['patchelf', '--print-rpath', path])
    old_rpath_list = old_rpath.decode('utf-8').strip().split(':')
    if binary:
        prefix = "/opt/apollo/neo/bin"
    else:
        prefix = '/opt/apollo/neo/lib'
    new_rpath_list = list(filter(
        lambda x: x is not None and x != '',
        map(lambda x: resolve_install_rpath(path, x, prefix), old_rpath_list)))
    new_rpath = ':'.join(new_rpath_list)
    compatible_rpath_list = []
    if compatible:
        for i in new_rpath_list:
            # indirect dependence
            if i.startswith("/opt/apollo/neo/lib"):
                compatible_rpath_list.append(
                    os.path.abspath(i).replace("/opt/apollo/neo/lib", "/apollo/bazel-bin"))
            # direct dependence
            elif i.startswith("$ORIGIN"):
                # binary
                if "../lib/" in i:
                    suffix = i.split("../lib/")[-1]
                # library
                else:
                    suffix = i.split("../")[-1]
                compatible_rpath_list.append(os.path.join("/apollo/bazel-bin", suffix))

        compatible_rpath_list = list(set(compatible_rpath_list))

    if len(compatible_rpath_list) > 0:
        new_rpath = "{}:{}".format(new_rpath, ":".join(compatible_rpath_list))

    check_call(['patchelf', '--force-rpath', '--set-rpath', new_rpath, path])


def main(args):
    global color
    global list_only
    global prefix
    global strip
    global strip_tool
    global install_lib
    global legacy

    # global dbg
    # global gpu
    # global dev

    # Set up options.
    parser = argparse.ArgumentParser()
    parser.add_argument('prefix', type=str, help='Install prefix')
    parser.add_argument(
        '--color', action='store_true', default=False,
        help='colorize the output')
    parser.add_argument(
        '--list', action='store_true', default=False,
        help='print the list of installed files; do not install anything')
    parser.add_argument(
        '--no_strip', dest='strip', action='store_false', default=True,
        help='do not strip symbols (for debugging)')
    parser.add_argument(
        '--strip_tool', type=str, default='strip',
        help='strip program')
    parser.add_argument(
        '--pre_clean', action='store_true', default=False,
        help='ensure clean install by removing `prefix` dir if it exists '
             'before installing')
    parser.add_argument('--no_lib', dest='install_lib', action='store_false', default=True,
                        help='do not install files in lib dir.')

    parser.add_argument('--no_fix_rpath', dest='fix_rpath', action='store_false', default=True,
                        help='do not fix the rpath of .so files.')

    # parser.add_argument('--dbg', action='store_true', default=False,
    #                     help='debug package with debugging symbols.')
    # parser.add_argument('--gpu', action='store_true', default=False,
    #                     help='build with gpu.')
    # parser.add_argument('--dev', action='store_true', default=False,
    #                     help='dev package with headers.')
    parser.add_argument('--legacy', action='store_true', default=False,
                        help='legacy way to release output.')
    parser.add_argument('--compatible-with-src', action='store_true', default=False,
                        help='output are compatible with the src env.')
    args = parser.parse_args(args)

    color = args.color
    # Get install prefix.
    prefix = args.prefix
    list_only = args.list
    # Check if we want to avoid stripping symbols.
    strip = args.strip
    strip_tool = args.strip_tool
    pre_clean = args.pre_clean
    install_lib = args.install_lib
    fix_rpath = args.fix_rpath
    compatible = args.compatible_with_src

    # dbg = args.dbg
    # gpu = args.gpu
    # dev = args.dev

    legacy = args.legacy

    # Transform install prefix if DESTDIR is set.
    # https://www.gnu.org/prep/standards/html_node/DESTDIR.html
    destdir = os.environ.get('DESTDIR')
    if destdir:
        prefix = destdir + prefix

    # Because Bazel executes us in a strange working directory and not the
    # working directory of the user's shell, enforce that the install
    # location is an absolute path so that the user is not surprised.
    if not os.path.isabs(prefix):
        parser.error(
            "Install prefix must be an absolute path (got '{}')\n".format(
                prefix))

    if color:
        ansi_color_escape = "\x1b[36m"
        ansi_reset_escape = "\x1b[0m"
    else:
        ansi_color_escape = ""
        ansi_reset_escape = ""

    if pre_clean:
        if os.path.isdir(prefix):
            print(f"Remove previous directory: {prefix}")
            shutil.rmtree(prefix)

    if strip:
        # Match the output of the CMake install/strip target
        # (https://git.io/fpdzK).
        print("{}Installing the project stripped...{}".format(
            ansi_color_escape, ansi_reset_escape))
    else:
        # Match the output of the CMake install target (https://git.io/fpdzo).
        print("{}Install the project...{}".format(
            ansi_color_escape, ansi_reset_escape))

    # Execute the install actions.
    install("xx/conf/xx.conf", "share/xx/conf/xx.conf", "neo", "xx")
    install("xx/conf/xx.pb.txt", "share/xx/conf/xx.pb.txt", "neo", "xx")
    install("xx/dag/xx.dag", "share/xx/dag/xx.dag", "neo", "xx")
    install("xx/launch/xx.launch", "share/xx/launch/xx.launch", "neo", "xx")
    install("xx/libxx_component_lib.so", "lib/xx/libxx_component_lib.so", "neo", "xx", "export_library", "xx_component_lib")
    install("xx/libxx_component.so", "lib/xx/libxx_component.so", "neo", "xx")
    install("xx/cyberfile.xml", "xx/cyberfile.xml", "neo", "xx")
    install("xx/proto/xx.pb.h", "include/xx/proto/xx.pb.h", "neo", "xx")
    install("xx/proto/xx.pb.cc", "include/xx/proto/xx.pb.cc", "neo", "xx")
    install("xx/proto/lib_xx_proto.a", "include/xx/proto/lib_xx_proto.a", "neo", "xx")
    install("xx/proto/lib_xx_proto.so", "include/xx/proto/lib_xx_proto.so", "neo", "xx")
    install("xx/proto/xx_pb2.py", "python/xx/proto/xx_pb2.py", "neo", "xx")
    install("xx/proto/lib_xx_proto_xp_bin.so", "lib/xx/proto/lib_xx_proto_xp_bin.so", "neo", "xx", "export_library", "xx_proto")

    # Libraries paths may need to be updated in libraries and executables.
    if fix_rpath:
        fix_rpaths_and_strip(compatible)

    create_package_meta(prefix)

if __name__ == "__main__":
    main(sys.argv[1:])
