#! /usr/bin/env python
# Part of the A-A-P recipe executive: The main function.

# Copyright (C) 2002-2003 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING

# Main Python code that executes an A-A-P recipe

import sys
from traceback import print_exception, format_exception

# Each phase of executing a recipe is done by one module
from DoAddDef import doadddef
from DoArgs import doargs
from DoBuild import dobuild
from DoRead import doread
from DoInstall import doinstall

from Cache import dump_cache
from Error import *
from Sign import sign_write_all, sign_clear_all
from Util import *
import Global
from Message import *

# Globals
exit_status = 0             # exit status; default is zero (success)
exit_info = None            # exception stack when something went wrong.

def error_msg(recdict, msg):
    """Print an error message and set the exit status to one."""
    global exit_status
    msg_error(recdict, msg)
    if exit_status == 0:
        exit_status = 1

profiling = 0

def do_the_work(argv, find_recipe, commands):
    """Common function for main() and execute().
       "argv" is the list of command line arguments (excluding the program
       name).
       When "find_recipe" is non-zero, search for a recipe to load.
       When "commands" is None, execute the specified or default target(s).
       When "commands" is empty, do nothing.
       When "commands" is a non-empty string, execute these commands.
       Returns an error message and a work object.
       exit_status is set to a non-zero value when something failed.
    """
    global exit_status, exit_info
    exit_status = 0
    exit_info = None

    # We require Python 1.5 or later.
    if sys.version[0] == '1' and int(sys.version[2]) < 5:
        exit_status = 1
        return "A-A-P requires Python version 1.5 or later.", None

    # Need to know the directory of this module.  But __file__ isn't defined
    # when executed directly, then use argv[0] instead, see below.
    if not Global.aap_rootdir:
        Global.set_aap_rootdir(os.path.abspath(os.path.dirname(__file__)))

    # Internationalisation inits: setlocale and gettext.
    i18n_init()

    #
    # Do the main work.
    #
    msg = None
    work = None

    try:
        # 1. Process the command line arguments.
        Global.cmd_args = doargs(argv)

        if Global.cmd_args.has_option("verbose"):
            Global.cmd_args.printit()
        
        # When profiling is requested and it wasn't started yet, start all over
        # with profiling enabled.
        global profiling
        if not profiling and Global.cmd_args.has_option("profile"):
            profiling = 1
            import profile
            prof = profile.Profile()
            try:
                res = prof.runcall(do_the_work, argv, find_recipe, commands)
            finally:
                prof.dump_stats(Global.cmd_args.options.get("profile"))
            return res

        # When "--install pkg" is used: install a package.
        if Global.cmd_args.has_option("install"):
            work = doinstall(Global.cmd_args.options.get("install"))
        else:
            # 2. Read the recipe and included recipes.  Assignments and commands
            #    are executed directly, rules and dependencies are stored.
            #    "work" is a Work object with collected items from the recipe.
            work = doread(find_recipe)

            # 3. Add the default dependencies
            doadddef(work, work.recdict, 1)

            # 4. Build each target or execute the commands.
            if commands:
                from ParsePos import ParsePos
                from RecPos import RecPos
                from Process import Process

                fp = ParsePos([ RecPos(_('execute()'), 0) ],
                                                      string = commands + '\n')
                Process(fp, work.recdict, 0)
            elif commands is None:
                dobuild(work)

    except NormalExit:  # planned exit
        pass
    except SystemExit, r:
        exit_status = r
        msg = _("Aborted")
    except KeyboardInterrupt:
        exit_status = 1
        msg = _("Interrupted")
    except UserError, e:
        exit_status = 1
        msg = e.args
    except SyntaxError, e:
        exit_status = 1
        exit_info = sys.exc_info()
        msg = _("Syntax error") + str(e)
    except:
        exit_status = 1
        exit_info = sys.exc_info()
        msg = _("Internal Error")

    if work:
        # Dump entries for the downloaded files.
        dump_cache(work.recdict)

        # Dump the sign files.  Clear all signatures (for any next run).
        sign_write_all(work.recdict)
        sign_clear_all()

        # Close any subshell for installing packages.
        from Port import close_sushell
        close_sushell(work.recdict)

        # Close any root shell.
        close_rootshell(work.recdict)

        # Cleanup for any configure checks.
        import DoConf
        DoConf.cleanup(work.recdict)

    return msg, work


