#!/usr/bin/python
#             __________               __   ___.
#   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
#   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
#   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
#   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
#                     \/            \/     \/    \/            \/
# $Id$
#
# Copyright (c) 2009 Dominik Riebeling
#
# All files in this archive are subject to the GNU General Public License.
# See the file COPYING in the source tree root for full license agreement.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
#
# Automate building releases for deployment.
# Run from any folder to build
# - trunk
# - any tag (using the -t option)
# - any local folder (using the -p option)
# Will build a binary archive (tar.bz2 / zip) and source archive.
# The source archive won't be built for local builds. Trunk and
# tag builds will retrieve the sources directly from svn and build
# below the systems temporary folder.
#
# If the required Qt installation isn't in PATH use --qmake option.
# Tested on Linux and MinGW / W32
#
# requires python which package (http://code.google.com/p/which/)
# requires pysvn package.
# requires upx.exe in PATH on Windows.
#

import re
import os
import sys
import tarfile
import zipfile
import shutil
import subprocess
import getopt
import time
import hashlib
import tempfile

# modules that are not part of python itself.
try:
    import pysvn
except ImportError:
    print "Fatal: This script requires the pysvn package to run."
    print "       See http://pysvn.tigris.org/."
    sys.exit(-5)
try:
    import which
except ImportError:
    print "Fatal: This script requires the which package to run."
    print "       See http://code.google.com/p/which/."
    sys.exit(-5)

# == Global stuff ==
# Windows nees some special treatment. Differentiate between program name
# and executable filename.
program = "RockboxUtility"
project = "rbutil/rbutilqt/rbutilqt.pro"
environment = os.environ
make = "make"
if sys.platform == "win32":
    progexe = "Release/" + program + ".exe"
    make = "mingw32-make"
elif sys.platform == "darwin":
    progexe = program + ".app"
    # OS X 10.6 defaults to gcc 4.2. Building universal binaries that are
    # compatible with 10.4 requires using gcc-4.0.
    if not "QMAKESPEC" in environment:
        environment["QMAKESPEC"] = "macx-g++40"
else:
    progexe = program

# all files of the program. Will get put into an archive after building
# (zip on w32, tar.bz2 on Linux). Does not apply on Mac which uses dmg.
programfiles = [ progexe ]

svnserver = "svn://svn.rockbox.org/rockbox/"
# Paths and files to retrieve from svn when creating a tarball.
# This is a mixed list, holding both paths and filenames.
svnpaths = [ "rbutil/",
             "tools/ucl",
             "tools/rbspeex",
             "apps/codecs/libspeex",
             "docs/COPYING",
             "docs/CREDITS",
             "tools/iriver.c",
             "tools/Makefile",
             "tools/mkboot.h",
             "tools/voicefont.c",
             "tools/VOICE_PAUSE.wav",
             "tools/wavtrim.h",
             "tools/iriver.h",
             "tools/mkboot.c",
             "tools/telechips.c",
             "tools/telechips.h",
             "tools/voicefont.h",
             "tools/wavtrim.c",
             "tools/sapi_voice.vbs" ]
# set this to true to run upx on the resulting binary, false to skip this step.
# only used on w32.
useupx = False

# == Functions ==
def usage(myself):
    print "Usage: %s [options]" % myself
    print "       -q, --qmake=<qmake>   path to qmake"
    print "       -p, --project=<pro>   path to .pro file for building with local tree"
    print "       -t, --tag=<tag>       use specified tag from svn"
    print "       -a, --add=<file>      add file to build folder before building"
    print "       -s, --source-only     only create source archive"
    print "       -b, --binary-only     only create binary archive"
    if sys.platform != "darwin":
        print "       -d, --dynamic         link dynamically instead of static"
    print "       -k, --keep-temp       keep temporary folder on build failure"
    print "       -h, --help            this help"
    print "  If neither a project file nor tag is specified trunk will get downloaded"
    print "  from svn."

