# Copyright (c) 2003
#   Riverbank Computing Limited <info@riverbankcomputing.co.uk>
#
# This is the build script for PyQt.  It should be run in the top level
# directory of the source distribution and by the Python interpreter for which
# it is being built.  It uses either qmake or tmake to do the hard work of
# generating the platform specific Makefiles.


import sys
import os
import glob
import re
import tempfile
import shutil
import py_compile
import compileall
import string
import time


# Define the globals.
progName = None
qtDir = None
qtIncDir = None
qtLibDir = None
qtVersion = -1
qtLib = None
platPyScriptDir = None
platPyDLLDir = None
platPySiteDir = None
platPyIncDir = None
platPyLibDir = None
platPyLib = None
sipModuleDir = None
sipIncDir = None
sipBin = None
sipVersion = 3
sciIncDir = None
sciLibDir = None
sciVersion = -1
platBinDir = None
modDir = None
platMake = None
platCopy = None
platMkdir = None
platRmdir = None
platQTDIRName = None
pyFullVers = None
pyVersNr = None
makefileGen = None
makeBin = None
buildModules = ["qt"]
proPatches = {}
makefilePatches = {}
tempBuildDir = None
usingTmake = 0
enableOldPython = 0
debugMode = "release"
catCppFiles = 0
catSplit = 1
qpeTag = None
licType = None
gccFlags = []


def usage(rcode = 2):
    """Display a usage message and exit.

    rcode is the return code passed back to the calling process.
    """
    print "Usage:"
    print "    %s [-h] [-a version] [-b dir] [-c] [-d dir] [-e dir] [-f gccflag] [-g prog] [-i dir] [-j #] [-l Qt-library] [-m prog] [-n dir] [-o dir] [-p dir] [-q dir] [-r dir] [-s dir] [-t dir] [-u] [-w]" % (progName)
    print "where:"
    print "    -h             display this help message"
    print "    -a tag         explicitly enable the qtpe module"
    print "    -b dir         where pyuic and pylupdate will be installed [default %s]" % (platBinDir)
    print "    -c             concatenate each module's C++ source files"
    print "    -d dir         where PyQt will be installed [default %s]" % (modDir)
    print "    -e dir         the directory containing the SIP header files [default %s]" % (sipIncDir)
    print "    -f gccflag     additional GCC flag, eg. -fno-exceptions"
    print "    -g prog        the name of the Makefile generator"
    print "    -i dir         the directory containing the Qt header files [default %s%sinclude]" % (platQTDIRName,os.sep)
    print "    -j #           split the concatenated C++ source files into # pieces [default 1]"
    print "    -l Qt-library  explicitly specify the type of Qt library, either qt, qt-mt, qt-mtedu or qte"
    print "    -m prog        the name of the Make program [default %s]" % (platMake)
    print "    -n dir         the directory containing the QScintilla header files [default %s%sinclude]" % (platQTDIRName, os.sep)
    print "    -o dir         the directory containing the QScintilla library [default %s%slib]" % (platQTDIRName, os.sep)
    print "    -p dir         the name of the SIP code generator [default sip]"
    print "    -q dir         the Qt base directory [default %s]" % (platQTDIRName)
    print "    -r dir         the directory containing the Qt library [default %s%slib]" % (platQTDIRName,os.sep)
    print "    -s dir         the directory containing the SIP module"
    print "    -t dir         the directory containing the Python library directories [default %s]" % (platPyLibDir)
    print "    -u             build with debugging symbols"
    print "    -w             enable the use of Python 1.5.x under Windows"

    sys.exit(rcode)


def format(mess,delin = 1):
    """Format a message by inserting line breaks at appropriate places.

    mess is the text of the message.

    delin is non-zero if the text should be deliniated.

    Return the formatted message.
    """
    margin = 78
    curs = 0

    if delin:
        msg = "*" * margin + "\n"
    else:
        msg = ""

    for w in string.split(mess):
        l = len(w)
        if curs != 0 and curs + l > margin:
            msg = msg + "\n"
            curs = 0

        if curs > 0:
            msg = msg + " "
            curs = curs + 1

        msg = msg + w
        curs = curs + l

    msg = msg + "\n"

    if delin:
        msg = msg + ("*" * margin) + "\n"

    return msg


def error(mess):
    """Display an error message and exit.

    mess is the text of the message.
    """
    sys.stderr.write(format("Error: %s" % (mess)))
    sys.exit(1)


def inform(mess,delin = 1):
    """Display an information message.

    mess is the text of the message.

    delin is non-zero if the text should be deliniated.
    """
    sys.stdout.write(format(mess,delin))


def initGlobals():
    """Sets the values of globals that need more than a simple assignment.
    This is called before the command line is parsed to set defaults for the
    help, and afterwards to reflect the command line.
    """
    global pyFullVers, pyVersNr

    pyFullVers = string.split(sys.version)[0]

    vl = string.split(re.findall("[0-9.]*",pyFullVers)[0],".")
    major = vl[0]
    minor = vl[1]

    pyVers = major + "." + minor
    pyVersNr = int(major) * 10 + int(minor)

    global platMake, platCopy, platMkdir, platRmdir
    global platPyLibDir, platPyScriptDir, platPyDLLDir, platPySiteDir
    global platPyIncDir, platPyLib, platQTDIRName, platBinDir

    if sys.platform == "win32":
        if platMake is None:
            platMake = "nmake"

        platCopy = "copy /y"
        platMkdir = "mkdir"
        platRmdir = "rmdir /s /q"

        if platPyLibDir is None:
            platPyLibDir = sys.prefix

        platPyScriptDir = platPyLibDir + "\\Lib"
        platPyDLLDir = platPyLibDir + "\\DLLs"

        if pyVersNr < 22:
            platPySiteDir = platPyScriptDir
        else:
            platPySiteDir = platPyLibDir + "\\Lib\\site-packages"

        platPyIncDir = sys.prefix + "\\include"
        platQTDIRName = "%QTDIR%"

        if platBinDir is None:
            platBinDir = sys.exec_prefix

        if debugMode == "debug":
            dbg = "_d"
        else:
            dbg = ""

        platPyLib = platPyLibDir + "\\libs\\python%d%s.lib" % (pyVersNr,dbg)
    else:
        if platMake is None:
            platMake = "make"

        platCopy = "cp"
        platMkdir = "mkdir"
        platRmdir = "rm -rf"

        if platPyLibDir is None:
            platPyLibDir = sys.prefix + "/lib/python" + pyVers

        platPyScriptDir = platPyLibDir
        platPyDLLDir = platPyLibDir + "/lib-dynload"

        if pyVersNr < 20:
            platPySiteDir = platPyLibDir
        else:
            platPySiteDir = platPyLibDir + "/site-packages"

        platPyIncDir = sys.prefix + "/include/python" + pyVers
        platQTDIRName = "$QTDIR"

        if platBinDir is None:
            platBinDir = "/usr/local/bin"

    global modDir, sipIncDir

    if modDir is None:
        modDir = platPySiteDir

    if sipIncDir is None:
        sipIncDir = platPyIncDir


def escape(s):
    """Escape all the backslashes (usually Windows directory separators) in a
    string so that they don't confuse regular expression substitutions.  Also
    enclose in double quotes if the string contains spaces.

    s is the string to escape.

    Returns the escaped string.
    """
    s = string.replace(s,"\\","\\\\")

    if string.find(s," ") >= 0:
        s = "\"" + s + "\""

    return s


def isProgram(f):
    """Return non-zero if a filename refers to an executable file.
    """
    return os.access(f,os.X_OK) and not os.path.isdir(f)


def searchPath(prog,xtradir = None):
    """Search the PATH for a program and return it's absolute pathname.

    prog is the name of the program.
    xtradir is the name of an optional extra directory to search as well.
    """
    if sys.platform == "win32":
        prog = prog + ".exe"

    # Create the list of directories to search.
    try:
        path = os.environ["PATH"]
    except KeyError:
        path = os.defpath

    dlist = string.split(path,os.pathsep)

    if xtradir:
        dlist.insert(0,xtradir)

    # Create the list of files to check.
    flist = []

    for d in dlist:
        flist.append(os.path.abspath(os.path.join(d,prog)))

    # Search for the file.
    for f in flist:
        if isProgram(f):
            return f

    # Nothing found.
    return None


def patchFile(name,pdict):
    """Apply the patches to a file.

    name is the name of the file, excluding the .in extension.
    pdict is the patch dictionary.
    """
    fi = open(name + ".in","r")
    fo = open(name,"w")

    for line in fi.readlines():
        for (pattern, repl) in pdict.values():
            line = re.sub(pattern,repl,line)

        fo.write(line)
    
    fi.close()
    fo.close()


def isQtLibrary(dir,lib):
    """See if a Qt library appears in a particular directory.

    dir is the name of the directory.
    lib is the name of the library.

    Returns the basename of the library file that was found or None if nothing
    was found.
    """
    if sys.platform == "win32":
        lpatt = "\\" + lib + "[0-9]*.lib"
    else:
        lpatt = "/lib" + lib + ".*"

    l = glob.glob(dir + lpatt)

    if len(l) == 0:
        return None

    return os.path.basename(l[0])


def copyToFile(name,text):
    """Copy a string to a file.

    name is the name of the file.
    text is the contents to copy to the file.
    """
    f = open(name,"w")
    f.write(text)
    f.close()


def mkTempBuildDir(olddir = None):
    """Create a temporary build directory for a console application called
    qttest, complete with patched Makefile.  The global tempBuildDir is set to
    the name of the directory.  The temporary directory becomes the current
    directory.

    olddir is None if the directory should be created, otherwise it is deleted.

    Returns the name of the previous current directory.
    """
    global tempBuildDir

    if olddir is None:
        tempBuildDir = tempfile.mktemp()

        try:
            os.mkdir(tempBuildDir)
        except:
            error("Unable to create temporary directory.")

        newdir = pushDir(tempBuildDir)

        copyToFile("qttest.pro.in",
"""TEMPLATE = app
TARGET = qttest
CONFIG += qt @TEST_OPENGL@ console warn_off release @BL_THREAD@
INCLUDEPATH = @BL_INCLUDEPATH@
DEFINES = @BL_DEFINES@
SOURCES = qttest.cpp
LIBS += @TEST_QUILIB@ @TEST_SCILIBDIR@

win32:LIBS += @BL_PYLIB@
""")

        # Disable OpenGL, qui and QScintilla support by default.
        proPatches["TOPENGL"] = [re.compile("@TEST_OPENGL@",re.M), ""]
        proPatches["TQUILIB"] = [re.compile("@TEST_QUILIB@",re.M), ""]
        proPatches["TSCILIBDIR"] = [re.compile("@TEST_SCILIBDIR@",re.M), ""]

        # Create a dummy source file to suppress a qmake warning.
        copyToFile("qttest.cpp","")

        buildMakefile("qttest.pro")
    else:
        popDir(olddir)
        newdir = None

        shutil.rmtree(tempBuildDir,1)

    return newdir


def tryModule(mname,incfile,ctor):
    """See if a PyQt module should be built and update the buildModules list
    accordingly.

    mname is the name of the PyQt module.
    incfile is the C++ header file that defines the class being used for the
    test.
    ctor is the constructor of the class being used for the test.
    """
    copyToFile("qttest.cpp",
"""#include <%s>

int main(int argc,char **argv)
{
    new %s;
}
""" % (incfile,ctor))

    if runMake(None,0) == 0:
        buildModules.append(mname)
        inform("The %s module will be built." % (mname))
    else:
        inform("The %s module will not be built." % (mname))

    runMake("clean")


