#  Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
#  All Rights Reserved.
#  Confidential and Proprietary - Qualcomm Technologies, Inc.


"""Collection of general-purpose tools/methods for use in SSG SConscripts"""
# system modules
import datetime
import glob
import hashlib
import os
import re
import subprocess
import sys
import tempfile
import threading
import time

from distutils.version import LooseVersion
from xml.etree import ElementTree

# local modules
import SCons
import SCons.Defaults

import QueryKrnlSvcNCmnlibApi

# constants
THIS_DIR = os.path.abspath(os.path.dirname(__file__))

# globals
_LIB_CACHE = {}
_TOOL_CACHE = {}

#------------------------------------------------------------------------------
# Hooks for Scons
#------------------------------------------------------------------------------
def exists(env):
    return env.Detect('ssg_environment')


def generate(env):
    elf_scripts_path = env.subst('${BUILD_ROOT}/core/bsp/build/scripts/')
    sys.path.append(os.path.abspath(elf_scripts_path))
    env.AddMethod(feature_flag, "DeclareFeature")
    env.AddMethod(get_scons_abspath, "SConsAbsPath")
    env.AddMethod(get_app_id_by_image_name, "GetAppIDByImageName")
    env.AddMethod(offtarget_testing, "OfftargetTesting")
    env.AddMethod(is_64_bit_image, "Is64BitImage")
    env.AddMethod(is_modem_chipset, "IsModemChipset")
    env.AddMethod(standalone_sdk, "StandaloneSdk")
    env.AddMethod(file_exists, "SconsFileExists")
    env.AddMethod(dir_exists, "SconsDirExists")
    env.AddMethod(filter_nodes, "FilterNodes")
    env.AddMethod(split_sources, "SplitSources")
    env.AddMethod(get_image_config_xml, "GetImageConfigXml")
    env.AddMethod(has_apps_component,"HasAppsComponent")
    env.AddMethod(xbl_sec_feature_list, "XblSecFeatureList")
    env.AddMethod(check_linker_relro_support, "CheckLinkerRelroSupport")
    env.AddMethod(verify_llvmbin_access, "VerifyLLVMBINAccess")
    env.AddMethod(get_offtarget_llvm, "GetOfftargetLLVM")
    env.AddMethod(check_toolchain, "CheckToolchain")
    env.AddMethod(check_library, "CheckLib")
    env.AddMethod(check_shared_library, "CheckSharedLib")
    env.AddMethod(check_static_library, "CheckStaticLib")
    env.AddMethod(disable_sls, "DisableSLS")
    env.AddMethod(enable_sls, "EnableSLS")
    env.AddMethod(is_cplusplus, "IsCplusplus")
    env.AddMethod(is_armv9_app_env_stat, "IsArmv9AppEnvStat")
    env.AddMethod(is_riscv_app_env_stat, "IsRiscVAppEnvStat")
    env.AddMethod(check_scons, "CheckSCons")
    env.AddMethod(save_lst_entries, "SaveListEntries")
    env.AddMethod(merge_lst_entries, "MergeListEntries")

    env.AddMethod(QueryKrnlSvcNCmnlibApi.query_supported_krnl_svc, "QuerySupportedKrnlSvc")
    env.AddMethod(QueryKrnlSvcNCmnlibApi.query_supported_cmnlib_api, "QuerySupportedCmnlibApi")
    # This method is supported only in internal SDK build system, therefore adding a check for it.
    if env.get('STANDALONE_SDK'):
      # This file is present and needed only in internal SDK.
      if os.path.isfile(os.path.join(env.subst('${SDK_ROOT}'), 'scripts', 'ExternalSdkBuildSanity.py')):
        sys.path.insert(0, os.path.join(env.subst('${SDK_ROOT}'), 'scripts'))
        import ExternalSdkBuildSanity
        env.AddMethod(ExternalSdkBuildSanity.external_sdk_build_sanity, "ExtSdkBldSanity")

    env.AddMethod(offtarget_is_enabled, 'OfftargetIsEnabled')

    # This method is supported only in ssg build system, therefore adding a check for it.
    if not env.get('STANDALONE_SDK'):
      sys.path.insert(0, THIS_DIR)
      import sdk_dpl_files_track
      env.AddMethod(sdk_dpl_files_track.sdk_lib_src_track, 'SdkLibsSrcTrack')
    #We need to have a dummy DoUnitTest method for when we aren't building the unittest target
    #the actual unittest sconscript will load the actual tools/features
    env.AddMethod(dummy_method,"DoBareTest")
    env.AddMethod(dummy_method,"DoGoogleTest")

    #Dummy Doxygen method
    env.AddMethod(dummy_method,"AddDoxySource")

    #Dummy publish and require APIs
    env.AddMethod(dummy_method,"PublishProtectedApi")
    env.AddMethod(dummy_method,"PublishPrivateApi")
    env.AddMethod(dummy_method,"RequirePublicApi")
    env.AddMethod(dummy_method,"RequireRestrictedApi")
    env.AddMethod(dummy_method,"RequireProtectedApi")
    env.AddMethod(dummy_method,"RequirePrivateApi")

    env.AddMethod(dummy_method,"SyscallEntry")
    env.AddMethod(dummy_method,"AddTZBSPSyscalls")
    env.AddMethod(dummy_method,"SyscallListAsBuilderSource")
    env.AddMethod(dummy_method,"BuilderSourceAsSyscallList")
    env.AddMethod(dummy_method,"GenerateTZBSPSyscallDef")

    env.AddMethod(uncloneable_list,'UncloneableList')
    env.AddMethod(uncloneable_dict,'UncloneableDict')
    env.AddMethod(uncloneable_tuple,'UncloneableTuple')

    env.AddMethod(usermode_supports_architecture,'UserModeSupportsArchitecture')
    env.AddMethod(usermode_preferred_architecture,'UserModePreferredArchitecture')
    env.AddMethod(load_user_mode_config, 'LoadUserModeConfig')

    # This method is exposed from TZ APPS build system.
    # but used in TZ build system.
    # Hence, dummy method is exposed here for TZ build system.
    env.AddMethod(dummy_method, 'CaptureTaData')

    env.AddMethod(ta_compression_supported, 'TACompressionSupported')

    # now use DeclareFeature to declare this script itself
    env.DeclareFeature("SSG_BUILD_ENVIRONMENT", uses_prefix=True)


    env.Tool('image_configuration', toolpath=[THIS_DIR])
    env.Tool('cmdline_config', toolpath=[THIS_DIR])


    env.AddCommandlineArgument(env.BoolVariable('do_build_resource_stats','print out stats about memory and cpu load while building',0))
    default_env = SCons.Defaults._fetch_DefaultEnvironment()
    #This file is called from a few potential places during a full build; we only want to spawn the
    #stats printer thread once
    if env.get('do_build_resource_stats') and not default_env.get('CPU_LOAD_THREADING_STUFF_INITTED'):
      default_env['CPU_LOAD_THREADING_STUFF_INITTED'] = True
      init_print_cpu_load(env)