def getsources(svnsrv, filelist, dest):
    '''Get the files listed in filelist from svnsrv and put it at dest.'''
    client = pysvn.Client()
    print "Checking out sources from %s, please wait." % svnsrv

    for elem in filelist:
        url = re.subn('/$', '', svnsrv + elem)[0]
        destpath = re.subn('/$', '', dest + elem)[0]
        # make sure the destination path does exist
        d = os.path.dirname(destpath)
        if not os.path.exists(d):
            os.makedirs(d)
        # get from svn
        try:
            client.export(url, destpath)
        except:
            print "SVN client error: %s" % sys.exc_value
            print "URL: %s, destination: %s" % (url, destpath)
            return -1
    print "Checkout finished."
    return 0


def gettrunkrev(svnsrv):
    '''Get the revision of trunk for svnsrv'''
    client = pysvn.Client()
    entries = client.info2(svnsrv, recurse=False)
    return entries[0][1].rev.number


def findversion(versionfile):
    '''figure most recent program version from version.h,
    returns version string.'''
    h = open(versionfile, "r")
    c = h.read()
    h.close()
    r = re.compile("#define +VERSION +\"(.[0-9\.a-z]+)\"")
    m = re.search(r, c)
    s = re.compile("\$Revision: +([0-9]+)")
    n = re.search(s, c)
    if n == None:
        print "WARNING: Revision not found!"
    return m.group(1)


def findqt():
    '''Search for Qt4 installation. Return path to qmake.'''
    print "Searching for Qt"
    bins = ["qmake", "qmake-qt4"]
    for binary in bins:
        try:
            q = which.which(binary)
            if len(q) > 0:
                result = checkqt(q)
                if not result == "":
                    return result
        except:
            print sys.exc_value

    return ""


def checkqt(qmakebin):
    '''Check if given path to qmake exists and is a suitable version.'''
    result = ""
    # check if binary exists
    if not os.path.exists(qmakebin):
        print "Specified qmake path does not exist!"
        return result
    # check version
    output = subprocess.Popen([qmakebin, "-version"], stdout=subprocess.PIPE,
            stderr=subprocess.PIPE)
    cmdout = output.communicate()
    # don't check the qmake return code here, Qt3 doesn't return 0 on -version.
    for ou in cmdout:
        r = re.compile("Qt[^0-9]+([0-9\.]+[a-z]*)")
        m = re.search(r, ou)
        if not m == None:
            print "Qt found: %s" % m.group(1)
            s = re.compile("4\..*")
            n = re.search(s, m.group(1))
            if not n == None:
                result = qmakebin
    return result


def qmake(qmake="qmake", projfile=project, wd=".", static=True):
    print "Running qmake in %s..." % wd
    command = [qmake, "-config", "release", "-config", "noccache"]
    if static == True:
        command.append("-config")
        command.append("static")
    command.append(projfile)
    output = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=wd, env=environment)
    output.communicate()
    if not output.returncode == 0:
        print "qmake returned an error!"
        return -1
    return 0


def build(wd="."):
    # make
    print "Building ..."
    output = subprocess.Popen([make], stdout=subprocess.PIPE, cwd=wd)
    while True:
        c = output.stdout.readline()
        sys.stdout.write(".")
        sys.stdout.flush()
        if not output.poll() == None:
            sys.stdout.write("\n")
            sys.stdout.flush()
            if not output.returncode == 0:
                print "Build failed!"
                return -1
            break
    if sys.platform != "darwin":
        # strip. OS X handles this via macdeployqt.
        print "Stripping binary."
        output = subprocess.Popen(["strip", progexe], stdout=subprocess.PIPE, cwd=wd)
        output.communicate()
        if not output.returncode == 0:
            print "Stripping failed!"
            return -1
    return 0


def upxfile(wd="."):
    # run upx on binary
    print "UPX'ing binary ..."
    output = subprocess.Popen(["upx", progexe], stdout=subprocess.PIPE, cwd=wd)
    output.communicate()
    if not output.returncode == 0:
        print "UPX'ing failed!"
        return -1
    return 0