def readVersion(fname,name,desc):
    """Read the version information for a package from a file.

    fname is the name of the file.
    name is the name of the package.
    desc is the descriptive name of the package.

    Returns a tuple of the version as a number and as a string.
    """
    vers = 0
    versstr = ""
    numtarget = name + "_VERSION"
    strtarget = name + "_VERSION_STR"

    f = open(fname)
    l = f.readline()

    while l:
        wl = string.split(l)
        if len(wl) == 3 and wl[0] == "#define":
            if wl[1] == numtarget:
                v = wl[2]

                if v[0:2] == "0x":
                    vers = string.atoi(v,16)
                else:
                    dec = int(v)
                    maj = dec / 100
                    min = (dec % 100) / 10
                    bug = (dec % 10)
                    vers = (maj << 16) + (min << 8) + bug

            if wl[1] == strtarget:
                versstr = wl[2][1:-1]

            if versstr != "" and vers > 0:
                break

        l = f.readline()

    f.close()

    if versstr == "":
        error("The %s version number could not be determined by parsing %s." % (desc,qglobal))

    return (vers, versstr)


def checkQtDirAndVersion():
    """Check that Qt can be found and what its version is.
    """
    # Check the Qt directory is known and valid.
    global qtDir

    if qtDir is None:
        try:
            qtDir = os.environ["QTDIR"]
        except KeyError:
            error("Please set the name of the Qt base directory, either by using the -q argument or setting the QTDIR environment variable.")
    else:
        os.environ["QTDIR"] = qtDir

    # For lib template project files.
    makefilePatches["QTDIR_LIB"] = [re.compile("(^CC)",re.M), "QTDIR    = " + escape(qtDir) + "\n\\1"]

    # For subdir template project files.
    makefilePatches["QTDIR_SUBDIR"] = [re.compile("(^MAKEFILE)",re.M), "QTDIR    = " + escape(qtDir) + "\n\\1"]

    if not os.access(qtDir,os.F_OK):
        error("The %s Qt base directory does not seem to exist. Use the -q argument or the QTDIR environment variable to set the correct directory." % (qtDir))

    inform("%s is the Qt base directory." % (qtDir))

    # Check that the Qt header files have been installed.
    if qtIncDir:
        qtincdir = qtIncDir
    else:
        qtincdir = qtDir + os.sep + "include"

    qglobal = qtincdir + os.sep + "qglobal.h"

    if not os.access(qglobal,os.F_OK):
        error("qglobal.h could not be found in %s. Use the -i argument to explicitly specify the correct directory." % (qtincdir))

    inform("%s contains qglobal.h." % (qtincdir))

    # Get the Qt version number.
    global qtVersion

    qtVersion, qtversstr = readVersion(qglobal,"QT","Qt")

    # Prevent the Makefiles recreating themselves.  Only seems to affect Qt
    # v3.1.0 and later.
    if qtVersion >= 0x030100:
        makefilePatches["FORCE"] = [re.compile(" FORCE",re.M), ""]

    # Try and work out which Qt edition it is.
    qtedition = ""
    qteditionstr = ""

    if qtVersion >= 0x030000:
        qconfig = qtincdir + os.sep + "qconfig.h"

        if not os.access(qconfig,os.F_OK):
            error("qconfig.h could not be found in %s." % (qtincdir))

        f = open(qconfig)
        l = f.readline()

        while l:
            wl = string.split(l)
            if len(wl) == 3 and wl[0] == "#define" and wl[1] == "QT_PRODUCT_LICENSE":
                qtedition = wl[2][4:-1]
                break

            l = f.readline()

        f.close()

        if qtedition == "":
            error("The Qt edition could not be determined by parsing %s." % (qconfig))

        qteditionstr = qtedition + " edition "

    inform("Qt %s %sis being used." % (qtversstr,qteditionstr))

    # Check the licenses are compatible.
    if qtedition:
        if (qtedition == "free" and licType != "GPL") or \
           (qtedition != "free" and licType == "GPL"):
            error("This version of PyQt and the %s edition of Qt have incompatible licenses." % (qtedition))

    # The professional edition needs special handling if XML support is needed.
    if qtedition == "professional":
        proPatches["SCOPE"] = [re.compile("@BL_SCOPE@",re.M), "rbprof"]
    else:
        proPatches["SCOPE"] = [re.compile("@BL_SCOPE@",re.M), ""]

    # Link in the qassistantclient library for Qt v3.1+.
    qaclib = ""

    if qtVersion >= 0x030100:
        if sys.platform == "win32":
            qaclib = "$(QTDIR)\\lib\\qassistantclient.lib"
        else:
            qaclib = "-lqassistantclient"

    proPatches["QACLIB"] = [re.compile("@BL_QASSISTANTCLIENT@",re.M), qaclib]

    # Set the list of directories containing header files.  tmake needs the
    # current directory explicitly included (but we don't yet know if we are
    # using qmake or tmake).
    incdirs = "."

    # Put any explicit SIP directory before the Python directory.
    if sipIncDir != platPyIncDir:
        incdirs = incdirs + " " + escape(sipIncDir)

    incdirs = incdirs + " " + escape(platPyIncDir)

    if qtIncDir:
        incdirs = incdirs + " " + escape(qtIncDir)

    # Find the QScintilla header files.
    if qtVersion >= 0x030000:
        global sciIncDir

        if not sciIncDir:
            sciIncDir = qtDir + os.sep + "include"

        sciglobal = sciIncDir + os.sep + "qextscintillaglobal.h"

        if os.access(sciglobal,os.F_OK):
            if sciIncDir != qtincdir:
                incdirs = incdirs + " " + escape(sciIncDir)

            inform("%s contains qextscintillaglobal.h." % (sciIncDir))

            # Get the QScintilla version number.
            global sciVersion

            sciVersion, sciversstr = readVersion(sciglobal,"QSCINTILLA","QScintilla")

            inform("QScintilla %s is being used." % (sciversstr))

            # If we find a snapshot then set the version number to 0 as a
            # special case.
            if sciversstr[:8] == "snapshot":
                sciVersion = 0
        else:
            inform("qextscintillaglobal.h could not be found in %s and so the qtext module will not be built. If QScintilla is installed then use the -n argument to explicitly specify the correct directory." % (sciIncDir))

    proPatches["INCLUDEPATH"] = [re.compile("@BL_INCLUDEPATH@",re.M), incdirs]