def main(setroot = 0):
    """
    The main function to execute an A-A-P recipe.
    """

    if setroot:
        # We need to know the location of our modules (find ccskim there).
        try:
            progname = os.path.realpath(sys.argv[0])
        except:
            # Doesn't have os.path.realpath(), it's new in Python 2.2
            # Use our copy of it.
            try:
                progname = myrealpath(sys.argv[0])
            except:
                # Still not working?  Fall back to using abspath().
                progname = os.path.abspath(sys.argv[0])

        Global.set_aap_rootdir(os.path.dirname(progname))

        # When started with a relative path and changing directories we still
        # need to be able to find our modules.
        sys.path.append(Global.aap_rootdir)

    # Do the work.
    msg, work = do_the_work(sys.argv[1:], 1, None)
    handle_work_done(msg, work)
    sys.exit(exit_status)


def handle_work_done(msg, work):
    """Handle errors after calling do_the_work()."""
    if msg:
        if work:
            error_msg(work.recdict, msg)
        else:
            error_msg(None, msg)
        if exit_info:
            print_exception(exit_info[0], exit_info[1], exit_info[2])
            error_msg(None, string.join(format_exception(exit_info[0],
                                                  exit_info[1], exit_info[2])))

    # Get the log file name before it's cleared by stopping the log.
    # Stop the log before the message, so that it doesn't get into the log.
    if exit_status:
        if msg_logname():
            logmsg = "More info in the logfile: " + msg_logname()
        else:
            logmsg = ''
    msg_stoplog()

    if exit_status and logmsg:
        if work:
            msg_info(work.recdict, logmsg)
        else:
            msg_info(None, logmsg)


# Return a canonical path (i.e. the absolute location of a file on the
# filesystem).  This is from os.path of Python 2.2.
def myrealpath(filename):
    """Return the canonical path of the specified filename, eliminating any
    symbolic links encountered in the path."""
    filename = os.path.abspath(filename)

    bits = ['/'] + filename.split('/')[1:]
    for i in range(2, len(bits)+1):
        component = apply(os.path.join, tuple(bits[0:i]))
        if os.path.islink(component):
            resolved = os.readlink(component)
            (dir, file) = os.path.split(component)
            resolved = os.path.normpath(os.path.join(dir, resolved))
            newpath = apply(os.path.join, tuple([resolved] + bits[i:]))
            return myrealpath(newpath)

    return filename


# When executed directly, call the main function.
if __name__ == '__main__':
    main(1)


#
# Other programs may call this funtion to execute one or more recipe commands.
# For example: execute(":do view thisfile")
#
def execute(commands, argv = [], find_recipe = 0):
    """Execute recipe commands "commands".  See do_the_work() for details about
       "commands".
       "argv" is a list of command line arguments.
       "find_recipe" is non-zero to find a default recipe.
       Returns an error message or None."""
    msg, work = do_the_work(argv, find_recipe, commands)

    if exit_info:
        print msg
        print_exception(exit_info[0], exit_info[1], exit_info[2])

    msg_stoplog()

    return msg


#
# Other programs may call this function to fetch a list of files.
# Uses the "main.aap" recipe, unless "argv" specifies another recipe to use.
#
def fetch(fnames, argv = []):
    """Fetch files in list "fnames".
       "argv" is a list of command line arguments.
       Will search for a default recipe if none is specified.
       Returns an error message or None."""
    from Dictlist import list2str
    return execute(":fetch %s" % list2str(fnames), argv, 1)


#
# Other programs may call this function to obtain the list of nodes and
# recdict.  It was added to be used by the IDE.
# Uses the "main.aap" recipe, unless "argv" specifies another recipe to use.
#
def get_nodelist(argv = []):
    """Obtain the information from the recipe specified with "argv" or the
       default recipe.
       Return a tuple with the list of nodes and a dictionary for the global
       variables."""
    msg, work = do_the_work(argv, 1, '')
    handle_work_done(msg, work)

    if not work:
        return [], {}
    return work.nodes.values(), work.recdict


def get_actionlist(argv = []):
    """Obtain a dictionary that lists the actions supported for each file
       type.  Can be used to make sure that executing the action will actually
       work.  "argv" is usually empty."""
    msg, work = do_the_work(argv, 0, '')
    handle_work_done(msg, work)

    if not work:
        return {}

    import Action
    return Action.action_get_list()



# vim: set sw=4 et sts=4 tw=79 fo+=l:
