#!/usr/bin/python -ttu
# vim: ai ts=4 sts=4 et sw=4

#    Copyright (c) 2007 Intel Corporation
#
#    This program is free software; you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the Free
#    Software Foundation; version 2 of the License
#
#    This program is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
#    for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with this program; if not, write to the Free Software Foundation, Inc., 59
#    Temple Place - Suite 330, Boston, MA 02111-1307, USA.


import os
import platform
import string
import sys
import re
import traceback

from optparse import OptionParser

sys.path.insert(0, '/usr/share/pdk/lib')
import SDK
import mic_cfg
import pdk_utils


ENVIRONMENT_VARS = {
    'LANG' : 'C',
    'LC_ALL' : 'C',
}

debug = False
if mic_cfg.config.has_option('general', 'debug'):
    debug = int(mic_cfg.config.get('general', 'debug'))

# We will only load the GUI library code if we have no arguments specified
if len(sys.argv) == 1:
    try:
        import gui
    except RuntimeError:
        traceback.print_exc()
        # Probably running in a text console
        pass

sdk = SDK.SDK()

def main():
    if sys.hexversion < 0x2040000:
        print >> sys.stderr, "Error: %s depends on a Python version of at least 2.4!" % (sys.argv[0])
        sys.exit(1)
    # Make sure the executables we need are on the system
    verifyExes()
    # Set needed environment variables
    setEnvironmentVariables()
    if len(sys.argv) == 1:
        if not pdk_utils.areWeRoot():
            print "Error: Must run image creator with sudo or run as root"
            print "Example, run image creator:  sudo image-creator"
            sys.exit(1)
        if os.getenv("DISPLAY"):
            try:
                gui.App().run()
                return 0
            except:
                traceback.print_exc()
                print "Error: Unable to run the GUI, maybe you are in a text console"
                print "Do -h for help"
                return 1
        else:
            print
            print "Error: Your DISPLAY environment variable is NOT set"
            print "The Moblin Image Creator GUI can NOT run"
            return 1

    options, args = parseCommandLine()
    if options.cmd == "list-platforms":
        list_platforms()
    elif options.cmd == "clear-rootstraps":
        clear_rootstraps()
    elif options.cmd == "list-projects":
        list_projects()
    elif options.cmd == 'list-targets':
        list_targets(options)
    elif options.cmd == 'list-fsets':
        list_fsets(options)
    elif options.cmd == 'create-project':
        create_project(options)
    elif options.cmd == 'delete-project':
        delete_project(options)
    elif options.cmd == 'umount-project':
        umount_project(options)
    elif options.cmd == 'umount-target':
        umount_target(options)
    elif options.cmd == 'run-target':
        run_target(options)
    elif options.cmd == 'run-project':
        run_project(options)
    elif options.cmd == 'create-target':
        create_target(options)
    elif options.cmd == 'delete-target':
        delete_target(options)
    elif options.cmd == 'install-fset':
        install_fset(options)
    elif options.cmd == 'update-project':
        update_project(options)
    elif options.cmd == 'update-target':
        update_target(options)
    elif options.cmd in ['create-live-usb', 'create-live-usbrw', 'create-install-usb']:
        create_image(options)
    elif options.cmd == 'chroot-project':
        chroot_project(options)
    elif options.cmd == 'chroot-target':
        chroot_target(options)
    elif options.cmd == "save-project":
        save_project(options)
    elif options.cmd == "load-project":
        load_project(options)
    else:
        print "Unknown command specified"
        return 1
    sdk.umount()
    return 0
        