def checkQtLibrary():
    """Check which Qt library is to be used.
    """
    global qtLib

    if qtLibDir:
        qtlibdir = qtLibDir
    else:
        qtlibdir = qtDir + os.sep + "lib"

    if qtLib is None:
        mtlib = isQtLibrary(qtlibdir,"qt-mt")
        stlib = isQtLibrary(qtlibdir,"qt")
        edlib = isQtLibrary(qtlibdir,"qt-mtedu")
        emlib = isQtLibrary(qtlibdir,"qte")

        nrlibs = 0
        names = ""

        if stlib:
            nrlibs = nrlibs + 1
            names = names + ", qt"
            qtLib = "qt"
            lib = stlib

        if mtlib:
            nrlibs = nrlibs + 1
            names = names + ", qt-mt"
            qtLib = "qt-mt"
            lib = mtlib

        if edlib:
            nrlibs = nrlibs + 1
            names = names + ", qt-mtedu"
            qtLib = "qt-mtedu"
            lib = edlib

        if emlib:
            nrlibs = nrlibs + 1
            names = names + ", qte"
            qtLib = "qte"
            lib = emlib

        if nrlibs == 0:
            error("No Qt libraries could be found in %s." % (qtlibdir))

        if nrlibs > 1:
            error("These versions of the Qt libraries were found: %s. Use the -l argument to explicitly specify which you want to use." % (names[2:]))

        inform("The %s Qt library was found." % (qtLib))
    else:
        lib = isQtLibrary(qtlibdir,qtLib)

        if not lib:
            error("The %s Qt library could not be found in %s." % (qtLib,qtlibdir))

    if qtLib in ("qt-mt", "qt-mtedu"):
        proPatches["THREAD"] = [re.compile("@BL_THREAD@",re.M), "thread"]
        inform("Qt thread support is enabled.")
    else:
        proPatches["THREAD"] = [re.compile("@BL_THREAD@",re.M), ""]
        inform("Qt thread support is disabled.")

    # tmake under Windows doesn't know the version of the Qt library, so we
    # have to patch it.
    if sys.platform == "win32" and usingTmake:
        makefilePatches["QTLIB"] = [re.compile("qt.lib",re.M), lib]


def installChecks():
    """Carry out basic checks about the installation.
    """
    # Check we are in the top level directory.
    if not os.access(progName,os.F_OK):
        error("This program must be run from the top level directory of the package, ie. the directory containing this program.")

    global licType

    try:
        import license
        licType = license.LicenseType
        licname = license.LicenseName
    except:
        licType = "GPL"
        licname = "GNU General Public License"

    inform("This is the %s version of PyQt and is licensed under the %s." % (licType,licname),0)

    print
    print "Type 'L' to view the license."
    print "Type 'yes' to accept the terms of the license."
    print "Type 'no' to decline the terms of the license."
    print

    while 1:
        try:
            resp = raw_input("Do you accept the terms of the license? ")
        except:
            resp = ""

        resp = string.lower(string.strip(resp))

        if resp == "yes":
            break

        if resp == "no":
            sys.exit(0)

        if resp == "l":
            os.system("more LICENSE")

    inform("Building the %s version of PyQt 3.7 for Python %s on %s." % (licType,pyFullVers,sys.platform))

    if licType != "GPL":
        if os.access("sip/" + license.LicenseFile,os.F_OK):
            inform("Found the license file %s.\n" % (license.LicenseFile))
        else:
            error("Please copy the license file %s to the sip directory.\n" % (license.LicenseFile))

    # We can't be sure that we can run programs under Windows (because it might
    # be 95 or 98) with Python 1.5.x and get the return codes.  So we force the
    # user to explicitly say that's what they want to do.
    if sys.platform == "win32" and pyVersNr < 16:
        if not enableOldPython:
            error("You are using Python %s under Windows. If you are running Windows95 or Windows98 then there may be problems trying to configure PyQt. It is recomended that you upgrade to Python 1.6 or later. Use the -w argument to suppress this check.\n" % (pyFullVers))

    # Check the installation directory is valid.
    if not os.access(modDir,os.F_OK):
        error("The %s PyQt destination directory does not seem to exist. Use the -d argument to set the correct directory." % (modDir))

    proPatches["DESTDIR"] = [re.compile("@BL_DESTDIR@",re.M), escape(modDir)]

    inform("%s is the PyQt installation directory." % (modDir))

    # Check that the Python header files have been installed.
    if not os.access(platPyIncDir + "/Python.h",os.F_OK):
        error("Python.h could not be found in %s. Make sure Python is fully installed, including any development packages." % (platPyIncDir))

    inform("%s contains Python.h." % (platPyIncDir))

    # Check that the Python library can be found.
    if platPyLib:
        if not os.access(platPyLib,os.F_OK):
            error("%s could not be found. Make sure Python is fully installed, including any development packages." % (platPyLib))

        proPatches["PYLIB"] = [re.compile("@BL_PYLIB@",re.M), escape(platPyLib)]

        inform("%s is installed." % (platPyLib))

    # Look for the SIP binary.
    global sipBin

    if sipBin is None:
        sipBin = searchPath("sip",platBinDir)
    elif not isProgram(sipBin):
        sipBin = None

    if sipBin is None:
        error("The SIP code generator could not be found. Use the -p argument to set the correct program.")

    inform("%s will be used as the SIP code generator." % (sipBin))

    # Check that the SIP header file has been installed.
    if not os.access(sipIncDir + "/sip.h",os.F_OK):
        error("sip.h could not be found in %s. Use the -e argument to set the correct directory." % (sipIncDir))

    inform("%s contains sip.h." % (sipIncDir))

    # Check which major version of SIP it is.
    global sipModuleDir, sipVersion

    f = open(sipIncDir + "/sip.h")
    l = f.readline()

    while l:
        wl = string.split(l)
        if len(wl) == 3 and wl[0] == "#define" and wl[1] == "SIP_API_MAJOR_NR":
            sipVersion = 4
            break

        l = f.readline()

    f.close()

    # Check the SIP module can be found.
    if sipModuleDir is None:
        dirlist = [platPyDLLDir, platPyScriptDir, platPySiteDir]
    else:
        dirlist = [sipModuleDir]
        sipModuleDir = None

    for d in dirlist:
        if sipVersion == 4:
            if glob.glob(d + "/sip.*"):
                sipModuleDir = d
                break
        else:
            if glob.glob(d + "/libsip.*"):
                sipModuleDir = d
                break

    if sipModuleDir is None:
        error("The SIP v%d module could not be found. Use the -s argument to set the correct directory." % (sipVersion))

    inform("Using the SIP v%d.x module in %s." % (sipVersion,sipModuleDir))

    proPatches["SIPVERS"] = [re.compile("@BL_SIP@",re.M), "sip%d" % (sipVersion)]

    if sipVersion == 4:
        proPatches["DLL"] = [re.compile("@BL_DLL@",re.M), "plugin"]
    else:
        proPatches["DLL"] = [re.compile("@BL_DLL@",re.M), "dll"]
        proPatches["LIBS"] = [re.compile("@BL_SIPMODDIR@",re.M), escape(sipModuleDir)]

    # Check the Qt installation.
    checkQtDirAndVersion()

    # Look for the Makefile generator.
    checkMakefileGenerator()

    if usingTmake and sys.platform == "win32":
        defines = "QT_DLL"
    else:
        defines = ""

    proPatches["DEFINES"] = [re.compile("@BL_DEFINES@",re.M), defines]

    checkQtLibrary()

    # Look for Make.
    global makeBin

    if makeBin is None:
        makeBin = searchPath(platMake)
    elif not isProgram(makeBin):
        makeBin = None

    if makeBin is None:
        error("A make program could not be found. Use the -m argument to set the correct program.")

    # For lib template project files.
    makefilePatches["MAKE_LIB"] = [re.compile("(^CC)",re.M), "MAKE     = " + escape(makeBin) + "\n\\1"]

    # For subdir template project files.
    makefilePatches["MAKE_SUBDIR"] = [re.compile("(^MAKEFILE)",re.M), "MAKE     = " + escape(makeBin) + "\n\\1"]

    inform("%s will be used as the make program." % (makeBin))

    # SIP v4 requires later versions.
    if sipVersion == 4:
        if qtVersion < 0x030000:
            error("SIP v4.0 and later require Qt v3.0 or later.")

        if pyVersNr < 23:
            error("SIP v4.0 and later require Python v2.3 or later.")

        if usingTmake:
            error("SIP v4.0 and later require qmake instead of tmake.")


