## @file
# Update build revisions of the tools when performing a developer build
#
# This script will modife the C/Include/Common/BuildVersion.h file and the two
# Python scripts, Python/Common/BuildVersion.py and Python/UPT/BuildVersion.py.
# If SVN is available, the tool will obtain the current checked out version of
# the source tree for including the the --version commands.

#  Copyright (c) 2014 - 2015, Intel Corporation. All rights reserved.<BR>
#
#  This program and the accompanying materials
#  are licensed and made available under the terms and conditions of the BSD License
#  which accompanies this distribution.  The full text of the license may be found at
#  http://opensource.org/licenses/bsd-license.php
#
#  THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
#  WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
##
""" This program will update the BuildVersion.py and BuildVersion.h files used to set a tool's version value """
from __future__ import absolute_import

import os
import shlex
import subprocess
import sys

from argparse import ArgumentParser, SUPPRESS
from tempfile import NamedTemporaryFile
from types import IntType, ListType


SYS_ENV_ERR = "ERROR : %s system environment variable must be set prior to running this tool.\n"

__execname__ = "UpdateBuildVersions.py"
SVN_REVISION = "$LastChangedRevision: 3 $"
SVN_REVISION = SVN_REVISION.replace("$LastChangedRevision:", "").replace("$", "").strip()
__copyright__ = "Copyright (c) 2014, Intel Corporation. All rights reserved."
VERSION_NUMBER = "0.7.0"
__version__ = "Version %s.%s" % (VERSION_NUMBER, SVN_REVISION)


def ParseOptions():
    """
    Parse the command-line options.
    The options for this tool will be passed along to the MkBinPkg tool.
    """
    parser = ArgumentParser(
        usage=("%s [options]" % __execname__),
        description=__copyright__,
        conflict_handler='resolve')

    # Standard Tool Options
    parser.add_argument("--version", action="version",
                        version=__execname__ + " " + __version__)
    parser.add_argument("-s", "--silent", action="store_true",
                        dest="silent",
                        help="All output will be disabled, pass/fail determined by the exit code")
    parser.add_argument("-v", "--verbose", action="store_true",
                        dest="verbose",
                        help="Enable verbose output")
    # Tool specific options
    parser.add_argument("--revert", action="store_true",
                        dest="REVERT", default=False,
                        help="Revert the BuildVersion files only")
    parser.add_argument("--svn-test", action="store_true",
                        dest="TEST_SVN", default=False,
                        help="Test if the svn command is available")
    parser.add_argument("--svnFlag", action="store_true",
                        dest="HAVE_SVN", default=False,
                        help=SUPPRESS)

    return(parser.parse_args())


def ShellCommandResults(CmdLine, Opt):
    """ Execute the comand, returning the output content """
    file_list = NamedTemporaryFile(delete=False)
    filename = file_list.name
    Results = []

    returnValue = 0
    try:
        subprocess.check_call(args=shlex.split(CmdLine), stderr=subprocess.STDOUT, stdout=file_list)
    except subprocess.CalledProcessError as err_val:
        file_list.close()
        if not Opt.silent:
            sys.stderr.write("ERROR : %d : %s\n" % (err_val.returncode, err_val.__str__()))
            if os.path.exists(filename):
                sys.stderr.write("      : Partial results may be in this file: %s\n" % filename)
            sys.stderr.flush()
        returnValue = err_val.returncode

    except IOError as (errno, strerror):
        file_list.close()
        if not Opt.silent:
            sys.stderr.write("I/O ERROR : %s : %s\n" % (str(errno), strerror))
            sys.stderr.write("ERROR : this command failed : %s\n" % CmdLine)
            if os.path.exists(filename):
                sys.stderr.write("      : Partial results may be in this file: %s\n" % filename)
            sys.stderr.flush()
        returnValue = errno

    except OSError as (errno, strerror):
        file_list.close()
        if not Opt.silent:
            sys.stderr.write("OS ERROR : %s : %s\n" % (str(errno), strerror))
            sys.stderr.write("ERROR : this command failed : %s\n" % CmdLine)
            if os.path.exists(filename):
                sys.stderr.write("      : Partial results may be in this file: %s\n" % filename)
            sys.stderr.flush()
        returnValue = errno

    except KeyboardInterrupt:
        file_list.close()
        if not Opt.silent:
            sys.stderr.write("ERROR : Command terminated by user : %s\n" % CmdLine)
            if os.path.exists(filename):
                sys.stderr.write("      : Partial results may be in this file: %s\n" % filename)
            sys.stderr.flush()
        returnValue = 1

    finally:
        if not file_list.closed:
            file_list.flush()
            os.fsync(file_list.fileno())
            file_list.close()

    if os.path.exists(filename):
        fd_ = open(filename, 'r')
        Results = fd_.readlines()
        fd_.close()
        os.unlink(filename)

    if returnValue > 0:
        return returnValue

    return Results


