#!/usr/bin/python
#
# makeupdates - Generate an updates.img containing changes since the last
#               tag, but only changes to the main anaconda runtime.
#               initrd/stage1 updates have to be created separately.
#
# Copyright (C) 2009  Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Author: David Cantrell <dcantrell@redhat.com>

import getopt
import os
import shutil
import subprocess
import sys

def getArchiveTag(configure, spec):
    tag = ""

    f = open(configure)
    lines = f.readlines()
    f.close()

    for line in lines:
        if line.startswith('AC_INIT('):
            fields = line.split('[')
            tag += fields[1].split(']')[0] + '-' + fields[2].split(']')[0]
            break
        else:
            continue

    f = open(spec)
    lines = f.readlines()
    f.close()

    for line in lines:
        if line.startswith('Release:'):
            tag += '-' + line.split()[1].split('%')[0]
        else:
            continue

    return tag

def getArchiveTagOffset(configure, spec, offset):
    tag = getArchiveTag(configure, spec)

    if not tag.count("-") >= 2:
        return tag
    ldash = tag.rfind("-")
    bldash = tag[:ldash].rfind("-")
    ver = tag[bldash+1:ldash]

    if not ver.count(".") >= 1:
        return tag
    ver = ver[:ver.rfind(".")]

    if not len(ver) > 0:
        return tag
    globstr = "refs/tags/" + tag[:bldash+1] + ver + ".*"
    proc = subprocess.Popen(['git', 'for-each-ref', '--sort=taggerdate',
                             '--format=%(tag)', globstr],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE).communicate()
    lines = proc[0].strip("\n").split('\n')
    lines.reverse()

    try:
        return lines[offset]
    except IndexError:
        return tag

def doGitDiff(tag, args=[]):
    proc = subprocess.Popen(['git', 'diff', '--name-status', tag] + args,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE).communicate()

    lines = proc[0].split('\n')
    return lines

def copyUpdatedFiles(tag, updates, cwd):
    def pruneFile(src, names):
        lst = []

        for name in names:
            if name.startswith('Makefile') or name.endswith('.pyc'):
                lst.append(name)

        return lst

    def install_to_dir(fname, relpath):
        sys.stdout.write("Including %s\n" % fname)
        outdir = os.path.join(updates, relpath)
        if not os.path.isdir(outdir):
            os.makedirs(outdir)
        shutil.copy2(file, outdir)

    subdirs = []

    # Updates get overlaid onto the runtime filesystem. Anaconda expects them
    # to be in /run/install/updates, so put them in
    # $updatedir/run/install/updates.
    tmpupdates = updates.rstrip('/')
    if not tmpupdates.endswith("/run/install/updates"):
        tmpupdates = os.path.join(tmpupdates, "run/install/updates")

    lines = doGitDiff(tag)
    for line in lines:
        fields = line.split()

        if len(fields) < 2:
            continue

        status = fields[0]
        file = fields[1]

        if status == "D":
            continue

        if file.endswith('.spec.in') or (file.find('Makefile') != -1) or \
           file.endswith('.c') or file.endswith('.h') or \
           file.endswith('.sh') or file == 'configure.ac':
            continue

        if file.endswith('.glade'):
            # Some UI files should go under ui/<dir> where dir is the
            # directory above the file.glade
            dir_parts = os.path.dirname(file).split(os.path.sep)
            g_idx = dir_parts.index("gui")
            uidir = os.path.sep.join(dir_parts[g_idx+1:])
            path_comps = [tmpupdates, "ui"]
            if uidir:
                path_comps.append(uidir)
            install_to_dir(file, os.path.join(*path_comps))
        elif file.startswith('pyanaconda/'):
            # pyanaconda stuff goes into /tmp/updates/[path]
            dirname = os.path.join(tmpupdates, os.path.dirname(file))
            install_to_dir(file, dirname)
        elif file == 'anaconda':
            # anaconda itself we just overwrite
            install_to_dir(file, "usr/sbin")
        elif file.endswith('.service') or file.endswith(".target"):
            # same for systemd services
            install_to_dir(file, "lib/systemd/system")
        elif file.endswith('/anaconda-generator'):
            # yeah, this should probably be more clever..
            install_to_dir(file, "lib/systemd/system-generators")
        elif file == "data/tmux.conf":
            install_to_dir(file, "usr/share/anaconda")
        elif file == "data/anaconda-gtk.css":
            install_to_dir(file, "usr/share/anaconda")
        elif file == "data/interactive-defaults.ks":
            install_to_dir(file, "usr/share/anaconda")
        elif file == "data/liveinst/liveinst":
            install_to_dir(file, "usr/sbin")
        elif file.startswith("data/pixmaps"):
            install_to_dir(file, "usr/share/anaconda/pixmaps")
        elif file.startswith("data/ui/"):
            install_to_dir(file, "usr/share/anaconda/ui")
        elif file.startswith("data/post-scripts/"):
            install_to_dir(file, "usr/share/anaconda/post-scripts")
        elif file.endswith("anaconda-yum"):
            install_to_dir(file, "usr/libexec/anaconda")
        elif file.find('/') != -1:
            fields = file.split('/')
            subdir = fields[0]
            if subdir in ['po', 'scripts','command-stubs', 'tests',
                          'docs', 'fonts', 'utils',
                          'liveinst', 'dracut', 'data']:
                continue
            else:
                sys.stdout.write("Including %s\n" % (file,))
                install_to_dir(file, tmpupdates)
        else:
            sys.stdout.write("Including %s\n" % (file,))
            install_to_dir(file, tmpupdates)