def parseCommandLine():
    valid_commands = {
        # command name,         requires root priviledge
        "chroot-project" :      1,
        "chroot-target" :       1,
        "clear-rootstraps" :    1,
        "create-install-usb" :  1,
        "create-live-usb" :     1,
        "create-live-usbrw" :   1,
        "create-project" :      1,
        "create-target" :       1,
        "delete-project" :      1,
        "delete-target" :       1,
        "install-fset" :        1,
        "list-fsets" :          0,
        "list-platforms" :      0,
        "list-projects" :       0,
        "list-targets" :        0,
        "load-project" :        1,
        "run-project" :         1,
        "run-target" :          1,
        "save-project" :        1,
        "umount-project" :      1,
        "umount-target" :       1,
        "update-project" :      1,
        "update-target" :       1,
    }
    valid_command_string = ""
    commands = valid_commands.keys()
    commands.sort()
    # Get all but the last one
    for command in commands[:-1]:
        valid_command_string += "%s, " %  command
    valid_command_string += "or %s" % commands[-1]

    parser = OptionParser(add_help_option=False)

    parser.add_option("-c", "--command", dest="cmd",
        help="Where CMD is one of: %s" % valid_command_string, metavar="CMD")
    parser.add_option("--platform-name", dest="platform_name",
        help="Platform name")
    parser.add_option("--project-name", dest="project_name",
        help="Project name")
    parser.add_option("--project-description", dest="project_desc",
        help="Project description")
    parser.add_option("--project-path", dest="project_path",
        help="Project path")
    parser.add_option("-t", "--target-name", dest="target_name",
        help="Target name")
    parser.add_option("--fset-name", dest="fset_name",
        help="Feature set identifier")
    parser.add_option("--image-name", dest="image_name",
        help="Name to use for target image file")
    parser.add_option("--run-command", dest="run_command",
        help="Command to execute inside a project or target chroot filesystem")
    parser.add_option("--bypass-rootstrap",
        action="store_false", dest="use_rootstrap", default=True,
        help="Disable creation or use of rootstrap files")
    parser.add_option("-d", "--enable-debug",
        action="store_true", dest="debug", default=False,
        help="Enable additional debug package while installing fsets")
    parser.add_option("-f", "--file-name", dest="file_name",
        help="Filename to use when saving or restoring a project, if .mic.tar.bz2 is not the extension during a save then .mic.tar.bz2 will be appended")
    parser.add_option("-h", "--help", help="show this help message and exit",
        dest = "help", action="store_true", default=False)
    examples = """
Examples:
<Adding a new project>
    image-creator --command=create-project \\
                  --platform-name='donley' \\
                  --project-name='MyProject' \\
                  --project-desc='Example project' \\
                  --project-path=/usr/src/projects/myproject

<Delete a project>
    image-creator --command=delete-project \\
                  --project-name='MyOtherProject'

<Unmount a project> Normally this will not be needed as projects are
                    automatically unmounted upon exit.
    image-creator --command=umount-project \\
                  --project-name='MyOtherProject'

<Unmount a target>  Normally this will not be needed as targets are
                    automatically unmounted upon exit
    image-creator --command=umount-target \\
                  --project-name='MyOtherProject' \\
                  --target-name='MyTarget'

<Run a command inside a project>
    image-creator --command=run-project \\
                  --project-name='MyOtherProject' \\
                  --run-command='dpkg -l'

<Run a command inside a target>
    image-creator --command=run-target \\
                  --project-name='MyOtherProject' \\
                  --target-name='MyTarget' \\
                  --run-command='dpkg -l'

<Adding a new target to an existing project>
    image-creator --command=create-target \\
                  --project-name='MyProject' \\
                  --target-name='MyTarget'

<Delete a target>
    image-creator --command=delete-target \\
                  --project-name='MyProject' \\
                  --target-name='MyOtherTarget'

<Installing a Function Set (fset) into a given target>
    image-creator --command=install-fset \\
                  --platform-name='donley' \\
                  --project-name='MyProject' \\
                  --target-name='MyTarget' \\
                  --fset='Core' \\

<Change into a given project buildroot filesystem>
    image-creator --command=chroot-project \\
                  --project-name='MyProject' \\

<Change into a given projects target filesystem>
    image-creator --command=chroot-target \\
                  --project-name='MyProject' \\
                  --target-name='MyTarget' \\

<Updating a given target inside a project>
    image-creator --command=update-target \\
                  --project-name='MyProject' \\
                  --target-name='MyTarget' \\

<Updating a given project>
    image-creator --command=update-project \\
                  --project-name='MyProject'

<Create a image>
    image-creator --command=|create-live-usb \\
                            |create-live-usbrw \\
                            |create-install-usb]  \\
                  --project-name='Myproject' \\
                  --target-name='MyTraget' \\
                  --image-name='MyImage'

<Save a project>
    image-creator --command=save-project \\
        --project-name='Myproject' \\
        --file-name='file_name_to_save_project'

<Load a saved project>
    image-creator --command=load-project \\
        --project-name='Myproject' \\
        --file-name='file_name_of_saved_project'
        --project-path='path_to_restore_project_to'
                  """

    (options, args) = parser.parse_args()
    # if nothing at all is passed in then return back
    if not options and not args:
        return options, args
    if options.help:
        parser.print_help()
        print
        print examples
        sys.exit(0)
    # now lets validate stuff
    if not options.cmd:
        parser.error("Must specify a -c/--command argument")
    if options.cmd not in valid_commands:
        parser.error("Command specified for option -c/--command invalid.  Valid commands are: %s" % valid_command_string)
    need_root = valid_commands[options.cmd]
    if need_root and not pdk_utils.areWeRoot():
        parser.error("Command specified requires you to run as root or using sudo.  Command is: %s" % options.cmd)
    # Our FSets are converted to lowecase, so do the same for the command line
    # argument
    if options.fset_name:
        options.fset_name = options.fset_name.lower()
    if options.cmd == 'list-targets' and not options.project_name:
        parser.error("Must specify a project-name when using %s command." % options.cmd)
    if options.cmd == "list-fsets" and not options.platform_name:
        parser.error("Must specify platform-name when using %s command" % options.cmd)
    if options.cmd == "create-project":
        if not options.platform_name:
            parser.error("Must specify platform-name when using %s command" % options.cmd)
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.project_desc:
            parser.error("Must specify project-description when using %s command" % options.cmd)
        if not options.project_path:
            parser.error("Must specify project-path when using %s command" % options.cmd)
    if options.cmd == "delete-project" and not options.project_name:
        parser.error("Must specify project-name when using %s command" % options.cmd)
    if options.cmd == "umount-project" and not options.project_name:
        parser.error("Must specify project-name when using %s command" % options.cmd)
    if options.cmd == "umount-target":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.target_name:
            parser.error("Must specify target-name when using %s command" % options.cmd)
    if options.cmd == "run-project":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.run_command:
            parser.error("Must specify run-command when using %s command" % options.cmd)
    if options.cmd == "run-target":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.target_name:
            parser.error("Must specify target-name when using %s command" % options.cmd)
        if not options.run_command:
            parser.error("Must specify run-command when using %s command" % options.cmd)
    if options.cmd == "update-project" and not options.project_name:
        parser.error("Must specify project-name when using %s command" % options.cmd)
    if options.cmd == "create-target" or options.cmd == "delete-target" or options.cmd == "update-target":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.target_name:
            parser.error("Must specify target-name when using %s command" % options.cmd)
    if options.cmd == "install-fset":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.target_name:
            parser.error("Must specify target-name when using %s command" % options.cmd)
        if not options.fset_name:
            parser.error("Must specify fset-name when using %s command" % options.cmd)
    if options.cmd == "create-live-usb" or options.cmd == "create-live-usbrw" or options.cmd == "create-install-usb":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.target_name:
            parser.error("Must specify target-name when using %s command" % options.cmd)
        if not options.image_name:
            parser.error("Must specify image-name when using %s command" % options.cmd)
    if options.cmd == "chroot-project":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
    if options.cmd == "chroot-target":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.target_name:
            parser.error("Must specify target-name when using %s command" % options.cmd)
    if options.cmd == "save-project" or options.cmd == "load-project":
        if not options.project_name:
            parser.error("Must specify project-name when using %s command" % options.cmd)
        if not options.file_name:
            parser.error("Must specify file-name when using %s command" % options.cmd)
    if options.cmd == "load-project":
        if not options.project_path:
            parser.error("Must specify project-path when using %s command" % options.cmd)
        if not options.file_name.endswith('.mic.tar.bz2'):
            parser.error("Filename must have the extension: .mic.tar.bz2")
    if options.project_path:
        options.project_path = os.path.realpath(os.path.abspath(os.path.expanduser(options.project_path)))
    if options.file_name:
        options.file_name = os.path.realpath(os.path.abspath(os.path.expanduser(options.file_name)))

    return options, args