def UpdateBuildVersionPython(Rev, UserModified, opts):
    """ This routine will update the BuildVersion.h files in the C source tree """
    for SubDir in ["Common", "UPT"]:
        PyPath = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source", "Python", SubDir)
        BuildVersionPy = os.path.join(PyPath, "BuildVersion.py")
        fd_ = open(os.path.normpath(BuildVersionPy), 'r')
        contents = fd_.readlines()
        fd_.close()
        if opts.HAVE_SVN is False:
            BuildVersionOrig = os.path.join(PyPath, "orig_BuildVersion.py")
            fd_ = open (BuildVersionOrig, 'w')
            for line in contents:
                fd_.write(line)
            fd_.flush()
            fd_.close()
        new_content = []
        for line in contents:
            if line.strip().startswith("gBUILD_VERSION"):
                new_line = "gBUILD_VERSION = \"Developer Build based on Revision: %s\"" % Rev
                if UserModified:
                    new_line = "gBUILD_VERSION = \"Developer Build based on Revision: %s with Modified Sources\"" % Rev
                new_content.append(new_line)
                continue
            new_content.append(line)

        fd_ = open(os.path.normpath(BuildVersionPy), 'w')
        for line in new_content:
            fd_.write(line)
        fd_.close()


def UpdateBuildVersionH(Rev, UserModified, opts):
    """ This routine will update the BuildVersion.h files in the C source tree """
    CPath = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source", "C", "Include", "Common")
    BuildVersionH = os.path.join(CPath, "BuildVersion.h")
    fd_ = open(os.path.normpath(BuildVersionH), 'r')
    contents = fd_.readlines()
    fd_.close()
    if opts.HAVE_SVN is False:
        BuildVersionOrig = os.path.join(CPath, "orig_BuildVersion.h")
        fd_ = open(BuildVersionOrig, 'w')
        for line in contents:
            fd_.write(line)
        fd_.flush()
        fd_.close()

    new_content = []
    for line in contents:
        if line.strip().startswith("#define"):
            new_line = "#define __BUILD_VERSION \"Developer Build based on Revision: %s\"" % Rev
            if UserModified:
                new_line = "#define __BUILD_VERSION \"Developer Build based on Revision: %s with Modified Sources\"" % \
                            Rev
            new_content.append(new_line)
            continue
        new_content.append(line)

    fd_ = open(os.path.normpath(BuildVersionH), 'w')
    for line in new_content:
        fd_.write(line)
    fd_.close()


def RevertCmd(Filename, Opt):
    """ This is the shell command that does the SVN revert """
    CmdLine = "svn revert %s" % Filename.replace("\\", "/").strip()
    try:
        subprocess.check_output(args=shlex.split(CmdLine))
    except subprocess.CalledProcessError as err_val:
        if not Opt.silent:
            sys.stderr.write("Subprocess ERROR : %s\n" % err_val)
            sys.stderr.flush()

    except IOError as (errno, strerror):
        if not Opt.silent:
            sys.stderr.write("I/O ERROR : %d : %s\n" % (str(errno), strerror))
            sys.stderr.write("ERROR : this command failed : %s\n" % CmdLine)
            sys.stderr.flush()

    except OSError as (errno, strerror):
        if not Opt.silent:
            sys.stderr.write("OS ERROR : %d : %s\n" % (str(errno), strerror))
            sys.stderr.write("ERROR : this command failed : %s\n" % CmdLine)
            sys.stderr.flush()

    except KeyboardInterrupt:
        if not Opt.silent:
            sys.stderr.write("ERROR : Command terminated by user : %s\n" % CmdLine)
            sys.stderr.flush()

    if Opt.verbose:
        sys.stdout.write("Reverted this file: %s\n" % Filename)
        sys.stdout.flush()


def GetSvnRevision(opts):
    """ Get the current revision of the BaseTools/Source tree, and check if any of the files have been modified """
    Revision = "Unknown"
    Modified = False

    if opts.HAVE_SVN is False:
        sys.stderr.write("WARNING: the svn command-line tool is not available.\n")
        return (Revision, Modified)

    SrcPath = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source")
    # Check if there are modified files.
    Cwd = os.getcwd()
    os.chdir(SrcPath)

    StatusCmd = "svn st -v --depth infinity --non-interactive"
    contents = ShellCommandResults(StatusCmd, opts)
    os.chdir(Cwd)
    if type(contents) is ListType:
        for line in contents:
            if line.startswith("M "):
                Modified = True
                break

    # Get the repository revision of BaseTools/Source
    InfoCmd = "svn info %s" % SrcPath.replace("\\", "/").strip()
    Revision = 0
    contents = ShellCommandResults(InfoCmd, opts)
    if type(contents) is IntType:
        return 0, Modified
    for line in contents:
        line = line.strip()
        if line.startswith("Revision:"):
            Revision = line.replace("Revision:", "").strip()
            break

    return (Revision, Modified)