def _compilableChanged(tag, compilable):
    lines = doGitDiff(tag, [compilable])

    for line in lines:
        fields = line.split()

        if len(fields) < 2:
            continue

        status = fields[0]
        file = fields[1]

        if status == "D":
            continue

        if file.startswith('Makefile') or file.endswith('.h') or \
           file.endswith('.c') or file.endswith('.py'):
            return True

    return False

def isysChanged(tag):
    return _compilableChanged(tag, 'pyanaconda/isys')

def widgetsChanged(tag):
    return _compilableChanged(tag, 'widgets')

def copyUpdatedIsys(updates, cwd):
    os.chdir(cwd)

    if not os.path.isfile('Makefile'):
        if not os.path.isfile('configure'):
            os.system('./autogen.sh')
        os.system('./configure --prefix=`rpm --eval %_prefix`')

    os.system('make')

    # Updates get overlaid onto the runtime filesystem. Anaconda expects them
    # to be in /run/install/updates, so put them in
    # $updatedir/run/install/updates.
    tmpupdates = updates.rstrip('/')
    if not tmpupdates.endswith("/run/install/updates/pyanaconda"):
        tmpupdates = os.path.join(tmpupdates, "run/install/updates/pyanaconda")

    if not os.path.isdir(tmpupdates):
        os.makedirs(tmpupdates)

    isysmodule = os.path.realpath(cwd + '/pyanaconda/isys/.libs/_isys.so')

    if os.path.isfile(isysmodule):
        shutil.copy2(isysmodule, tmpupdates)

def copyUpdatedWidgets(updates, cwd):
    os.chdir(cwd)

    if os.path.isdir("/lib64"):
        libdir = "/lib64/"
    else:
        libdir = "/lib/"

    if not os.path.isdir(updates + libdir):
        os.makedirs(updates + libdir)

    if not os.path.isdir(updates + libdir + "girepository-1.0"):
        os.makedirs(updates + libdir + "girepository-1.0")

    if not os.path.isfile('Makefile'):
        if not os.path.isfile('configure'):
            os.system('./autogen.sh')
        os.system('./configure --prefix=`rpm --eval %_prefix` --enable-gtk-doc --enable-introspection')

    os.system('make')

    files = ["libAnacondaWidgets.so", "libAnacondaWidgets.so.0", "libAnacondaWidgets.so.0.0.0"]
    for f in files:
        path = os.path.normpath(cwd + "/widgets/src/.libs/" + f)
        if os.path.islink(path) and not os.path.exists(updates + libdir + os.path.basename(path)):
            os.symlink(os.readlink(path), updates + libdir + os.path.basename(path))
        elif os.path.isfile(path):
            shutil.copy2(path, updates + libdir)

    typelib = os.path.realpath(cwd + "/widgets/src/AnacondaWidgets-1.0.typelib")
    if os.path.isfile(typelib):
        shutil.copy2(typelib, updates + libdir + "girepository-1.0")