def checkMakefileGenerator():
    """Check that the Makefile generator can be found.
    """
    global makefileGen, usingTmake

    if makefileGen is None:
        if qtVersion >= 0x030000:
            mfg = "qmake"
        else:
            mfg = "tmake"

        xtradir = qtDir + "/bin"

        makefileGen = searchPath(mfg,xtradir)
    elif not isProgram(makefileGen):
        mfg = makefileGen
        makefileGen = None

    if makefileGen is None:
        error("The Makefile generator %s could not be found. Use the -g argument to set the correct program." % (mfg))

    if os.path.basename(makefileGen) in ("tmake", "tmake.exe"):
        usingTmake = 1

        # Make sure that references to tmake use the correct absolute pathname.
        makefilePatches["TMAKE1"] = [re.compile("(^TMAKE[ \t]*=).*$",re.M), "\\1 " + escape(makefileGen)]
        makefilePatches["TMAKE2"] = [re.compile("^\ttmake(.*)$",re.M), "\t" + escape(makefileGen) + "\\1"]

        # Add the install target for Makefiles generated from the subdirs
        # template.
        makefilePatches["TMAKE3"] = [re.compile("^(clean release debug):$",re.M), "\\1 install:"]

        # Remove the invocation of the tmake_all target.
        makefilePatches["TMAKE4"] = [re.compile(" tmake_all",re.M), ""]

        # Remove the SIP version selection because tmake doesn't understand
        # multiple levels and we know it's SIP v3 anyway.
        proPatches["SIPV3"] = [re.compile("sip3:",re.M), ""]
        proPatches["SIPV4"] = [re.compile("sip4:.*$",re.M), ""]
    else:
        # Make sure that references to qmake use the correct absolute pathname.
        makefilePatches["QMAKE1"] = [re.compile("(^QMAKE[ \t]*=).*$",re.M), "\\1 " + escape(makefileGen)]

        # Remove the invocation of the qmake_all target.
        makefilePatches["QMAKE2"] = [re.compile(" qmake_all",re.M), ""]

    inform("%s will be used to generate Makefiles." % (makefileGen))

    # If it is tmake then make sure TMAKEPATH is set.
    if usingTmake:
        try:
            os.environ["TMAKEPATH"]
        except:
            error("tmake requires that the TMAKEPATH environment variable is properly set.")


def tuneCompiler():
    """Tune the compiler flags.
    """
    if len(gccFlags) > 0:
        subst = "\\1"

        for f in gccFlags:
            subst = subst + " -f" + f

        subst = subst + "\n"

        makefilePatches["CXXFLAGS"] = [re.compile("(^CXXFLAGS.*)$",re.M), subst]