def CheckSvn(opts):
    """
    This routine will return True if an svn --version command succeeds, or False if it fails.
    If it failed, SVN is not available.
    """
    OriginalSilent = opts.silent
    opts.silent = True
    VerCmd = "svn --version"
    contents = ShellCommandResults(VerCmd, opts)
    opts.silent = OriginalSilent
    if type(contents) is IntType:
        if opts.verbose:
            sys.stdout.write("SVN does not appear to be available.\n")
            sys.stdout.flush()
        return False

    if opts.verbose:
        sys.stdout.write("Found %s" % contents[0])
        sys.stdout.flush()
    return True


def CopyOrig(Src, Dest, Opt):
    """ Overwrite the Dest File with the Src File content """
    try:
        fd_ = open(Src, 'r')
        contents = fd_.readlines()
        fd_.close()
        fd_ = open(Dest, 'w')
        for line in contents:
            fd_.write(line)
        fd_.flush()
        fd_.close()
    except IOError:
        if not Opt.silent:
            sys.stderr.write("Unable to restore this file: %s\n" % Dest)
            sys.stderr.flush()
        return 1

    os.remove(Src)
    if Opt.verbose:
        sys.stdout.write("Restored this file: %s\n" % Src)
        sys.stdout.flush()

    return 0


def CheckOriginals(Opts):
    """
    If SVN was not available, then the tools may have made copies of the original BuildVersion.* files using
    orig_BuildVersion.* for the name. If they exist, replace the existing BuildVersion.* file with the corresponding
    orig_BuildVersion.* file.
    Returns 0 if this succeeds, or 1 if the copy function fails. It will also return 0 if the orig_BuildVersion.* file
    does not exist.
    """
    CPath = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source", "C", "Include", "Common")
    BuildVersionH = os.path.join(CPath, "BuildVersion.h")
    OrigBuildVersionH = os.path.join(CPath, "orig_BuildVersion.h")
    if not os.path.exists(OrigBuildVersionH):
        return 0
    if CopyOrig(OrigBuildVersionH, BuildVersionH, Opts):
        return 1
    for SubDir in ["Common", "UPT"]:
        PyPath = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source", "Python", SubDir)
        BuildVersionPy = os.path.join(PyPath, "BuildVersion.h")
        OrigBuildVersionPy = os.path.join(PyPath, "orig_BuildVersion.h")
        if not os.path.exists(OrigBuildVersionPy):
            return 0
        if CopyOrig(OrigBuildVersionPy, BuildVersionPy, Opts):
            return 1

    return 0


def RevertBuildVersionFiles(opts):
    """
    This routine will attempt to perform an SVN --revert on each of the BuildVersion.* files
    """
    if not opts.HAVE_SVN:
        if CheckOriginals(opts):
            return 1
        return 0
    # SVN is available
    BuildVersionH = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source", "C", "Include", "Common", "BuildVersion.h")
    RevertCmd(BuildVersionH, opts)
    for SubDir in ["Common", "UPT"]:
        BuildVersionPy = os.path.join(os.environ['BASE_TOOLS_PATH'], "Source", "Python", SubDir, "BuildVersion.py")
        RevertCmd(BuildVersionPy, opts)

def UpdateRevisionFiles():
    """ Main routine that will update the BuildVersion.py and BuildVersion.h files."""
    options = ParseOptions()
    # Check the working environment
    if "WORKSPACE" not in os.environ.keys():
        sys.stderr.write(SYS_ENV_ERR % 'WORKSPACE')
        return 1
    if 'BASE_TOOLS_PATH' not in os.environ.keys():
        sys.stderr.write(SYS_ENV_ERR % 'BASE_TOOLS_PATH')
        return 1
    if not os.path.exists(os.environ['BASE_TOOLS_PATH']):
        sys.stderr.write("Unable to locate the %s directory." % os.environ['BASE_TOOLS_PATH'])
        return 1


    options.HAVE_SVN = CheckSvn(options)
    if options.TEST_SVN:
        return (not options.HAVE_SVN)
    # done processing the option, now use the option.HAVE_SVN as a flag. True = Have it, False = Don't have it.
    if options.REVERT:
        # Just revert the tools an exit
        RevertBuildVersionFiles(options)
    else:
        # Revert any changes in the BuildVersion.* files before setting them again.
        RevertBuildVersionFiles(options)
        Revision, Modified = GetSvnRevision(options)
        if options.verbose:
            sys.stdout.write("Revision: %s is Modified: %s\n" % (Revision, Modified))
            sys.stdout.flush()
        UpdateBuildVersionH(Revision, Modified, options)
        UpdateBuildVersionPython(Revision, Modified, options)

    return 0


if __name__ == "__main__":
    sys.exit(UpdateRevisionFiles())