def ta_compression_supported(env):
  """Returns the True/False based on whether current chipset supports TA compression or not"""
  chipsets_not_supporting_ta_compression = ['kailua', 'kodiak', 'kuno', 'lanai', 'lassen', 'lemans', 'matrix', 'monaco', 'pinnacles', 'sa525m', 'sariska', 'waipio','molokai','netrani', 'aspen']
  if env['CHIPSET'] in chipsets_not_supporting_ta_compression:
    return False
  return True


def print_cpu_load(psutil):
  p = psutil.Process()
  #as far as the terminal output is concerned, one print statement is atomic. so we put all of
  #the output into one string so that the stats aren't interleaved with other build output
  with p.oneshot():
    s = '__cpustats__ ' + str(time.time())
    s += ' getloadavg' + str(psutil.getloadavg())
    s += ' cpu_percent(' + str(psutil.cpu_percent(percpu=False))
    s += ') ' + str(psutil.virtual_memory())
  print(s)

def print_cpu_load_daemon(psutil, *args, **kwargs):
  while True:
    print_cpu_load(psutil)
    time.sleep(10)

def init_print_cpu_load(env):
  # Gathering cpu/ram usage stats requires psutil, which is not part of the standard pythonlib2
  # library. On internal linux harvesters, we can grab it from /pkg/ssg/..., but can't make that
  # guarantee in any other way really, so we try to import it, and if we fail, we just dont gather
  # build stats
  try:
    if os.path.isdir('/pkg/ssg'):
      sys.path.append('/pkg/ssg/pythonlib2.7/site-packages')
    import psutil
    print_cpu_load(psutil)
    #we use threading rather than multiprocessing because scons does black magic
    #to rename the Pickling module, which is also used by multiprocessing. Said
    #black magic kills the multiprocessing spawning code with weird errors. It does
    #not, however, affect how threading spawns threads. The print_cpu_load is very
    #nice and sleeps a long time, so it does not affect build time significantly,
    #regardless of the GIL existing
    thread = threading.Thread(target=print_cpu_load_daemon, args=(psutil,))
    # spawning the print_cpu_load thread as a daemon means it will get killed when
    # the parent scons process is also killed
    thread.daemon = True
    thread.start()
    print("Initialized build resource gathering statistics!")
  except:
    print("Could not initialize build resource gathering statistics!")
    env['do_build_resource_stats'] = False
    if '/pkg/ssg/pythonlib2.7/site-packages' in sys.path:
      sys.path.remove('/pkg/ssg/pythonlib2.7/site-packages')