def compileChecks():
    """Tune the compiler flags, and see which PyQt modules to build.
    """
    tuneCompiler()

    # Check which PyQt modules will build.
    if qtVersion >= 0x020000:
        inform("Checking which additional PyQt modules to build.");

        tryModule("qtcanvas","qcanvas.h","QCanvas()")
        tryModule("qtnetwork","qsocket.h","QSocket()")
        tryModule("qttable","qtable.h","QTable()")
        tryModule("qtxml","qdom.h","QDomImplementation()")

        if qtVersion >= 0x030000:
            tryModule("qtsql","qsql.h","QSql()")

        # We need a different Makefile for the qtgl module.
        proPatches["TOPENGL"] = [re.compile("@TEST_OPENGL@",re.M), "opengl"]
        buildMakefile("qttest.pro")

        tryModule("qtgl","qgl.h","QGLWidget()")

        # Put things back.
        proPatches["TOPENGL"] = [re.compile("@TEST_OPENGL@",re.M), ""]
        buildMakefile("qttest.pro")

        # Check for the qui library.
        if qtVersion >= 0x030000:
            if sys.platform == "win32":
                quilib = "$(QTDIR)\\lib\\qui.lib"
            else:
                quilib = "-lqui"

            proPatches["TQUILIB"] = [re.compile("@TEST_QUILIB@",re.M), quilib]
            buildMakefile("qttest.pro")

            tryModule("qtui","qwidgetfactory.h","QWidgetFactory()")

        # Put things back.
        proPatches["TQUILIB"] = [re.compile("@TEST_QUILIB@",re.M), ""]
        buildMakefile("qttest.pro")

        # Check the location of the QScintilla library.
        if sciVersion >= 0:
            global sciLibDir

            if not sciLibDir:
                if qtLibDir:
                    sciLibDir = qtLibDir
                else:
                    sciLibDir = qtDir + os.sep + "lib"

            if sys.platform == "win32":
                lpatt = "\\qscintilla.lib"
            else:
                lpatt = "/libqscintilla.*"

            if len(glob.glob(sciLibDir + lpatt)) == 0:
                inform("The QScintilla library could not be found in %s and so the qtext module will not be built. If QScintilla is installed then use the -o argument to explicitly specify the correct directory." % (sciLibDir))
            else:
                proPatches["SCILIBDIR"] = [re.compile("@BL_SCILIBDIR@",re.M), escape(sciLibDir)]

                inform("%s contains the QScintilla library." % (sciLibDir))

                # We need a different Makefile for the qtext module.
                if sys.platform == "win32":
                    scilib = escape(sciLibDir + "\\qscintilla.lib")
                else:
                    scilib = escape("-L" + sciLibDir) + " -lqscintilla"

                proPatches["TSCILIBDIR"] = [re.compile("@TEST_SCILIBDIR@",re.M), scilib]
                buildMakefile("qttest.pro")

                tryModule("qtext","qextscintillabase.h","QextScintillaBase()")

                # Put things back.
                proPatches["TSCILIBDIR"] = [re.compile("@TEST_SCILIBDIR@",re.M), ""]
                buildMakefile("qttest.pro")



def generateFeatures(featfile):
    """Generate the header file describing the Qt features that are enabled if
    it doesn't already exist.  (If it already exists then we are probably cross
    compiling and generated the file through other means.)

    featfile is the name of the features file.
    """
    if os.access(featfile,os.F_OK):
        inform("Using existing features file.")
        return

    inform("Generating the features file.")

    # The features that a given Qt configuration may or may not support.  Note
    # that STYLE_WINDOWSXP requires special handling.
    flist = ["ACTION", "CLIPBOARD", "CODECS", "COLORDIALOG", "DATASTREAM",
             "DIAL", "DNS", "DOM", "DRAGANDDROP", "ICONVIEW", "IMAGE_TEXT",
             "INPUTDIALOG", "FILEDIALOG", "FONTDATABASE", "FONTDIALOG",
             "MESSAGEBOX", "MIMECLIPBOARD", "NETWORKPROTOCOL", "PICTURE",
             "PRINTDIALOG", "PRINTER", "PROGRESSDIALOG", "PROPERTIES",
             "SEMIMODAL", "SIZEGRIP", "SOUND", "SPLITTER", "STYLE_CDE",
             "STYLE_INTERLACE", "STYLE_MOTIF", "STYLE_MOTIFPLUS",
             "STYLE_PLATINUM", "STYLE_SGI", "STYLE_WINDOWS", "TABDIALOG",
             "TABLE", "TABLEVIEW", "TRANSFORMATIONS", "TRANSLATION", "WIZARD",
             "WORKSPACE"]

    # Generate the program which will generate the features file.
    f = open("qttest.cpp","w")

    # Escape the backslashes so that the same can be embedded in a C++ string.
    ffstr = string.replace(featfile,"\\","\\\\")

    f.write(
"""#include <Python.h>
#include <stdio.h>
#include <qglobal.h>
#include <qapplication.h>

int main(int argc,char **argv)
{
    FILE *fp;
    QApplication app(argc,argv,0);

    if ((fp = fopen("%s","w")) == NULL)
    {
        printf("Unable to create '%s'\\n");
        return 1;
    }

#if !defined(QT_THREAD_SUPPORT) || !defined(WITH_THREAD)
    fprintf(fp,"-x Qt_THREAD_SUPPORT\\n");
#endif

#if (defined(Q_OS_WIN32) || defined(Q_OS_WIN64)) && QT_VERSION >= 0x030000
    if (qWinVersion() != Qt::WV_XP)
        fprintf(fp,"-x Qt_STYLE_WINDOWSXP\\n");
#endif
""" % (ffstr, ffstr))

    for feat in flist:
        f.write(
"""
#if defined(QT_NO_%s)
    fprintf(fp,"-x Qt_%s\\n");
#endif
""" % (feat,feat))

    f.write(
"""
    fclose(fp);

    return 0;
}
""")

    f.close()

    runMake()
    runProgram(os.getcwd() + os.sep + "qttest")
    runMake("clean")

    inform("Generated the features file.")