def addRpms(updates, add_rpms):
    for rpm in add_rpms:
        cmd = "cd %s && rpm2cpio %s | cpio -dium" % (updates, rpm)
        sys.stdout.write(cmd+"\n")
        os.system(cmd)

def createUpdatesImage(cwd, updates):
    os.chdir(updates)
    os.system("find . | cpio -c -o | gzip -9cv > %s/updates.img" % (cwd,))
    sys.stdout.write("updates.img ready\n")

def usage(cmd):
    sys.stdout.write("Usage: %s [OPTION]...\n" % (cmd,))
    sys.stdout.write("Options:\n")
    sys.stdout.write("    -k, --keep       Do not delete updates subdirectory.\n")
    sys.stdout.write("    -c, --compile    Compile code if there are isys changes.\n")
    sys.stdout.write("    -h, --help       Display this help and exit.\n")
    sys.stdout.write("    -t, --tag        Make image from TAG to HEAD.\n")
    sys.stdout.write("    -o, --offset     Make image from (latest_tag - OFFSET) to HEAD.\n")
    sys.stdout.write("    -a, --add        Add contents of rpm to the update\n")

def main(argv):
    prog = os.path.basename(sys.argv[0])
    cwd = os.getcwd()
    configure = os.path.realpath(cwd + '/configure.ac')
    spec = os.path.realpath(cwd + '/anaconda.spec.in')
    updates = cwd + '/updates'
    keep, compile, help, unknown = False, False, False, False
    tag = None
    opts = []
    offset = 0
    add_rpms = []

    try:
        opts, args = getopt.getopt(sys.argv[1:], 'a:t:o:kc?',
                                   ['add=', 'tag=', 'offset=',
                                    'keep', 'compile', 'help'])
    except getopt.GetoptError:
        help = True

    for o, a in opts:
        if o in ('-k', '--keep'):
            keep = True
        elif o in ('-c', '--compile'):
            compile = True
        elif o in ('-?', '--help'):
            help = True
        elif o in ('-t', '--tag'):
            tag = a
        elif o in ('-o', '--offset'):
            offset = int(a)
        elif o in ('-a', '--add'):
            add_rpms.append(os.path.abspath(a))
        else:
            unknown = True

    if help:
        usage(prog)
        sys.exit(0)
    elif unknown:
        sys.stderr.write("%s: extra operand `%s'" % (prog, sys.argv[1],))
        sys.stderr.write("Try `%s --help' for more information." % (prog,))
        sys.exit(1)

    if not os.path.isfile(configure) and not os.path.isfile(spec):
        sys.stderr.write("You must be at the top level of the anaconda source tree.\n")
        sys.exit(1)

    if not tag:
        if offset < 1:
            tag = getArchiveTag(configure, spec)
        else:
            tag = getArchiveTagOffset(configure, spec, offset)
        sys.stdout.write("Using tag: %s\n" % tag)

    if not os.path.isdir(updates):
        os.makedirs(updates)

    copyUpdatedFiles(tag, updates, cwd)

    if compile:
        if isysChanged(tag):
            copyUpdatedIsys(updates, cwd)

        if widgetsChanged(tag):
            copyUpdatedWidgets(updates, cwd)

    if add_rpms:
        addRpms(updates, add_rpms)

    createUpdatesImage(cwd, updates)

    if not keep:
        shutil.rmtree(updates)

if __name__ == "__main__":
    main(sys.argv)