def feature_flag(env, flag, uses_prefix=False):
    """Adds a feature-definition flag to the environment in a way that is
       compatible with both QC SCons (USES_FLAGS) & Open Source SCons. Either
       implementation can be checked in SConscripts with 'if <flag> in env'.
       If 'uses_prefix' is True, also declares a USES_<flag> version of the
       flag, even if QC SCons is not available (useful for tools/features loaded
       globally before QC SCons)."""
    try:
        env.AddUsesFlags(flag)
    except AttributeError:
        env[flag] = True

    # most CoreBSP scripts expect that uses flags will always begin with
    # USES_* for consistency. If this feature flag does not already do that,
    # define an additional flag for their benefit.
    if flag.startswith("USES_"):
        return

    try:
        env.AddUsesFlags("USES_" + flag)
    except AttributeError:
        if uses_prefix:
            env["USES_" + flag] = True


def get_scons_abspath(env, item):
    """Returns the absolute path of the given string or SCons Node"""
    ret = None
    if type(item) == str:
      ret = env.File(item).get_abspath()
    elif isinstance(item, SCons.Node.Node):
      ret = item.get_abspath()
    elif isinstance(item, SCons.Node.Alias.Alias):
      ret = item.get_abspath()

    assert ret != None, "Unexpected item type '{}'".format(str(type(item)))
    return ret


def file_exists(env, filepath):
    """Returns True if 'filepath' exists, else False. Does not work for
       directories"""
    return os.path.exists(str(env.File(filepath).srcnode()))


def dir_exists(env, dirpath):
    """Returns True if 'dirpath' exists, else False. Only works for
       directories"""
    return os.path.exists(str(env.Dir(dirpath).srcnode()))


def filter_nodes(env, nodes, filter_string, abspath=False):
    """Filters down the given list of 'nodes' to only those that contain
       'filter_string' after env.subst & os.normpath calls on both the node
       & the filter_string. Returns the filtered list of nodes. If 'abspath'
       is True (default False), also applies os.path.abspath to the node &
       filter string."""
    norm_func = os.path.normpath
    if abspath:
        # abspath also includes normpath automatically
        norm_func = os.path.abspath

    subst_filter = norm_func(env.subst(filter_string))
    ret = []
    for node in nodes:
        if subst_filter in norm_func(env.subst(str(node))):
            ret.append(node)

    return ret


def split_sources(env, nodes, source_extensions=None):
    """Splits out the given list of nodes into 'sources', as defined by the
       given list of 'source_extensions' (else default '.c', 'cpp', '.s', etc)
       and 'other' (eg headers). Returns (sources, other)."""
    if source_extensions is None:
        source_extensions = [".c", ".cpp", ".cxx", ".s", ".S", ".spp", ".SPP"]
    else:
        source_extensions = env.Flatten(source_extensions)

    sources = []
    other = []
    for node in nodes:
        for ext in source_extensions:
            if str(node).endswith(ext):
                sources.append(node)
                break
        else:  # doesn't match any extension
            other.append(node)

    assert len(sources) + len(other) == len(nodes), str(nodes)
    return sources, other

def is_cplusplus(env, sources):
  cpp_suffixes = ['.cpp', '.cc', '.cxx', '.c++', '.C++', '.mm']
  if not sources:
    return False
  for source in env.Flatten(sources):
    if source:
      ext = os.path.splitext(str(source))[1]
      if ext in cpp_suffixes:
        return True
  return False

# By default, isarmv9_stat is considered true 64-bit target based on armv9 flag
# configured in sdk_config_<chipset>.cfg. It will only be set false if explicitly
# set false by some TA(non embedded) environment.
def is_armv9_app_env_stat(env):
  isarmv9_stat = False
  if env['PROC'] == 'A53_64' and env.get('ARM_AARCH') == "armv9":
    isarmv9_stat = True
  if 'isarmv9' in env and not env['isarmv9']:
    isarmv9_stat = False
  return isarmv9_stat

def is_riscv_app_env_stat(env):
  isriscv_stat = False
  if env['PROC'] == 'RV64' and env['TZ_EXEC_MODE'] == 'riscv64' and env.get('RISCV_AARCH') == "riscv64":
    isriscv_stat = True
  return isriscv_stat