def clear_rootstraps():
    sdk.clear_rootstraps()
    
def list_platforms():
    for key in sorted(sdk.platforms.iterkeys()):
        print "%s" % (sdk.platforms[key].name)

def list_projects():
    for key in sorted(sdk.projects.iterkeys()):
        print "%s ==> %s" % (sdk.projects[key].name, sdk.projects[key].path)

def list_fsets(options):
    if options.platform_name not in sdk.platforms:
        print >> sys.stderr, "%s: is not a valid platform name" % options.platform_name
        sys.exit(1)
    for key in sorted(sdk.platforms[options.platform_name].fset.iterkeys()):
        print "%s" % (sdk.platforms[options.platform_name].fset[key].name)

def list_targets(options):
    for key in (sdk.projects[options.project_name].targets.iterkeys()):
        print "%s" % (sdk.projects[options.project_name].targets[key].name)

def create_project(options):
    if options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s already exist!" % (options.project_name)
        sys.exit(1)
    if options.platform_name not in sdk.platforms:
        print >> sys.stderr, "%s: is not a valid platform name" % options.platform_name
        sys.exit(1)
    cwd = os.getcwd()
    if cwd.find(options.project_path) == 0:
	    print >> sys.stderr, "The project path can not be part of the current working directory"
	    sys.exit(1)
    proj = sdk.create_project(options.project_path, options.project_name, options.project_desc, sdk.platforms[options.platform_name], options.use_rootstrap)
    proj.install()