def zipball(versionstring, buildfolder):
    '''package created binary'''
    print "Creating binary zipball."
    archivebase = program + "-" + versionstring
    outfolder = buildfolder + "/" + archivebase
    archivename = archivebase + ".zip"
    # create output folder
    os.mkdir(outfolder)
    # move program files to output folder
    for f in programfiles:
        shutil.copy(buildfolder + "/" + f, outfolder)
    # create zipball from output folder
    zf = zipfile.ZipFile(archivename, mode='w', compression=zipfile.ZIP_DEFLATED)
    for root, dirs, files in os.walk(outfolder):
        for name in files:
            physname = os.path.join(root, name)
            filename = re.sub("^" + buildfolder, "", physname)
            zf.write(physname, filename)
        for name in dirs:
            physname = os.path.join(root, name)
            filename = re.sub("^" + buildfolder, "", physname)
            zf.write(physname, filename)
    zf.close()
    # remove output folder
    shutil.rmtree(outfolder)
    return archivename


def tarball(versionstring, buildfolder):
    '''package created binary'''
    print "Creating binary tarball."
    archivebase = program + "-" + versionstring
    outfolder = buildfolder + "/" + archivebase
    archivename = archivebase + ".tar.bz2"
    # create output folder
    os.mkdir(outfolder)
    # move program files to output folder
    for f in programfiles:
        shutil.copy(buildfolder + "/" + f, outfolder)
    # create tarball from output folder
    tf = tarfile.open(archivename, mode='w:bz2')
    tf.add(outfolder, archivebase)
    tf.close()
    # remove output folder
    shutil.rmtree(outfolder)
    return archivename


def macdeploy(versionstring, buildfolder):
    '''package created binary to dmg'''
    dmgfile = program + "-" + versionstring + ".dmg"
    appbundle = buildfolder + "/" + progexe

    # workaround to Qt issues when building out-of-tree. Hardcoded for simplicity.
    sourcebase = buildfolder + re.sub('rbutilqt.pro$', '', project)
    shutil.copy(sourcebase + "icons/rbutilqt.icns", appbundle + "/Contents/Resources/")
    shutil.copy(sourcebase + "Info.plist", appbundle + "/Contents/")
    # end of Qt workaround

    output = subprocess.Popen(["macdeployqt", progexe, "-dmg"], stdout=subprocess.PIPE, cwd=buildfolder)
    output.communicate()
    if not output.returncode == 0:
        print "macdeployqt failed!"
        return -1
    # copy dmg to output folder
    shutil.copy(buildfolder + "/" + program + ".dmg", dmgfile)
    return dmgfile

def filehashes(filename):
    '''Calculate md5 and sha1 hashes for a given file.'''
    if not os.path.exists(filename):
        return ["", ""]
    m = hashlib.md5()
    s = hashlib.sha1()
    f = open(filename, 'rb')
    while True:
        d = f.read(65536)
        if d == "":
            break
        m.update(d)
        s.update(d)
    return [m.hexdigest(), s.hexdigest()]


def filestats(filename):
    if not os.path.exists(filename):
        return
    st = os.stat(filename)
    print filename, "\n", "-" * len(filename)
    print "Size:    %i bytes" % st.st_size
    h = filehashes(filename)
    print "md5sum:  %s" % h[0]
    print "sha1sum: %s" % h[1]
    print "-" * len(filename), "\n"


def tempclean(workfolder, nopro):
    if nopro == True:
        print "Cleaning up working folder %s" % workfolder
        shutil.rmtree(workfolder)
    else:
        print "Project file specified or cleanup disabled!"
        print "Temporary files kept at %s" % workfolder