def get_image_config_xml(env, xml_file, alias_name):
    """Returns a valid, chipset-specific path for the IMAGE_CONFIG_XML
       environment variable required by devcfg"""
    img_cfg = "${BUILD_ROOT}/core/bsp/build/chipset/${CHIPSET}/" + xml_file
    if env.SconsFileExists(img_cfg):
        return env.subst(img_cfg)

    env.PrintWarning("Using stubs IMAGE_CONFIG_XML for " + alias_name)
    return env.subst("${BUILD_ROOT}/ssg/bsp/build/chipset/sdm0000/" + xml_file)


def get_app_id_by_image_name(env, image_name):
    """Return app id by calculating hash value with image name

       On MBNv6, app id has 4 byte length (8 hex characters) while previous
       version supported 8 byte, 16 hex characters. So, we limit it as 4 byte
       with exception (sdm670/sdm845) due to potential regression
       """

    if env["CHIPSET"] in ['sdm670', 'sdm845']:
        app_id_hexstr_len = 16
    else:
        app_id_hexstr_len = 8

    return '0x' + hashlib.sha256(image_name.encode('utf-8')).hexdigest()[-app_id_hexstr_len:]

def offtarget_testing(env):
    return env.get('OFF_TARGET')

def is_64_bit_image(env):
    return env['PROC'] in ['A53_64', 'x86-64', 'RV64']

def is_modem_chipset(env):
    return env['SOC_TYPE'].lower() == "mdm"

def standalone_sdk(env):
    return env.get('STANDALONE_SDK')

def has_apps_component(env):
    return env.get('HAS_APPS_COMPONENT')

def xbl_sec_feature_list(env):
    uses_stubs = False
    if env.get("MSM_ID") != "0000" and \
            env.SconsFileExists("${BUILD_ROOT}/ssg/securemsm/xbl_sec/build/cfg/xbl_sec_build_config_${MSM_ID}.xml"):
        build_config_xml = env.subst('${BUILD_ROOT}/ssg/securemsm/xbl_sec/build/cfg/xbl_sec_build_config_${MSM_ID}.xml')
    else:
        uses_stubs = True
        build_config_xml = env.subst('${BUILD_ROOT}/ssg/securemsm/xbl_sec/build/cfg/xbl_sec_build_config_0000.xml')

    # parse target specific configuration file to get list of features the
    # target needs to be built with.
    feature_list = []
    build_config_tree = ElementTree.parse(build_config_xml)
    build_config_root = build_config_tree.getroot()
    for child in build_config_root:
        if child.attrib["name"] == env['CHIPSET'] or uses_stubs:
            for feature in child.iter("feature"):
                feature_list.append(feature.text)

    return feature_list