def delete_project(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    sdk.delete_project(options.project_name)

def umount_project(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    for target in sdk.projects[options.project_name].targets:
        sdk.projects[options.project_name].targets[target].umount()
    sdk.projects[options.project_name].umount()

def umount_target(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    if not options.target_name in sdk.projects[options.project_name].targets:
        print >> sys.stderr, "Target %s does not exist" % (options.project_name)
        sys.exit(1)
    sdk.projects[options.project_name].targets[options.target_name].umount()

def run_project(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    sys.exit(sdk.projects[options.project_name].chroot(options.run_command))

def run_target(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    if not options.target_name in sdk.projects[options.project_name].targets:
        print >> sys.stderr, "Target %s does not exist" % (options.project_name)
        sys.exit(1)
    sys.exit(sdk.projects[options.project_name].targets[options.target_name].chroot(options.run_command))

def update_project(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    sdk.projects[options.project_name].update()
    
def create_target(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    if re.search(r'\W', options.target_name):
        print >> sys.stderr, "Target names can only contain alpha/numeric characters"
        sys.exit(1)
    target = sdk.projects[options.project_name].create_target(options.target_name, options.use_rootstrap)

def delete_target(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    proj = sdk.projects[options.project_name]
    if not options.target_name in proj.targets:
        print >> sys.stderr, "Target %s does not exist" % (options.target_name)
        sys.exit(1)
    proj.delete_target(options.target_name)

def update_target(options):
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    proj = sdk.projects[options.project_name]
    if not options.target_name in proj.targets:
        print >> sys.stderr, "Target %s does not exist" % (options.target_name)
        sys.exit(1)
    proj.targets[options.target_name].update()
    
def install_fset(options):
    if options.platform_name:
        print "Ignoring Plaform name %s" % (options.platform_name)
        print "Using %s" % (sdk.projects[options.project_name].platform.name)
    if not options.project_name in sdk.projects:
        print >> sys.stderr, "Project %s does not exist" % (options.project_name)
        sys.exit(1)
    platform_name = sdk.projects[options.project_name].platform.name
    if not options.target_name in sdk.projects[options.project_name].targets:
        print >> sys.stderr, "Target %s does not exist" % (options.target_name)
        sys.exit(1)
    if not options.fset_name in sdk.platforms[platform_name].fset:
        print >> sys.stderr, "Feature set %s does not exist" % (options.fset_name)
        sys.exit(1)
    if platform_name not in sdk.platforms:
        print >> sys.stderr, "%s: is not a valid platform name" % platform_name
        sys.exit(1)
    fset = sdk.platforms[platform_name].fset[options.fset_name]
    fsets = sdk.platforms[platform_name].fset
    target = sdk.projects[options.project_name].targets[options.target_name]
    target.installFset(fset, fsets = fsets, debug_pkgs = options.debug)

def create_image(options):
    proj = sdk.projects[options.project_name]
    if options.cmd == 'create-live-usb':
        proj.create_live_usb(options.target_name, options.image_name)
    elif options.cmd == 'create-live-usbrw':
        proj.create_live_usb(options.target_name, options.image_name, 'EXT3FS')
    elif options.cmd == 'create-install-usb':
        proj.create_install_usb(options.target_name, options.image_name)

def chroot_project(options):
    proj = sdk.projects[options.project_name]
    proj.mount()
    os.system("chroot %s su -" % (proj.path))
    
def chroot_target(options):
    target = sdk.projects[options.project_name].targets[options.target_name]
    target.mount()
    os.system("chroot %s su -" % (target.path))

def save_project(options):
    project = sdk.save_project(options.project_name, options.file_name)

def load_project(options):
    project = sdk.load_project(options.project_name, options.project_path, options.file_name)

def print_exc_plus(type, value, tb):
    # From Python Cookbook 2nd Edition.  FIXME: Will need to remove this at
    # some point, or give attribution.
    # This is a modified version of recipe 8.6
    """ Print the usual traceback information, followed by a listing of
        all the local variables in each frame.
    """
    while tb.tb_next:
        tb = tb.tb_next
    stack = []
    f = tb.tb_frame
    while f:
        stack.append(f)
        f = f.f_back
    stack.reverse()
    traceback.print_exception(type, value, tb)
    print "Locals by frame, innermost last"
    for frame in stack:
        print
        print "Frame %s in %s at line %s" % (frame.f_code.co_name,
                                             frame.f_code.co_filename,
                                             frame.f_lineno)
        for key, value in frame.f_locals.items():
            print "\t%20s = " % key,
            # we must _absolutely_ avoid propagating exceptions, and str(value)
            # COULD cause any exception, so we MUST catch any...:
            try:
                print value
            except:
                print "<ERROR WHILE PRINTING VALUE>"
    traceback.print_exception(type, value, tb)

def findExe(exe_name, env=None):
    if env is None:
        env = os.environ
    if 'PATH' in env:
        env_list = env['PATH'].split(os.pathsep)
    else:
        print "Error: No 'PATH' environment variable defined"
        sys.exit(1)
    for dirname in env_list:
        file_name = os.path.join(dirname, exe_name)
        # FIXME: Should really check to see if it is executable, but for now
        # assume if it is in the path it is executable
        if os.path.exists(file_name):
            return True
    return False

def verifyExes():
    dist = "distribution.%s" %  mic_cfg.config.get('general', 'distribution')
    if not mic_cfg.config.has_section(dist):
        print "Unsupported distribution: %s" % dist
        sys.exit(1)
    required_exes = mic_cfg.config.get(dist, 'required_exes')
    exe_list = required_exes.split()
    exes_found = True
    for exe_name in exe_list:
        if not findExe(exe_name):
            print "Error: Could not find the executable: '%s' in the PATH" % exe_name
            exes_found = False
    if not exes_found:
        print "Without the executable(s) listed, image-creator will fail to operate properly."
        sys.exit(1)

def setEnvironmentVariables():
    for key, value in ENVIRONMENT_VARS.iteritems():
        os.environ[key] =  value

if __name__ == '__main__':
    if debug: sys.excepthook = print_exc_plus
    sys.exit(main())
