# @file Edk2ToolsBuild.py
# Invocable class that builds the basetool c files.
#
# Supports VS2019, VS2022, and GCC5
##
# Copyright (c) Microsoft Corporation
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
import os
import sys
import logging
import argparse
import multiprocessing
import shutil
from edk2toolext import edk2_logging
from edk2toolext.environment import self_describing_environment, shell_environment
from edk2toolext.base_abstract_invocable import BaseAbstractInvocable
from edk2toollib.utility_functions import RunCmd, GetHostInfo
from edk2toollib.windows.locate_tools import QueryVcVariables


class Edk2ToolsBuild(BaseAbstractInvocable):

    def ParseCommandLineOptions(self):
        ''' parse arguments '''
        ParserObj = argparse.ArgumentParser()
        ParserObj.add_argument("-t", "--tool_chain_tag", dest="tct", default="VS2022",
                               help="Set the toolchain used to compile the build tools")
        ParserObj.add_argument("-a", "--target_arch", dest="arch", default=None, choices=[None, 'IA32', 'X64', 'ARM', 'AARCH64'],
                               help="Specify the architecture of the built base tools. Not specifying this will fall back to the default "
                               "behavior, for Windows builds, IA32 target will be built, for Linux builds, target arch will be the same as host arch.")
        args = ParserObj.parse_args()
        self.tool_chain_tag = args.tct
        self.target_arch = args.arch

    def GetWorkspaceRoot(self):
        ''' Return the workspace root for initializing the SDE '''

        # this is the bastools dir...not the traditional EDK2 workspace root
        return os.path.dirname(os.path.abspath(__file__))

    def GetActiveScopes(self):
        ''' return tuple containing scopes that should be active for this process '''

        # Adding scope for cross compilers when building for ARM/AARCH64
        scopes = ('global',)
        if GetHostInfo().os == "Linux" and self.tool_chain_tag.lower().startswith("gcc"):
            if self.target_arch is None:
                return scopes
            if "AARCH64" in self.target_arch:
                scopes += ("gcc_aarch64_linux",)
            if "ARM" in self.target_arch:
                scopes += ("gcc_arm_linux",)
        return scopes

    def GetLoggingLevel(self, loggerType):
        ''' Get the logging level for a given type (return Logging.Level)
        base == lowest logging level supported
        con  == Screen logging
        txt  == plain text file logging
        md   == markdown file logging
        '''
        if(loggerType == "con"):
            return logging.ERROR
        else:
            return logging.DEBUG

    def GetLoggingFolderRelativeToRoot(self):
        ''' Return a path to folder for log files '''
        return "BaseToolsBuild"

    def GetVerifyCheckRequired(self):
        ''' Will call self_describing_environment.VerifyEnvironment if this returns True '''
        return True

    def GetLoggingFileName(self, loggerType):
        ''' Get the logging file name for the type.
        Return None if the logger shouldn't be created

        base == lowest logging level supported
        con  == Screen logging
        txt  == plain text file logging
        md   == markdown file logging
        '''
        return "BASETOOLS_BUILD"

    def WritePathEnvFile(self, OutputDir):
        ''' Write a PyTool path env file for future PyTool based edk2 builds'''
        content = '''##
# Set shell variable EDK_TOOLS_BIN to this folder
#
# Autogenerated by Edk2ToolsBuild.py
#
# Copyright (c), Microsoft Corporation
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
{
  "id": "You-Built-BaseTools",
  "scope": "edk2-build",
  "flags": ["set_shell_var", "set_path"],
  "var_name": "EDK_TOOLS_BIN"
}
'''
        with open(os.path.join(OutputDir, "basetoolsbin_path_env.yaml"), "w") as f:
            f.write(content)

    def Go(self):
        logging.info("Running Python version: " + str(sys.version_info))

        (build_env, shell_env) = self_describing_environment.BootstrapEnvironment(
            self.GetWorkspaceRoot(), self.GetActiveScopes())

        # # Bind our current execution environment into the shell vars.
        ph = os.path.dirname(sys.executable)
        if " " in ph:
            ph = '"' + ph + '"'
        shell_env.set_shell_var("PYTHON_HOME", ph)
        # PYTHON_COMMAND is required to be set for using edk2 python builds.
        pc = sys.executable
        if " " in pc:
            pc = '"' + pc + '"'
        shell_env.set_shell_var("PYTHON_COMMAND", pc)

        if self.tool_chain_tag.lower().startswith("vs"):
            if self.target_arch is None:
                # Put a default as IA32
                self.target_arch = "IA32"

            if self.target_arch == "IA32":
                VcToolChainArch = "x86"
                TargetInfoArch = "x86"
                OutputDir = "Win32"
            elif self.target_arch == "ARM":
                VcToolChainArch = "x86_arm"
                TargetInfoArch = "ARM"
                OutputDir = "Win32"
            elif self.target_arch == "X64":
                VcToolChainArch = "amd64"
                TargetInfoArch = "x86"
                OutputDir = "Win64"
            elif self.target_arch == "AARCH64":
                VcToolChainArch = "amd64_arm64"
                TargetInfoArch = "ARM"
                OutputDir = "Win64"
            else:
                raise NotImplementedError()

            self.OutputDir = os.path.join(
                shell_env.get_shell_var("EDK_TOOLS_PATH"), "Bin", OutputDir)

            # compiled tools need to be added to path because antlr is referenced
            HostInfo = GetHostInfo()
            if TargetInfoArch == HostInfo.arch:
                # not cross compiling
                shell_env.insert_path(self.OutputDir)
            else:
                # cross compiling:
                # as the VfrCompile tool is needed in the build process, we need
                # to build one for the host system, then add the path to the
                # tools to the PATH environment variable
                shell_environment.CheckpointBuildVars()
                if HostInfo.arch == "x86" and HostInfo.bit == "64":
                    host_arch = "X64"
                    host_toolchain_arch = "amd64"
                    TempOutputDir = os.path.join(shell_env.get_shell_var("EDK_TOOLS_PATH"), "Bin", "Win64")
                elif HostInfo.arch == "x86" and HostInfo.bit == "32":
                    host_arch = "IA32"
                    host_toolchain_arch = "x86"
                    TempOutputDir = os.path.join(shell_env.get_shell_var("EDK_TOOLS_PATH"), "Bin", "Win32")
                elif HostInfo.arch == "ARM" and HostInfo.bit == "64":
                    host_arch = "AARCH64"
                    host_toolchain_arch = "amd64_arm64"
                    TempOutputDir = os.path.join(shell_env.get_shell_var("EDK_TOOLS_PATH"), "Bin", "Win64")
                elif HostInfo.arch == "ARM" and HostInfo.bit == "32":
                    host_arch = "ARM"
                    host_toolchain_arch = "x86_arm"
                    TempOutputDir = os.path.join(shell_env.get_shell_var("EDK_TOOLS_PATH"), "Bin", "Win32")
                else:
                    raise Exception("Unsupported host system. %s %s" % (HostInfo.arch, HostInfo.bit))

                interesting_keys = ["ExtensionSdkDir", "INCLUDE", "LIB"]
                interesting_keys.extend(
                    ["LIBPATH", "Path", "UniversalCRTSdkDir", "UCRTVersion", "WindowsLibPath", "WindowsSdkBinPath"])
                interesting_keys.extend(
                    ["WindowsSdkDir", "WindowsSdkVerBinPath", "WindowsSDKVersion", "VCToolsInstallDir"])
                vc_vars = QueryVcVariables(
                    interesting_keys, host_toolchain_arch, vs_version=self.tool_chain_tag.lower())
                for key in vc_vars.keys():
                    logging.debug(f"Var - {key} = {vc_vars[key]}")
                    if key.lower() == 'path':
                        shell_env.set_path(vc_vars[key])
                    else:
                        shell_env.set_shell_var(key, vc_vars[key])

                # Note: This HOST_ARCH is in respect to the BUILT base tools, not the host arch where
                # this script is BUILDING the base tools.
                shell_env.set_shell_var('HOST_ARCH', host_arch)
                shell_env.insert_path(TempOutputDir)

                # All set, build the tools for the host system.
                ret = RunCmd('nmake.exe', None,
                             workingdir=shell_env.get_shell_var("EDK_TOOLS_PATH"))
                if ret != 0:
                    raise Exception("Failed to build base tools for host system.")

                # Copy the output to a temp directory
                TempFolder = os.path.join(shell_env.get_shell_var("EDK_TOOLS_PATH"), "BaseToolsBuild", "Temp")
                if not os.path.exists(TempFolder):
                    os.makedirs(TempFolder)
                for file in os.listdir(TempOutputDir):
                    shutil.copy(os.path.join(TempOutputDir, file), TempFolder)

                # Clean up the build output
                ret = RunCmd('nmake.exe', 'cleanall',
                             workingdir=shell_env.get_shell_var("EDK_TOOLS_PATH"))

                # Remove the entire TempOutputDir
                shutil.rmtree(TempOutputDir)

                shell_environment.RevertBuildVars()
                shell_env.insert_path(TempFolder)

            # # Update environment with required VC vars.
            interesting_keys = ["ExtensionSdkDir", "INCLUDE", "LIB"]
            interesting_keys.extend(
                ["LIBPATH", "Path", "UniversalCRTSdkDir", "UCRTVersion", "WindowsLibPath", "WindowsSdkBinPath"])
            interesting_keys.extend(
                ["WindowsSdkDir", "WindowsSdkVerBinPath", "WindowsSDKVersion", "VCToolsInstallDir"])
            vc_vars = QueryVcVariables(
                interesting_keys, VcToolChainArch, vs_version=self.tool_chain_tag.lower())
            for key in vc_vars.keys():
                logging.debug(f"Var - {key} = {vc_vars[key]}")
                if key.lower() == 'path':
                    shell_env.set_path(vc_vars[key])
                else:
                    shell_env.set_shell_var(key, vc_vars[key])

            # Note: This HOST_ARCH is in respect to the BUILT base tools, not the host arch where
            # this script is BUILDING the base tools.
            shell_env.set_shell_var('HOST_ARCH', self.target_arch)

            # Actually build the tools.
            output_stream = edk2_logging.create_output_stream()
            ret = RunCmd('nmake.exe', None,
                         workingdir=shell_env.get_shell_var("EDK_TOOLS_PATH"))
            edk2_logging.remove_output_stream(output_stream)
            problems = edk2_logging.scan_compiler_output(output_stream)
            for level, problem in problems:
                logging.log(level, problem)
            if ret != 0:
                raise Exception("Failed to build.")

            self.WritePathEnvFile(self.OutputDir)
            return ret

        elif self.tool_chain_tag.lower().startswith("gcc"):
            # Note: This HOST_ARCH is in respect to the BUILT base tools, not the host arch where
            # this script is BUILDING the base tools.
            HostInfo = GetHostInfo()
            prefix = None
            TargetInfoArch = None
            if self.target_arch is not None:
                shell_env.set_shell_var('HOST_ARCH', self.target_arch)

                if "AARCH64" in self.target_arch:
                    prefix = shell_env.get_shell_var("GCC5_AARCH64_PREFIX")
                    if prefix == None:
                        # now check for install dir.  If set then set the Prefix
                        install_path = shell_environment.GetEnvironment().get_shell_var("GCC5_AARCH64_INSTALL")

                        # make GCC5_AARCH64_PREFIX to align with tools_def.txt
                        prefix = os.path.join(install_path, "bin", "aarch64-none-linux-gnu-")

                    shell_environment.GetEnvironment().set_shell_var("GCC_PREFIX", prefix)
                    TargetInfoArch = "ARM"

                elif "ARM" in self.target_arch:
                    prefix = shell_env.get_shell_var("GCC5_ARM_PREFIX")
                    if prefix == None:
                        # now check for install dir.  If set then set the Prefix
                        install_path = shell_environment.GetEnvironment().get_shell_var("GCC5_ARM_INSTALL")

                        # make GCC5_ARM_PREFIX to align with tools_def.txt
                        prefix = os.path.join(install_path, "bin", "arm-none-linux-gnueabihf-")

                    shell_environment.GetEnvironment().set_shell_var("GCC_PREFIX", prefix)
                    TargetInfoArch = "ARM"

                else:
                    TargetInfoArch = "x86"
            else:
                self.target_arch = HostInfo.arch
                TargetInfoArch = HostInfo.arch
            # Otherwise, the built binary arch will be consistent with the host system

            # Added logic to support cross compilation scenarios
            if TargetInfoArch != HostInfo.arch:
                # this is defaulting to the version that comes with Ubuntu 20.04
                ver = shell_environment.GetBuildVars().GetValue("LIBUUID_VERSION", "2.34")
                work_dir = os.path.join(shell_env.get_shell_var("EDK_TOOLS_PATH"), self.GetLoggingFolderRelativeToRoot())
                pack_name = f"util-linux-{ver}"
                unzip_dir = os.path.join(work_dir, pack_name)

                if os.path.isfile(os.path.join(work_dir, f"{pack_name}.tar.gz")):
                    os.remove(os.path.join(work_dir, f"{pack_name}.tar.gz"))
                if os.path.isdir(unzip_dir):
                    shutil.rmtree(unzip_dir)

                # cross compiling, need to rebuild libuuid for the target
                ret = RunCmd("wget", f"https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/v{ver}/{pack_name}.tar.gz", workingdir=work_dir)
                if ret != 0:
                    raise Exception(f"Failed to download libuuid version {ver} - {ret}")

                ret = RunCmd("tar", f"xvzf {pack_name}.tar.gz", workingdir=work_dir)
                if ret != 0:
                    raise Exception(f"Failed to untar the downloaded file {ret}")

                # configure the source to use the cross compiler
                pack_name = f"util-linux-{ver}"
                if "AARCH64" in self.target_arch:
                    ret = RunCmd("sh", f"./configure --host=aarch64-linux  -disable-all-programs --enable-libuuid CC={prefix}gcc", workingdir=unzip_dir)
                elif "ARM" in self.target_arch:
                    ret = RunCmd("sh", f"./configure --host=arm-linux  -disable-all-programs --enable-libuuid CC={prefix}gcc", workingdir=unzip_dir)
                if ret != 0:
                    raise Exception(f"Failed to configure the util-linux to build with our gcc {ret}")

                ret = RunCmd("make", "", workingdir=unzip_dir)
                if ret != 0:
                    raise Exception(f"Failed to build the libuuid with our gcc {ret}")

                shell_environment.GetEnvironment().set_shell_var("CROSS_LIB_UUID", unzip_dir)
                shell_environment.GetEnvironment().set_shell_var("CROSS_LIB_UUID_INC", os.path.join(unzip_dir, "libuuid", "src"))

            ret = RunCmd("make", "clean", workingdir=shell_env.get_shell_var("EDK_TOOLS_PATH"))
            if ret != 0:
                raise Exception("Failed to build.")

            cpu_count = self.GetCpuThreads()

            output_stream = edk2_logging.create_output_stream()
            ret = RunCmd("make", f"-C .  -j {cpu_count}", workingdir=shell_env.get_shell_var("EDK_TOOLS_PATH"))
            edk2_logging.remove_output_stream(output_stream)
            problems = edk2_logging.scan_compiler_output(output_stream)
            for level, problem in problems:
                logging.log(level, problem)
            if ret != 0:
                raise Exception("Failed to build.")

            self.OutputDir = os.path.join(
                shell_env.get_shell_var("EDK_TOOLS_PATH"), "Source", "C", "bin")

            self.WritePathEnvFile(self.OutputDir)
            return ret

        logging.critical("Tool Chain not supported")
        return -1

    def GetCpuThreads(self) -> int:
        ''' Function to return number of cpus. If error return 1'''
        cpus = 1
        try:
            cpus = multiprocessing.cpu_count()
        except:
            # from the internet there are cases where cpu_count is not implemented.
            # will handle error by just doing single proc build
            pass
        return cpus



def main():
    Edk2ToolsBuild().Invoke()


if __name__ == "__main__":
    main()