def check_linker_relro_support(env):
    if not 'LINKER_RELRO_SUPPORT' in env:
        test_ld_script = """
            PHDRS {
              RELRO PT_GNU_RELRO;
            }
            SECTIONS {
            }
            """
        linker_root = env.subst("${LLVMBIN}")
        if not linker_root:
            linker_root = os.environ["LLVMBIN"]

        assert linker_root, str(linker_root)
        linker = os.path.join(linker_root, "ld.qcld")

        try:
            name = None
            f = tempfile.NamedTemporaryFile(mode="w", delete=False)
            name = f.name
            f.write(test_ld_script)
            f.close()
            returncode = subprocess.run([linker, "-T", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode
        finally:
            if name:
                os.unlink(name)

        env['LINKER_RELRO_SUPPORT'] = True if returncode == 0 else False

    return env['LINKER_RELRO_SUPPORT']

def verify_llvmbin_access(env):
    """Pulls LLVMBIN environment variable and checks to ensure the path exists and is accessible.
    If it is not, errors out with clear message. If in a docker container, prints a message to the
    log advising user this could be an issue related to the incorrect container."""
    try:
        llvmbin = os.environ["LLVMBIN"]
        if not os.path.exists(llvmbin) or not os.access(llvmbin, os.X_OK):
            raise RuntimeError(f"ERROR: The specified llvm path {llvmbin} is either "
                               f"inaccessible or does not exist.")

    except RuntimeError as e:
        dockerfile = "/.dockerenv"
        if os.path.exists(dockerfile):
            print("ERROR: It is likely you are not using the correct docker container. "
                  "Please check the image and ensure you are pulling the corresponding container.")
        raise

def get_offtarget_llvm(env):
    """Returns the /usr/lib/llvm-*/bin directory of the highest available LLVM
       version for offtarget (x86) compilation. Checks for required tools and
       raises a version-requirement message if needed."""
    try:
        llvmbin = _get_offtarget_llvm(env)
        _required_tools = [
            "llvm-strip",
        ]
        for tool in _required_tools:
            if sys.platform.startswith("win"):
                tool = tool + ".exe"

            toolpath = os.path.join(llvmbin, tool)
            if not os.path.exists(toolpath):
                raise RuntimeError(("Required LLVM tool '{}' not found in {}.\n"
                                    "This can usually be resolved with:\n"
                                    "sudo apt-get install llvm-8 clang-8  # or higher").format(
                                        tool, llvmbin))
    except RuntimeError as e:
        _kwfallback = '/pkg/qct/software/llvm/release/arm/8.0/bin'
        if not os.path.exists(_kwfallback):
            raise

        print(str(e))
        print(("WARNING: Falling back to network toolchain to avoid fatal error:\n"
               "{}\nInstall llvm-8 clang-8 or later to resolve this warning.").format(
                   _kwfallback))
        llvmbin = _kwfallback

    return llvmbin

def _get_offtarget_llvm(env):
    """Returns the /usr/lib/llvm-*/bin directory of the highest available LLVM
       version for offtarget (x86) compilation"""
    llvm_versions = []
    for llvmdir in glob.glob("/usr/lib/llvm-*"):
        version_info = os.path.basename(llvmdir).split("-", 1)
        if len(version_info) == 1:
            continue
        try:
            llvm_versions.append((float(version_info[1]), llvmdir))
        except ValueError:
            continue

    # sort by LLVM version; return the highest
    llvm_versions.sort(reverse=True)
    for _, llvmdir in llvm_versions:
        llvmbin = os.path.join(llvmdir, "bin")
        if os.path.exists(llvmbin):
            return llvmbin

    raise RuntimeError("No local LLVM bin found (/usr/lib/llvm-*.bin).\n"
                       "This can usually be resolved with:\n"
                        "sudo apt-get install llvm-8 clang-8  # or higher")


def check_toolchain(env, tool, min_version=None, max_version=None, args=None):
    """Validates that 'tool' is present on the current system by attempting to
       invoke it as a subprocess. Queries version info with '--version' for
       most tools by default. A custom argument list can be passed via 'args'
       instead for tools that do not support this flag. Allows optional
       version comparison against 'min_version' and/or 'max_version'.
       Returns True if the tool invokes successfully and meets any version
       requirements, else False. Only works with the internal version regex:
       ([0-9]+\.[0-9]+(\.[0-9]+)?)"""
    if not isinstance(tool, str):
        raise TypeError("provided 'tool' must be a string: {} ({})".format(
            str(tool), str(type(tool))))

    _args = " ".join(args) if args else None
    try:
        version = _TOOL_CACHE[(tool, _args)]
    except KeyError:
        version = _get_toolchain_version(tool, args)
        _TOOL_CACHE[(tool, _args)] = version

    return _compare_version(version, min_version, max_version, name=tool)


def _get_toolchain_version(tool, args=None):
    """Helper method to query 'tool' version"""
    toolchain_cmd = [tool]
    if args:
        toolchain_cmd.extend(args)
    else:
        toolchain_cmd.append("--version")

    try:
        output = subprocess.check_output(
            toolchain_cmd, stderr=subprocess.STDOUT, encoding="utf-8")
    except OSError as e:
        print("WARNING: Failed to invoke '{}':\n{}".format(tool, str(e)))
        return None
    except subprocess.CalledProcessError as e:
        print("WARNING: '{}' call failed:\n{}".format(tool, e.stderr))
        return None

    _version_regex = re.compile(r"([0-9]+\.[0-9]+(\.[0-9]+)?)")
    match = _version_regex.search(output)
    if not match:
        print("WARNING: Could not find version in '{}' output:\n{}".format(
            tool, output))
        return None

    return match.group(1)


def check_shared_library(env, library, min_version=None, max_version=None):
    """CheckLibrary wrapper for shared libs only"""
    return env.CheckLib(library, min_version, max_version, static=False)


def check_static_library(env, library, min_version=None, max_version=None):
    """CheckLibrary wrapper for static libs only"""
    return env.CheckLib(library, min_version, max_version, static=True)


def check_library(env, library, min_version=None, max_version=None,
                  static=None):
    """Validates that 'library' is present on the current system via ldconfig,
       if available. Allows optional version comparison against 'min_version'
       and/or 'max_version'. Returns True if ldconfig invokes successfully,
       the library is found on the system, and the library meets any version
       requirements, else False. If 'static' is None (default) checks for both
       dynamic libs via ldconfig or static libs via 'whereis'. If 'static' is
       True or False, only checks for static or dynamic libs respectively.
       Uses distutils.version.LooseVersion to compare library versions by name.
    """
    installed_libs = _get_lib_versions(library, static)

    for lib in installed_libs:
        if _compare_version(lib, min_version, max_version):
            return True

    if not installed_libs:
        print("WARNING: {} not found".format(library))
    else:
        print(
            "WARNING: Available libs do not meet version reqs (min={}, max={}):\n{}".format(
                min_version, max_version, str(installed_libs)))

    return False


def _get_lib_versions(library, static=None):
    """Helper method to get all available versions of 'library' on the current
       system."""
    shared = (static is not True)
    static = (static is not False)

    libs = []
    if shared:
        try:
            shared_libs = _LIB_CACHE[(library, False)]
        except KeyError:
            shared_libs = _get_shared_libs(library)
            _LIB_CACHE[(library, False)] = shared_libs

        libs.extend(shared_libs)

    if static:
        try:
            static_libs = _LIB_CACHE[(library, True)]
        except KeyError:
            static_libs = _get_static_libs(library)
            _LIB_CACHE[(library, True)] = static_libs

        libs.extend(static_libs)

    return libs


def _get_shared_libs(library):
    """Searches for installed shared libs via 'ldconfig -p'"""
    libs = []
    # env isn't used but is required by SCons, so we pass None
    if not check_toolchain(None, "ldconfig"):
        return libs

    ldconfig_cmd = ["ldconfig", "-p"]

    try:
        output = subprocess.check_output(
            ldconfig_cmd, stderr=subprocess.STDOUT, encoding="utf-8")
    except OSError as e:
        print("WARNING: Failed to invoke ldconfig:\n{}".format(str(e)))
        return libs
    except subprocess.CalledProcessError as e:
        print("WARNING: ldconfig call failed:\n{}".format(e.stderr))
        return libs

    for line in output.splitlines():
        # sample ldconfig -p output format:
        #     libz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1
        line = line.strip()
        if line.startswith(library):
            libs.append(line.split()[0])

    return libs


def _get_static_libs(library):
    """Searches for installed static libs via 'whereis'"""
    libs = []
    # again, env isn't used, so we pass None
    if not check_toolchain(None, "whereis"):
        return libs

    whereis_cmd = ["whereis", library]
    try:
        output = subprocess.check_output(
            whereis_cmd, stderr=subprocess.STDOUT, encoding="utf-8")
    except OSError as e:
        print("WARNING: Failed to invoke whereis:\n{}".format(str(e)))
        return libs
    except subprocess.CalledProcessError as e:
        print("WARNING: whereis call failed:\n{}".format(e.stderr))
        return libs

    for line in output.splitlines():
        # sample whereis output format:
        # libssl: /usr/lib/x86_64-linux-gnu/libssl.a /usr/lib/x86_64-linux-gnu/libssl.so
        line = line.strip()
        line_parts = line.split(": ", 1)
        if len(line_parts) == 1:
            continue

        if not library.startswith(line_parts[0]):
            continue

        # prefix matches the lib, parse matches
        lib_candidates = line_parts[1].split()
        for candidate in lib_candidates:
            if library in candidate:
                libs.append(os.path.basename(candidate))

    return libs


def _compare_version(version, min_version=None, max_version=None, name=None):
    """Helper method to compare version numbers vs an optional min/max.
       Returns True if version is non-None and any min/max are satisfied,
       else returns False."""
    if version is None:
        return False

    version = LooseVersion(version)
    if min_version and version < LooseVersion(min_version):
        if name:
            print("WARNING: min version requirement for '{}' not met: {}".format(
                name, min_version))
        return False

    if max_version and version > LooseVersion(max_version):
        if name:
            print("WARNING: max version requirement for '{}' not met: {}".format(
                name, max_version))
        return False

    return True


def check_scons(env):
    """Checks if SCons is properly installed for the current python version.
       Returns True if there is a valid SCons install, else False."""
    ret = env.CheckToolchain(sys.executable, args=["-m", "SCons", "--version"])
    if not ret:
        print("ERROR: SCons is not installed for '{}'".format(sys.executable))
        print("Please either:")
        print("1) Build in a valid TZ Docker container")
        print("2) sudo {} -m pip install scons==4.0.1".format(sys.executable))
    return ret


def enable_sls(env):
    """Explicitly enable SLS hardening. Do not call this method unless you are
       sure it should be turned on. This will force SLS hardening for 64-bit
       chipsets and may still affect some 32-bit compilation calls."""
    env.Replace(SLS_FLAGS="-mharden-sls=all")


def disable_sls(env):
    """Explicit disable SLS hardening. Most 64-bit chipsets will be enabled by
       default unless this is called. Scope matters: a single elf can be
       disabled if desired. The call affects only the current env."""
    env.Replace(SLS_FLAGS="")


def dummy_method(*args, **kwargs):
    pass

class UncloneableList(list):
  def __semi_deepcopy__(self):
    return self

def uncloneable_list(env):
  return UncloneableList()

class UncloneableDict(dict):
  def __semi_deepcopy__(self):
    return self

def uncloneable_dict(env):
  return UncloneableDict()

class UncloneableTuple(tuple):
  def __semi_deepcopy__(self):
    return self


def add_bool_config_var(env,
                        config_name,
                        help=None,
                        default_value=None,
                        enable_map=None,
                        disable_map=None,
                        enable_if=None):
  if enable_map is None:
    enable_map = {}
  if disable_map is None:
    disable_map = {}
  if help is None:
    help = ""
  var = env.BoolVariable(config_name, help, default_value)
  return ConfigVar(var, enable_map, disable_map, enable_if)

def save_lst_entries(env, key_names):
  saved = {}

  for k in key_names:
    # should be list value if entry with the key exists in env
    v = env.get(k)
    if v is not None:
      assert(isinstance(v, list))
      saved[k] = v

  return saved

def merge_lst_entries(env, saved_entries):
  for k, saved_value in saved_entries.items():
    new_value = env.get(k, [])
    # type should be preserved, list.
    assert(isinstance(new_value, list))

    # removing duplicates
    env[k] = list(set(saved_value + new_value))

#files can contain either raw strings of paths to config files, or tuples of (str,bool)
#where tuple[0] is the path to the config file and tuple[1] is a bool designating whether
#or not the file is required. If the file is required and doesnt exist, we error out. If
#the file is not required and doesnt exist, we just print a warning
def add_config_vars(env, files, args):
  files_processed = []
  for f in files:
    required = True
    if isinstance(f,tuple):
      path, required = f
    else:
      path = f
    path = env.RealPath(path)
    if not env.SconsFileExists(path):
      if required:
        raise ValueError('ERROR: cannot open config file {}'.format(path))
      else:
        print(('WARNING: cannot open config file {}'.format(path)))
    files_processed.append(path)

  files_processed = [env.RealPath(f) for f in env.Flatten(files_processed)]

  # When SCons.Script.Varibles gets multiple cfg files (common and chipset specific), it overrides
  # entries in common cfg  with entries in chipset specific one rather than merging them.
  #
  # Due to this limitation of SCons, we process the list of config files in two phase.
  #
  # 1. Process keys with list values to merge
  #    Parsing cfg file one by one, merging previous entry value into new one to preserve default entry
  #    in common cfg and append chipset specific value rather than overriding.
  #    Feature lists (all_features/enabled_features/disabled_features)  are collected in this manner.
  #
  # 2. All other cases.
  #    Parsing cfg files at the same time. SCons.Script.Variables merging config files and then process.

  # phase 1
  keys_for_merging_list = ['all_features', 'enabled_features', 'disabled_features']
  for cfg in files_processed:
    variables = SCons.Script.Variables(cfg, args=SCons.Script.ARGUMENTS)

    for arg in args:
      if isinstance(arg,ConfigVar):
        var = arg.var
      else:
        var = arg
      if var[0] in keys_for_merging_list:
        variables.Add(var)

    saved_entries = env.SaveListEntries(keys_for_merging_list)
    variables.Update(env)
    env.MergeListEntries(saved_entries)

  # phase 2
  variables = SCons.Script.Variables(files_processed, args=SCons.Script.ARGUMENTS)
  for arg in args:
    if isinstance(arg,ConfigVar):
      var = arg.var
    else:
      var = arg
    if var[0] not in keys_for_merging_list:
      variables.Add(var)
  variables.Update(env)

  for arg in args:
    #at this point, there isn't anything to do for vanilla scons options. they've already been
    #loaded into the env and that's all that they can do
    if isinstance(arg, ConfigVar):
      var, enable_map, disable_map, enable_if = arg.unpack()
      config_name = var[0]
      if callable(enable_if):
        if not enable_if(env):
          env.Append(**disable_map)
          continue
      if env.get(config_name):
        env.Append(**enable_map)
      else:
        env.Append(**disable_map)

def add_commandline_argument(env, variable):
  vars = SCons.Script.Variables(args=SCons.Script.ARGUMENTS)
  vars.Add(variable)
  vars.Update(env)
  #if the default value for variable is None and the variable isnt specified on the commandline
  #scons just wont add it to the env. That doesn't make any sense, so let's fix that here
  default_val = variable[2]
  key = variable[0]
  if default_val is None and key not in env:
    env[key] = None

def bool_variable(env, key, help, default):
  return SCons.Variables.BoolVariable(key, help, default)

def enum_variable(env, key, help, default, allowed_values, map={}, ignorecase=0):
  return SCons.Variables.EnumVariable(key, help, default, allowed_values, map, ignorecase)

def list_variable(env, key, help, default, names, map={}):
  return SCons.Variables.ListVariable(key, help, default, names, map)

def package_variable(env, key, help, default, searchfunc=None):
  return SCons.Variables.PackageVariable(key, help, default, searchfunc)

def path_variable(env, key, help, default, validator=None):
  return SCons.Variables.PathVariable(key, help, default, validator)

class UncloneableList(list):
  def __semi_deepcopy__(self):
    return self

def uncloneable_list(env):
  return UncloneableList()

class UncloneableDict(dict):
  def __semi_deepcopy__(self):
    return self

def uncloneable_dict(env):
  return UncloneableDict()

class UncloneableTuple(tuple):
  def __semi_deepcopy__(self):
    return self

def uncloneable_tuple(env):
  return UncloneableTuple()

def offtarget_is_enabled(env):
  return env.get('OFF_TARGET_ENABLED')

# Use after init_arch. Allow x86-32, but not scorpion if aarch32 isn't in set.
# Likewise, allow x86-64 but not A53_64 if aarch64 isn't set.
def usermode_supports_architecture(env):
  return ((('aarch32' in env['EL0_ARCH_SET']) or not (env['PROC'] == 'scorpion'))
      and (('aarch64' in env['EL0_ARCH_SET']) or not (env['PROC'] == 'A53_64'))
      and (('riscv64' in env['EL0_ARCH_SET']) or not (env['PROC'] == 'RV64' and env['TZ_EXEC_MODE'] == 'riscv64' )))

# Use after init_arch. Unless Off-target, if 64-bit supported, A53_64 is
# preferred, if not, scorpion is preferred.
def usermode_preferred_architecture(env):
  if env['PROC'] == 'RV64' and env['TZ_EXEC_MODE'] == 'riscv64':
    return 'riscv64' in env['EL0_ARCH_SET']
  elif env['PROC'] == 'A53_64':
    return 'aarch64' in env['EL0_ARCH_SET']
  elif env['PROC'] == 'scorpion':
    return not ('aarch64' in env['EL0_ARCH_SET'])
  else:
    return True

def load_user_mode_config(env):
  if 'EL0_ARCH_SET' in env:
    return  #nested usage
  #if TZ kernel executes in 32bit mode, only 32bit usermode is supported
  if env['TZ_EXEC_MODE'] == 'aarch32':
    env['EL0_ARCH_SET'] = ['aarch32']
    print("ssg_environment, LoadUserModeConfig, add_config_into_env: aarch32 supported")
  else:
    build_root = env.RealPath("${BUILD_ROOT}", posix=False)
    chipset = env['CHIPSET']
    # print "ssg_environment, LoadUserModeConfig, add_config_into_env: chipset=", chipset
    filename = 'sdk_config_' + chipset + '.cfg'
    cfg_file = os.path.join(build_root,'ssg','bsp','trustzone','qsapps','deploy_sdk','build',filename)
    # print "ssg_environment, LoadUserModeConfig, add_config_into_env: cfg_file=", cfg_file
    cfg_files = [(cfg_file, False)]
    if env['PROC'] == 'RV64' and env['TZ_EXEC_MODE'] == 'riscv64':
      env.AddConfigVars(cfg_files, [('EL0_ARCH_SET', 'EL0 supported architectures', ['riscv64'])])
      env.AddConfigVars(cfg_files, [('RISCV_AARCH', 'RISCV_AARCH supported architecture', [''])])
    else:      
      env.AddConfigVars(cfg_files, [('EL0_ARCH_SET', 'EL0 supported architectures', ['aarch32','aarch64'])])
      env.AddConfigVars(cfg_files, [('ARM_AARCH', 'ARM supported architecture', [''])])
    if 'aarch32' in env['EL0_ARCH_SET']:
      print("ssg_environment, :oadUserModeConfig, add_config_into_env: aarch32 supported")
    if 'aarch64' in env['EL0_ARCH_SET']:
      print("ssg_environment, LoadUserModeConfig, add_config_into_env: aarch64 supported")