def generateSource(mname,plattag,qttag,xtrtag):
    """Generate the C++ source code for a particular PyQt module.

    mname is the name of the module.
    plattag is the SIP tag for the platform.
    qttag is the SIP tag for the Qt version.
    xtrtag is an optional extra SIP tag.
    """
    inform("Generating the C++ source for the %s module." % (mname))

    try:
        shutil.rmtree(mname)
    except:
        pass

    try:
        os.mkdir(mname)
    except:
        error("Unable to create the %s directory." % (mname))

    pro = mname + ".pro"

    argv = ["-t", plattag,
            "-t", qttag,
            "-z", "features",
            "-I", "sip",
            "-m", mname + "/" + pro + ".in",
            "-c", mname,
            "sip/" + mname + "mod.sip"]

    if xtrtag:
        argv.insert(0,xtrtag)
        argv.insert(0,"-t")

    runProgram(sipBin,argv)

    # Python names binary modules differently on different platforms.  We don't
    # use the plain module name with SIP v4, even though that is the name of
    # the module we want to generate, because qmake doesn't like targets called
    # "qt".
    target = mname + "c"

    if sipVersion == 4:
        # Change the name of the target to Python module standards.
        if sys.platform == "win32":
            makefilePatches["TARGET"] = [re.compile(target + "\\.dll",re.M), mname + ".pyd"]
        else:
            makefilePatches["TARGET"] = [re.compile("lib" + target,re.M), mname]
    else:
        if sys.platform == "win32":
            target = "lib" + target

            # We want to change the module extension on Windows.
            makefilePatches["TARGET"] = [re.compile(target + "\\.dll",re.M), target + ".pyd"]
        else:
            target = target + "module"

    # Generate the Makefile.
    inform("Generating the Makefile for the %s module." % (mname))

    olddir = pushDir(mname)

    proPatches["TARGET"] = [re.compile("@BL_TARGET@",re.M), target]

    proPatches["DEBUG"] = [re.compile("@BL_DEBUG@",re.M), debugMode]

    if catCppFiles:
        catFiles(mname)

    buildMakefile(pro)

    fixInstallTarget(mname)
    del proPatches["TARGET"]

    try:
        del makefilePatches["TARGET"]
    except:
        pass

    if sipVersion == 3:
        # Compile the Python part of the module.
        pyname = mname + ".py"

        inform("Compiling %s." % (pyname))
        py_compile.compile(pyname)

    popDir(olddir)


def pushDir(newdir):
    """Change to a new directory and return the old one.

    newdir is the new directory.
    """
    olddir = os.getcwd()
    os.chdir(newdir)

    return olddir


def popDir(olddir):
    """Change back to a previous directory.

    olddir is the old directory.
    """
    os.chdir(olddir)


def runProgram(prog,argv = [],fatal = 1):
    """Execute a program.

    prog is the name of the program.
    argv is the optional list of program arguments.
    fatal is non-zero if an error is considered fatal.

    Returns the error code.
    """
    # Use the basename to avoid problems with pathnames containing spaces.
    argv.insert(0,os.path.basename(prog))

    try:
        rc = os.spawnv(os.P_WAIT,prog,argv)
    except AttributeError:
        argv[0] = prog
        rc = os.system(string.join(argv))
    except:
        raise

    if rc != 0 and fatal:
        error("%s failed with an exit code of %d." % (prog,rc))

    return rc


def runMake(target = None,fatal = 1):
    """Run make.

    target is the optional make target.
    fatal is non-zero if an error is considered fatal.

    Returns the error code.
    """
    if target is None:
        argv = []
    else:
        argv = [target]

    # As the tests are quite quick, make sure that we don't get timestamp
    # problems.
    time.sleep(1)

    return runProgram(makeBin,argv,fatal)


def buildMakefile(pro):
    """Run qmake or tmake to create a Makefile from a project file.

    pro is the name of the project file.
    """
    patchFile(pro,proPatches)

    # Make sure that the Makefile has a later timestamp than the project file.
    time.sleep(1)

    runProgram(makefileGen,['-o', 'Makefile.in', pro])

    patchFile("Makefile",makefilePatches)


def catFiles(mname):
    """Concatenate a modules C++ source files.

    mname is the name of the module.
    """
    inform("Concatenating the C++ files for the %s module." % (mname))

    pro = mname + ".pro.in"

    f = open(pro,"r")
    buf = f.read()
    f.close()

    pat = re.compile("^SOURCES =.*.cpp\n",re.M | re.S)
    match = pat.search(buf)
    srclist = buf[match.start(0):match.end(0)]
    srclist = srclist[13:-1]
    srclist = string.replace(srclist,"\\\n\t","")
    srclist = string.split(srclist," ")

    # Concatenate the files.
    p = 0
    plist = []
    nrinpiece = (len(srclist) + catSplit - 1) / catSplit

    # Do each piece.
    while srclist:
        pname = "%shuge%d.cpp" % (mname, p)
        plist.append(pname)

        d = open(pname,"w")

        for cppfile in srclist[:nrinpiece]:
            f = open(cppfile,"r")
            d.write(f.read())
            f.close()

        d.close()

        srclist = srclist[nrinpiece:]
        p = p + 1

    # Replace the C++ file names in the project file.
    buf = re.sub(pat,"SOURCES = " + string.join(plist," ") + "\n",buf)

    f = open(pro + ".new","w")
    f.write(buf)
    f.close()

    os.remove(pro)
    os.rename(pro + ".new",pro)


def fixInstallTarget(mname = None):
    """Fix the install target for a Makefile.

    mname is the name of the PyQt module, or None if the target isn't for a
    module.
    """
    f = open("Makefile","a")
    f.write("\ninstall:\n")

    if mname and sipVersion == 3:
        f.write("\t-%s %s.py %s\n" % (platCopy,mname,modDir))
        f.write("\t-%s %s.pyc %s\n" % (platCopy,mname,modDir))

    f.close()


def versionToTag(vers,tags,desc):
    """Convert a version number to a tag.

    vers is the version number.
    tags is the dictionary of tags keyed by version number.
    desc is the descriptive name of the package.

    Returns the corresponding tag.
    """
    tag = None

    vl = tags.keys()
    vl.sort()

    # For a snapshot use the latest tag.
    if vers == 0:
        tag = tags[vl[-1]]
    else:
        for v in vl:
            if vers < v:
                tag = tags[v]
                break

        if tag is None:
            error("Invalid %s version: 0x%06x." % (desc,vers))

    return tag