def main():
    startup = time.time()
    try:
        opts, args = getopt.getopt(sys.argv[1:], "q:p:t:a:sbdkh",
            ["qmake=", "project=", "tag=", "add=", "source-only", "binary-only", "dynamic", "keep-temp", "help"])
    except getopt.GetoptError, err:
        print str(err)
        usage(sys.argv[0])
        sys.exit(1)
    qt = ""
    proj = ""
    svnbase = svnserver + "trunk/"
    tag = ""
    addfiles = []
    cleanup = True
    binary = True
    source = True
    keeptemp = False
    if sys.platform != "darwin":
        static = True
    else:
        static = False
    for o, a in opts:
        if o in ("-q", "--qmake"):
            qt = a
        if o in ("-p", "--project"):
            proj = a
            cleanup = False
        if o in ("-t", "--tag"):
            tag = a
            svnbase = svnserver + "tags/" + tag + "/"
        if o in ("-a", "--add"):
            addfiles.append(a)
        if o in ("-s", "--source-only"):
            binary = False
        if o in ("-b", "--binary-only"):
            source = False
        if o in ("-d", "--dynamic") and sys.platform != "darwin":
            static = False
        if o in ("-k", "--keep-temp"):
            keeptemp = True
        if o in ("-h", "--help"):
            usage(sys.argv[0])
            sys.exit(0)

    if source == False and binary == False:
        print "Building build neither source nor binary means nothing to do. Exiting."
        sys.exit(1)

    # search for qmake
    if qt == "":
        qm = findqt()
    else:
        qm = checkqt(qt)
    if qm == "":
        print "ERROR: No suitable Qt installation found."
        sys.exit(1)

    # create working folder. Use current directory if -p option used.
    if proj == "":
        w = tempfile.mkdtemp()
        # make sure the path doesn't contain backslashes to prevent issues
        # later when running on windows.
        workfolder = re.sub(r'\\', '/', w)
        if not tag == "":
            sourcefolder = workfolder + "/" + tag + "/"
            archivename = tag + "-src.tar.bz2"
            # get numeric version part from tag
            ver = "v" + re.sub('^[^\d]+', '', tag)
        else:
            trunk = gettrunkrev(svnbase)
            sourcefolder = workfolder + "/" + program + "-r" + str(trunk) + "/"
            archivename = program + "-r" + str(trunk) + "-src.tar.bz2"
            ver = "r" + str(trunk)
        os.mkdir(sourcefolder)
    else:
        workfolder = "."
        sourcefolder = "."
        archivename = ""
    # check if project file explicitly given. If yes, don't get sources from svn
    if proj == "":
        proj = sourcefolder + project
        # get sources and pack source tarball
        if not getsources(svnbase, svnpaths, sourcefolder) == 0:
            tempclean(workfolder, cleanup and not keeptemp)
            sys.exit(1)

        if source == True:
            tf = tarfile.open(archivename, mode='w:bz2')
            tf.add(sourcefolder, os.path.basename(re.subn('/$', '', sourcefolder)[0]))
            tf.close()
            if binary == False:
                shutil.rmtree(workfolder)
                sys.exit(0)
    else:
        # figure version from sources. Need to take path to project file into account.
        versionfile = re.subn('[\w\.]+$', "version.h", proj)[0]
        ver = findversion(versionfile)

    # check project file
    if not os.path.exists(proj):
        print "ERROR: path to project file wrong."
        sys.exit(1)

    # copy specified (--add) files to working folder
    for f in addfiles:
        shutil.copy(f, sourcefolder)
    buildstart = time.time()
    header = "Building %s %s" % (program, ver)
    print header
    print len(header) * "="

    # build it.
    if not qmake(qm, proj, sourcefolder, static) == 0:
        tempclean(workfolder, cleanup and not keeptemp)
        sys.exit(1)
    if not build(sourcefolder) == 0:
        tempclean(workfolder, cleanup and not keeptemp)
        sys.exit(1)
    if sys.platform == "win32":
        if useupx == True:
            if not upxfile(sourcefolder) == 0:
                tempclean(workfolder, cleanup and not keeptemp)
                sys.exit(1)
        archive = zipball(ver, sourcefolder)
    elif sys.platform == "darwin":
        archive = macdeploy(ver, sourcefolder)
    else:
        if os.uname()[4].endswith("64"):
            ver += "-64bit"
        archive = tarball(ver, sourcefolder)

    # remove temporary files
    tempclean(workfolder, cleanup)

    # display summary
    headline = "Build Summary for %s" % program
    print "\n", headline, "\n", "=" * len(headline)
    if not archivename == "":
        filestats(archivename)
    filestats(archive)
    duration = time.time() - startup
    building = time.time() - buildstart
    durmins = (int)(duration / 60)
    dursecs = (int)(duration % 60)
    buildmins = (int)(building / 60)
    buildsecs = (int)(building % 60)
    print "Overall time %smin %ssec, building took %smin %ssec." % \
        (durmins, dursecs, buildmins, buildsecs)


if __name__ == "__main__":
    main()