def main(argv):
    """The main function of the script.

    argv is the list of command line arguments.
    """
    import getopt

    # Parse the command line.
    global progName
    progName = os.path.basename(argv[0])

    initGlobals()

    try:
        optlist, args = getopt.getopt(argv[1:],"ha:b:cd:e:f:g:i:j:l:m:n:o:p:q:r:s:t:uw")
    except getopt.GetoptError:
        usage()

    explicitMake = 0

    for opt, arg in optlist:
        if opt == "-h":
            usage(0)
        elif opt == "-a":
            global qpeTag
            qpeTag = arg
        elif opt == "-b":
            global platBinDir
            platBinDir = arg
        elif opt == "-c":
            global catCppFiles
            catCppFiles = 1
        elif opt == "-d":
            global modDir
            modDir = arg
        elif opt == "-e":
            global sipIncDir
            sipIncDir = arg
        elif opt == "-f":
            global gccFlags
            gccFlags.append(arg)
        elif opt == "-g":
            global makefileGen
            makefileGen = arg
        elif opt == "-i":
            global qtIncDir
            qtIncDir = arg
        elif opt == "-j":
            global catSplit

            try:
                catSplit = int(arg)
            except:
                catSplit = 0

            if catSplit < 1:
                usage()
        elif opt == "-l":
            if arg in ("qt", "qt-mt", "qt-mtedu", "qte"):
                global qtLib
                qtLib = arg
            else:
                usage()
        elif opt == "-m":
            global makeBin
            makeBin = arg
            explicitMake = 1
        elif opt == "-n":
            global sciIncDir
            sciIncDir = arg
        elif opt == "-o":
            global sciLibDir
            sciLibDir = arg
        elif opt == "-p":
            global sipBin
            sipBin = arg
        elif opt == "-q":
            global qtDir
            qtDir = arg
        elif opt == "-r":
            global qtLibDir
            qtLibDir = arg
        elif opt == "-s":
            global sipModuleDir
            sipModuleDir = arg
        elif opt == "-t":
            global platPyLibDir
            platPyLibDir = arg
        elif opt == "-u":
            global debugMode
            debugMode = "debug"
        elif opt == "-w":
            global enableOldPython
            enableOldPython = 1

    initGlobals()

    installChecks()
    maindir = mkTempBuildDir()
    compileChecks()

    # Work out the platform and Qt version tags to pass to SIP to generate the
    # code we need.
    if qtLib == "qte":
        plattag = "WS_QWS"
    elif sys.platform == "win32":
        plattag = "WS_WIN"
    else:
        plattag = "WS_X11"

    qttags = {
        0x020000: "Qt_1_43",
        0x020100: "Qt_2_00",
        0x020200: "Qt_2_1_0",
        0x020300: "Qt_2_2_0",
        0x020301: "Qt_2_3_0",
        0x030000: "Qt_2_3_1",
        0x030001: "Qt_3_0_0",
        0x030002: "Qt_3_0_1",
        0x030004: "Qt_3_0_2",
        0x030005: "Qt_3_0_4",
        0x030006: "Qt_3_0_5",
        0x030100: "Qt_3_0_6",
        0x030101: "Qt_3_1_0",
        0x030102: "Qt_3_1_1",
        0x040000: "Qt_3_1_2"
    }

    qttag = versionToTag(qtVersion,qttags,"Qt")

    # Work out the QScintilla tag.
    if sciVersion >= 0:
        scitags = {
            0x010100: "QScintilla_1_0",
            0x020000: "QScintilla_1_1"
        }

        scitag = versionToTag(sciVersion,scitags,"QScintilla")
    else:
        scitag = None

    # Generate the features file.
    generateFeatures(maindir + os.sep + "features")

    # We don't need the temporary build directory anymore.
    mkTempBuildDir(maindir)

    subdirs = ""
    for mname in buildModules:
        if mname == "qtext":
            xtratag = scitag
        else:
            xtratag = None

        generateSource(mname,plattag,qttag,xtratag)
        subdirs = subdirs + " " + mname

    # We handle the qtpe module explicitly rather than auto-detect.  This is
    # because it does things a bit differently and I'm too lazy to deal with it
    # properly at the moment.
    if qpeTag:
        generateSource("qtpe",plattag,qttag,qpeTag)
        subdirs = subdirs + " qtpe"

    # See which version of pyuic to build.
    proPatches["BINDIR"] = [re.compile("@BL_BINDIR@",re.M), escape(platBinDir)]

    if qtVersion >= 0x030000:
        inform("Creating Makefile for pyuic3.")
        subdirs = subdirs + " pyuic3"
        olddir = pushDir("pyuic3")
    elif qtVersion >= 0x020000:
        inform("Creating Makefile for pyuic2.")
        subdirs = subdirs + " pyuic2"
        olddir = pushDir("pyuic2")

    buildMakefile("pyuic.pro")
    fixInstallTarget()
    popDir(olddir)

    # Build pylupdate if Qt v3.0 or later.
    if qtVersion >= 0x030000:
        inform("Creating Makefile for pylupdate3.")
        subdirs = subdirs + " pylupdate3"
        olddir = pushDir("pylupdate3")

        buildMakefile("pylupdate.pro")
        fixInstallTarget()
        popDir(olddir)

    # Generate the top-level Makefile.
    inform("Creating top level Makefile.")
    copyToFile("PyQt.pro.in","TEMPLATE = subdirs\nSUBDIRS =" + subdirs + "\n")
    buildMakefile("PyQt.pro")

    # Tell the user what to do next.
    if explicitMake:
        mk = makeBin
    else:
        (mk,ignore) = os.path.splitext(os.path.basename(makeBin))

    msg = "The build of the PyQt source code for your system is now complete. To compile and install PyQt run \"%s\" and \"%s install\" with appropriate user privileges." % (mk,mk)

    if qtVersion >= 0x030000:
        msg = msg + " eric the IDE written using PyQt is now available separately from http://www.die-offenbachs.de/detlev/eric3.html."

    inform(msg)


if __name__ == "__main__":
    try:
        main(sys.argv)
    except SystemExit:
        raise
    except:
        print \
"""An internal error occured.  Please report all the output from the program,
including the following traceback, to support@riverbankcomputing.co.uk.
"""
        raise
