Beispiel #1
0
                raise ValueError("%s: input file too large" % srcfile)

            # Split path into components.
            srcpathcomp = srcfile.split(os.sep)
            targetpathcomp = [s.decode(filesystemenc) for s in srcpathcomp]

            # Check if the file is an E32Image (EXE or DLL).
            filecapmask = symbianutil.e32imagecaps(string)

            # Warn against common mistakes when dealing with E32Image files.
            if filecapmask != None:
                if not srcfile.startswith(sysbinprefix):
                    # Warn against E32Image files outside /sys/bin.
                    print ("%s: warning: %s is an E32Image (EXE or DLL) "
                           "outside %s" % (pgmname, srcfile, sysbinprefix))
                elif (symbianutil.ise32image(string) == "DLL" and
                      (filecapmask & ~capmask) != 0x00000000L):
                    # Warn about insufficient capabilities to load
                    # a DLL from the PyS60 application.
                    print ("%s: warning: insufficient capabilities to "
                           "load %s" % (pgmname, srcfile))

            # Handle the extras directory.
            if extrasdir != None and extrasdir == srcpathcomp[0]:
                # Path is rooted at the drive root.
                targetfile = u"%s:\\%s" % (drive, "\\".join(targetpathcomp[1:]))
            else:
                # Path is rooted at the application private directory.
                targetfile = u"%s:\\private\\%08x\\%s" % (
                    drive, uid3, "\\".join(targetpathcomp))
Beispiel #2
0
def run(pgmname, argv):
    global debug

    # Determine system character encodings.
    try:
        # getdefaultlocale() may sometimes return None.
        # Fall back to ASCII encoding in that case.
        terminalenc = locale.getdefaultlocale()[1] + ""
    except TypeError:
        # Invalid locale, fall back to ASCII terminal encoding.
        terminalenc = "ascii"

    try:
        # sys.getfilesystemencoding() was introduced in Python v2.3 and
        # it can sometimes return None. Fall back to ASCII if something
        # goes wrong.
        filesystemenc = sys.getfilesystemencoding() + ""
    except (AttributeError, TypeError):
        filesystemenc = "ascii"

    try:
        gopt = getopt.gnu_getopt
    except:
        # Python <v2.3, GNU-style parameter ordering not supported.
        gopt = getopt.getopt

    # Parse command line arguments.
    short_opts = "u:n:r:l:i:s:c:f:x:t:a:k:p:b:d:gRH:e:vh"
    long_opts = [
        "uid=", "appname=", "version=", "lang=", "icon=",
        "shortcaption=", "caption=", "drive=", "extrasdir=", "textfile=",
        "cert=", "privkey=", "passphrase=", "unsigned", "caps=", "vendor=",
        "autostart", "runinstall", "heapsize=",
        "encoding=", "verbose", "debug", "help"
    ]
    args = gopt(argv, short_opts, long_opts)

    opts = dict(args[0])
    pargs = args[1]

    if len(pargs) == 0:
        raise ValueError("no source file name given")

    # Override character encoding of command line and filesystem.
    encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc,
                                                            filesystemenc)))
    try:
        terminalenc, filesystemenc = encs.split(",")
    except (ValueError, TypeError):
        raise ValueError("invalid encoding string '%s'" % encs)

    # Get source name, either a Python program or a directory.
    src = pargs[0].decode(terminalenc).encode(filesystemenc)
    if os.path.isdir(src):
        # Remove trailing slashes (or whatever the separator is).
        src = os.path.split(src + os.sep)[0]

        # Use last directory component as the name.
        basename = os.path.basename(src)

        # Source is a directory, recursively collect files it contains.
        srcdir = src
        srcfiles = []
        prefixlen = len(srcdir) + len(os.sep)
        def getfiles(arg, dirname, names):
            for name in names:
                path = os.path.join(dirname, name)
                if not os.path.isdir(path):
                    arg.append(path[prefixlen:])
        os.path.walk(srcdir, getfiles, srcfiles)

        # Read application version and UID3 from default.py.
        version, uid3 = scandefaults(os.path.join(srcdir, "default.py"))
    else:
        if src.lower().endswith(".py"):
            # Use program name without the .py extension.
            basename = os.path.basename(src)[:-3]
        else:
            # Unknown extension, use program name as-is.
            basename = os.path.basename(src)

        # Source is a file, use it.
        srcdir, srcfiles = os.path.split(src)
        srcfiles = [srcfiles]

        # Read application version and UID3 from file.
        version, uid3 = scandefaults(os.path.join(srcdir, srcfiles[0]))

    # Parse version string, use 1.0.0 by default.
    version = opts.get("--version", opts.get("-r", version))
    if version == None:
        version = "1.0.0"
        print ("%s: warning: no application version given, "
               "using %s" % (pgmname, version))
    try:
        version = parseversion(version)
    except (ValueError, IndexError, TypeError):
        raise ValueError("invalid version string '%s'" % version)

    # Determine output SIS file name.
    if len(pargs) == 1:
        # Derive output file name from input file name.
        outfile = "%s_v%d_%d_%d.sis" % (basename, version[0],
                                        version[1], version[2])
    elif len(pargs) == 2:
        outfile = pargs[1].decode(terminalenc).encode(filesystemenc)
        if os.path.isdir(outfile):
            # Output to directory, derive output name from input file name.
            outfile = os.path.join(outfile, "%s_v%d_%d_%d.sis" % (
                basename, version[0], version[1], version[2]))
        if not outfile.lower().endswith(".sis"):
            outfile += ".sis"
    else:
        raise ValueError("wrong number of arguments")

    # Determine application name (install dir.), use basename by default.
    appname = opts.get("--appname", opts.get("-n", basename))
    appname = appname.decode(terminalenc)

    # Auto-generate a test-range UID from application name.
    autouid = symbianutil.uidfromname(appname)

    # Get UID3.
    uid3 = opts.get("--uid", opts.get("-u", uid3))
    if uid3 == None:
        # No UID given, use auto-generated UID.
        uid3 = autouid
        print ("%s: warning: no UID given, using auto-generated "
               "test-range UID 0x%08x" % (pgmname, uid3))
    elif uid3.lower().startswith("0x"):
        # Prefer hex UIDs with leading "0x".
        uid3 = long(uid3, 16)
    else:
        try:
            if len(uid3) == 8:
                # Assuming hex UID even without leading "0x".
                print ('%s: warning: assuming hex UID even '
                       'without leading "0x"' % pgmname)
                uid3 = long(uid3, 16)
            else:
                # Decimal UID.
                uid3 = long(uid3)
                print ('%s: warning: decimal UID converted to 0x%08x' %
                       (pgmname, uid3))
        except ValueError:
            raise ValueError("invalid UID string '%s'" % uid3)

    # Warn against specifying a test-range UID manually.
    #if uid3 & 0xf0000000L == 0xe0000000L and uid3 != autouid:
    #    print ("%s: warning: manually specifying a test-range UID is "
    #           "not recommended" % pgmname)

    # Determine application language(s), use "EN" by default.
    lang = opts.get("--lang", opts.get("-l", "EN")).split(",")
    numlang = len(lang)

    # Verify that the language codes are correct.
    for l in lang:
        try:
            symbianutil.langidtonum[l]
        except KeyError:
            raise ValueError("%s: no such language code" % l)

    # Get icon file name.
    icon = opts.get("--icon", opts.get("-i", None))
    if icon != None:
        icon = icon.decode(terminalenc).encode(filesystemenc)

        # Read icon file.
        f = file(icon, "rb")
        icondata = f.read(MAXICONFILESIZE + 1)
        f.close()

        if len(icondata) > MAXICONFILESIZE:
            raise ValueError("icon file too large")
    else:
        # No icon given, use a default icon.
        icondata = zlib.decompress(defaulticondata.decode("base-64"))

    # Determine application short caption(s).
    shortcaption = opts.get("--shortcaption", opts.get("-s", ""))
    shortcaption = shortcaption.decode(terminalenc)
    if len(shortcaption) == 0:
        # Short caption not given, use application name.
        shortcaption = [appname] * numlang
    else:
        shortcaption = shortcaption.split(",")

    # Determine application long caption(s), use short caption by default.
    caption = opts.get("--caption", opts.get("-c", ""))
    caption = caption.decode(terminalenc)
    if len(caption) == 0:
        # Caption not given, use short caption.
        caption = shortcaption
    else:
        caption = caption.split(",")

    # Compare the number of languages and captions.
    if len(shortcaption) != numlang or len(caption) != numlang:
        raise ValueError("invalid number of captions")

    # Determine installation drive, any by default.
    drive = opts.get("--drive", opts.get("-f", "any")).upper()
    if drive == "ANY" or drive == "!":
        drive = "!"
    elif drive != "C" and drive != "E":
        raise ValueError("%s: invalid drive letter" % drive)

    # Determine vendor name(s), use "Ensymble" by default.
    vendor = opts.get("--vendor", opts.get("-d", "Ensymble"))
    vendor = vendor.decode(terminalenc)
    vendor = vendor.split(",")
    if len(vendor) == 1:
        # Only one vendor name given, use it for all languages.
        vendor = vendor * numlang
    elif len(vendor) != numlang:
        raise ValueError("invalid number of vendor names")

    extrasdir = opts.get("--extrasdir", opts.get("-x", None))
    if extrasdir != None:
        extrasdir = extrasdir.decode(terminalenc).encode(filesystemenc)
        if extrasdir[-1] == os.sep:
            # Strip trailing slash (or backslash).
            extrasdir = extrasdir[:-1]

        if os.sep in extrasdir:
            raise ValueError("%s: too many path components" % extrasdir)

    # Load text files.
    texts = []
    textfile = opts.get("--textfile", opts.get("-t", None))
    if textfile != None:
        texts = readtextfiles(textfile, lang)

    # Get certificate and its private key file names.
    cert = opts.get("--cert", opts.get("-a", None))
    privkey = opts.get("--privkey", opts.get("-k", None))
    unsign = ("--unsigned" in opts)
    if unsign:
        certdata = None
        privkeydata = None
    elif cert != None and privkey != None:
        # Convert file names from terminal encoding to filesystem encoding.
        cert = cert.decode(terminalenc).encode(filesystemenc)
        privkey = privkey.decode(terminalenc).encode(filesystemenc)

        # Read certificate file.
        f = file(cert, "rb")
        certdata = f.read(MAXCERTIFICATELENGTH + 1)
        f.close()

        if len(certdata) > MAXCERTIFICATELENGTH:
            raise ValueError("certificate file too large")

        # Read private key file.
        f = file(privkey, "rb")
        privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1)
        f.close()

        if len(privkeydata) > MAXPRIVATEKEYLENGTH:
            raise ValueError("private key file too large")
    elif cert == None and privkey == None:
        # No certificate given, use the Ensymble default certificate.
        # defaultcert.py is not imported when not needed. This speeds
        # up program start-up a little.
        from utils import defaultcert
        certdata = defaultcert.cert
        privkeydata = defaultcert.privkey

        print ("%s: warning: no certificate given, using "
               "insecure built-in one" % pgmname)

        # Warn if the UID is in the protected range.
        # Resulting SIS file will probably not install.
        if uid3 < 0x80000000L:
            print ("%s: warning: UID is in the protected range "
                   "(0x00000000 - 0x7ffffff)" % pgmname)
    else:
        raise ValueError("missing certificate or private key")

    # Get pass phrase. Pass phrase remains in terminal encoding.
    passphrase = opts.get("--passphrase", opts.get("-p", None))
    if passphrase == None and privkey != None:
        # Private key given without "--passphrase" option, ask it.
        if sys.stdin.isatty():
            # Standard input is a TTY, ask password interactively.
            passphrase = getpass.getpass("Enter private key pass phrase:")
        else:
            # Not connected to a TTY, read stdin non-interactively instead.
            passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1)

            if len(passphrase) > MAXPASSPHRASELENGTH:
                raise ValueError("pass phrase too long")

            passphrase = passphrase.strip()

    # Get capabilities and normalize the names.
    caps = opts.get("--caps", opts.get("-b", ""))
    capmask = symbianutil.capstringtomask(caps)
    caps = symbianutil.capmasktostring(capmask, True)

    # Determine if the application is requested to start on each device boot.
    autostart = False
    if "--autostart" in opts.keys() or "-g" in opts.keys():
        autostart = True

    runinstall = False
    if "--runinstall" in opts.keys() or "-R" in opts.keys():
        runinstall = True

    # Get heap sizes.
    heapsize = opts.get("--heapsize", opts.get("-H", "4k,1M")).split(",", 1)
    try:
        heapsizemin = symbianutil.parseintmagnitude(heapsize[0])
        if len(heapsize) == 1:
            # Only one size given, use it as both.
            heapsizemax = heapsizemin
        else:
            heapsizemax = symbianutil.parseintmagnitude(heapsize[1])
    except (ValueError, TypeError, IndexError):
        raise ValueError("%s: invalid heap size, one or two values expected" %
                         ",".join(heapsize))

    # Warn if the minimum heap size is larger than the maximum heap size.
    # Resulting SIS file will probably not install.
    if heapsizemin > heapsizemax:
        print ("%s: warning: minimum heap size larger than "
               "maximum heap size" % pgmname)

    # Determine verbosity.
    verbose = False
    if "--verbose" in opts.keys() or "-v" in opts.keys():
        verbose = True

    # Determine if debug output is requested.
    if "--debug" in opts.keys():
        debug = True

        # Enable debug output for OpenSSL-related functions.
        import cryptutil
        cryptutil.setdebug(True)

    # Ingredients for successful SIS generation:
    #
    # terminalenc   Terminal character encoding (autodetected)
    # filesystemenc File system name encoding (autodetected)
    # basename      Base for generated file names on host, filesystemenc encoded
    # srcdir        Directory of source files, filesystemenc encoded
    # srcfiles      List of filesystemenc encoded source file names in srcdir
    # outfile       Output SIS file name, filesystemenc encoded
    # uid3          Application UID3, long integer
    # appname       Application name and install directory in device, in Unicode
    # version       A triple-item tuple (major, minor, build)
    # lang          List of two-character language codes, ASCII strings
    # icon          Icon data, a binary string typically containing a SVG-T file
    # shortcaption  List of Unicode short captions, one per language
    # caption       List of Unicode long captions, one per language
    # drive         Installation drive letter or "!"
    # extrasdir     Path prefix for extra files, filesystemenc encoded or None
    # textfile      File name pattern of text file(s) to display during install
    # texts         Actual texts to display during install, one per language
    # cert          Certificate in PEM format
    # privkey       Certificate private key in PEM format
    # passphrase    Pass phrase of private key, terminalenc encoded string
    # caps, capmask Capability names and bitmask
    # vendor        List of Unicode vendor names, one per language
    # autostart     Boolean requesting application autostart on device boot
    # runinstall    Boolean requesting application autorun after installation
    # heapsizemin   Heap that must be available for the application to start
    # heapsizemax   Maximum amount of heap the application can allocate
    # verbose       Boolean indicating verbose terminal output

    if verbose:
        print
        print "Input file(s)       %s"      % " ".join(
            [s.decode(filesystemenc).encode(terminalenc) for s in srcfiles])
        print "Output SIS file     %s"      % (
            outfile.decode(filesystemenc).encode(terminalenc))
        print "UID                 0x%08x"  % uid3
        print "Application name    %s"      % appname.encode(terminalenc)
        print "Version             %d.%d.%d"    % (
            version[0], version[1], version[2])
        print "Language(s)         %s"      % ", ".join(lang)
        print "Icon                %s"      % ((icon and
            icon.decode(filesystemenc).encode(terminalenc)) or "<default>")
        print "Short caption(s)    %s"      % ", ".join(
            [s.encode(terminalenc) for s in shortcaption])
        print "Long caption(s)     %s"      % ", ".join(
            [s.encode(terminalenc) for s in caption])
        print "Install drive       %s"      % ((drive == "!") and
            "<any>" or drive)
        print "Extras directory    %s"      % ((extrasdir and
            extrasdir.decode(filesystemenc).encode(terminalenc)) or "<none>")
        print "Text file(s)        %s"      % ((textfile and
            textfile.decode(filesystemenc).encode(terminalenc)) or "<none>")
        print "Sign?               %s"      % (unsign and "no" or "yes")
        print "Certificate         %s"      % ((cert and
            cert.decode(filesystemenc).encode(terminalenc)) or "<default>")
        print "Private key         %s"      % ((privkey and
            privkey.decode(filesystemenc).encode(terminalenc)) or "<default>")
        print "Capabilities        0x%x (%s)" % (capmask, caps)
        print "Vendor name(s)      %s"      % ", ".join(
            [s.encode(terminalenc) for s in vendor])
        print "Autostart on boot   %s"      % ((autostart and "Yes") or "No")
        print "Run after install   %s"      % ((runinstall and "Yes") or "No")
        print "Heap size in bytes  %d, %d" % (heapsizemin, heapsizemax)
        print

    # Generate SimpleSISWriter object.
    sw = sisfile.SimpleSISWriter(lang, caption, uid3, version,
                                 vendor[0], vendor)

    # Add text file or files to the SIS object. Text dialog is
    # supposed to be displayed before anything else is installed.
    if len(texts) == 1:
        sw.addfile(texts[0], operation = sisfield.EOpText)
    elif len(texts) > 1:
        sw.addlangdepfile(texts, operation = sisfield.EOpText)

    # Generate "Python for S60" resource file.
    rsctarget = u"%s:\\resource\\apps\\%s_0x%08x.rsc" % (drive, appname, uid3)
    string = zlib.decompress(pythons60rscdata.decode("base-64"))
    sw.addfile(string, rsctarget)
    del string

    # Generate application registration resource file.
    regtarget = u"%s:\\private\\10003a3f\\import\\apps\\%s_0x%08x_reg.rsc" % (
        drive, appname, uid3)
    exename = u"%s_0x%08x" % (appname, uid3)
    locpath = u"\\resource\\apps\\%s_0x%08x_loc" % (appname, uid3)
    rw = rscfile.RSCWriter(uid2 = 0x101f8021, uid3 = uid3)
    # STRUCT APP_REGISTRATION_INFO from appinfo.rh
    res = rscfile.Resource(["LONG", "LLINK", "LTEXT", "LONG", "LTEXT", "LONG",
                            "BYTE", "BYTE", "BYTE", "BYTE", "LTEXT", "BYTE",
                            "WORD", "WORD", "WORD", "LLINK"],
                           0, 0, exename, 0, locpath, 1,
                           0, 0, 0, 0, "", 0,
                           0, 0, 0, 0)
    rw.addresource(res)
    string = rw.tostring()
    del rw
    sw.addfile(string, regtarget)
    del string

    # EXE target name
    exetarget = u"%s:\\sys\\bin\\%s_0x%08x.exe" % (drive, appname, uid3)

    # Generate autostart registration resource file, if requested.
    if autostart:
        autotarget = u"%s:\\private\\101f875a\\import\\[%08x].rsc" % (
            drive, uid3)
        rw = rscfile.RSCWriter(uid2 = 0, offset = "    ")
        # STRUCT STARTUP_ITEM_INFO from startupitem.rh
        res = rscfile.Resource(["BYTE", "LTEXT", "WORD",
                                "LONG", "BYTE", "BYTE"],
                               0, exetarget, 0, 0, 0, 0)
        rw.addresource(res)
        string = rw.tostring()
        del rw
        sw.addfile(string, autotarget)
        del string

    # Generate localisable icon/caption definition resource files.
    iconpath = "\\resource\\apps\\%s_0x%08x_aif.mif" % (appname, uid3)
    for n in xrange(numlang):
        loctarget = u"%s:\\resource\\apps\\%s_0x%08x_loc.r%02d" % (
            drive, appname, uid3, symbianutil.langidtonum[lang[n]])
        rw = rscfile.RSCWriter(uid2 = 0, offset = "    ")
        # STRUCT LOCALISABLE_APP_INFO from appinfo.rh
        res = rscfile.Resource(["LONG", "LLINK", "LTEXT",
                                "LONG", "LLINK", "LTEXT",
                                "WORD", "LTEXT", "WORD", "LTEXT"],
                               0, 0, shortcaption[n],
                               0, 0, caption[n],
                               1, iconpath, 0, "")
        rw.addresource(res)
        string = rw.tostring()
        del rw
        sw.addfile(string, loctarget)
        del string

    # Generate MIF file for icon.
    icontarget = "%s:\\resource\\apps\\%s_0x%08x_aif.mif" % (
        drive, appname, uid3)
    mw = miffile.MIFWriter()
    mw.addfile(icondata)
    del icondata
    string = mw.tostring()
    del mw
    sw.addfile(string, icontarget)
    del string

    # Add files to SIS object.
    if len(srcfiles) == 1:
        # Read file.
        f = file(os.path.join(srcdir, srcfiles[0]), "rb")
        string = f.read(MAXOTHERFILESIZE + 1)
        f.close()

        if len(string) > MAXOTHERFILESIZE:
            raise ValueError("%s: input file too large" % srcfiles[0])

        # Add file to the SIS object. One file only, rename it to default.py.
        target = "default.py"
        sw.addfile(string, "%s:\\private\\%08x\\%s" % (drive, uid3, target))
        del string
    else:
        if extrasdir != None:
            sysbinprefix = os.path.join(extrasdir, "sys", "bin", "")
        else:
            sysbinprefix = os.path.join(os.sep, "sys", "bin", "")

        # More than one file, use original path names.
        for srcfile in srcfiles:
            # Read file.
            f = file(os.path.join(srcdir, srcfile), "rb")
            string = f.read(MAXOTHERFILESIZE + 1)
            f.close()

            if len(string) > MAXOTHERFILESIZE:
                raise ValueError("%s: input file too large" % srcfile)

            # Split path into components.
            srcpathcomp = srcfile.split(os.sep)
            targetpathcomp = [s.decode(filesystemenc) for s in srcpathcomp]

            # Check if the file is an E32Image (EXE or DLL).
            filecapmask = symbianutil.e32imagecaps(string)

            # Warn against common mistakes when dealing with E32Image files.
            if filecapmask != None:
                if not srcfile.startswith(sysbinprefix):
                    # Warn against E32Image files outside /sys/bin.
                    print ("%s: warning: %s is an E32Image (EXE or DLL) "
                           "outside %s" % (pgmname, srcfile, sysbinprefix))
                elif (symbianutil.ise32image(string) == "DLL" and
                      (filecapmask & ~capmask) != 0x00000000L):
                    # Warn about insufficient capabilities to load
                    # a DLL from the PyS60 application.
                    print ("%s: warning: insufficient capabilities to "
                           "load %s" % (pgmname, srcfile))

            # Handle the extras directory.
            if extrasdir != None and extrasdir == srcpathcomp[0]:
                # Path is rooted at the drive root.
                targetfile = u"%s:\\%s" % (drive, "\\".join(targetpathcomp[1:]))
            else:
                # Path is rooted at the application private directory.
                targetfile = u"%s:\\private\\%08x\\%s" % (
                    drive, uid3, "\\".join(targetpathcomp))

            # Add file to the SIS object.
            sw.addfile(string, targetfile, capabilities = filecapmask)
            del string

    # Add target device dependency.
    sw.addtargetdevice(0x101f7961L, (0, 0, 0), None,
                       ["Series60ProductID"] * numlang)

    # Add "Python for S60" dependency, version 1.4.0 onwards.
    # NOTE: Previous beta versions of Python for S60 had a
    # different UID3 (0xf0201510).
    sw.adddependency(0x2000b1a0L, (1, 4, 0), None,
                     ["Python for S60"] * numlang)

    # Add certificate.
    if certdata is not None:
        #print (privkeydata, certdata, passphrase)
        sw.addcertificate(privkeydata, certdata, passphrase)

    # Generate an EXE stub and add it to the SIS object.
    string = execstubdata.decode("base-64")
    string = symbianutil.e32imagecrc(string, uid3, uid3, None,
                                     heapsizemin, heapsizemax, capmask)
    if runinstall:
        # To avoid running without dependencies, this has to be in the end.
        sw.addfile(string, exetarget, None, capabilities = capmask,
                   operation = sisfield.EOpRun,
                   options = sisfield.EInstFileRunOptionInstall)
    else:
        sw.addfile(string, exetarget, None, capabilities = capmask)

    del string

    # Generate SIS file out of the SimpleSISWriter object.
    sw.tofile(outfile)
Beispiel #3
0
def run(pgmname, argv):
    global debug

    # Determine system character encodings.
    try:
        # getdefaultlocale() may sometimes return None.
        # Fall back to ASCII encoding in that case.
        terminalenc = locale.getdefaultlocale()[1] + ""
    except TypeError:
        # Invalid locale, fall back to ASCII terminal encoding.
        terminalenc = "ascii"

    try:
        # sys.getfilesystemencoding() was introduced in Python v2.3 and
        # it can sometimes return None. Fall back to ASCII if something
        # goes wrong.
        filesystemenc = sys.getfilesystemencoding() + ""
    except (AttributeError, TypeError):
        filesystemenc = "ascii"

    try:
        gopt = getopt.gnu_getopt
    except:
        # Python <v2.3, GNU-style parameter ordering not supported.
        gopt = getopt.getopt

    # Parse command line arguments.
    short_opts = "u:n:r:l:i:s:c:f:x:t:a:k:p:b:d:gRH:e:vh"
    long_opts = [
        "uid=", "appname=", "version=", "lang=", "icon=", "shortcaption=",
        "caption=", "drive=", "extrasdir=", "textfile=", "cert=", "privkey=",
        "passphrase=", "unsigned", "caps=", "vendor=", "autostart",
        "runinstall", "heapsize=", "encoding=", "verbose", "debug", "help"
    ]
    args = gopt(argv, short_opts, long_opts)

    opts = dict(args[0])
    pargs = args[1]

    if len(pargs) == 0:
        raise ValueError("no source file name given")

    # Override character encoding of command line and filesystem.
    encs = opts.get("--encoding",
                    opts.get("-e", "%s,%s" % (terminalenc, filesystemenc)))
    try:
        terminalenc, filesystemenc = encs.split(",")
    except (ValueError, TypeError):
        raise ValueError("invalid encoding string '%s'" % encs)

    # Get source name, either a Python program or a directory.
    src = pargs[0].decode(terminalenc).encode(filesystemenc)
    if os.path.isdir(src):
        # Remove trailing slashes (or whatever the separator is).
        src = os.path.split(src + os.sep)[0]

        # Use last directory component as the name.
        basename = os.path.basename(src)

        # Source is a directory, recursively collect files it contains.
        srcdir = src
        srcfiles = []
        prefixlen = len(srcdir) + len(os.sep)

        def getfiles(arg, dirname, names):
            for name in names:
                path = os.path.join(dirname, name)
                if not os.path.isdir(path):
                    arg.append(path[prefixlen:])

        os.path.walk(srcdir, getfiles, srcfiles)

        # Read application version and UID3 from default.py.
        version, uid3 = scandefaults(os.path.join(srcdir, "default.py"))
    else:
        if src.lower().endswith(".py"):
            # Use program name without the .py extension.
            basename = os.path.basename(src)[:-3]
        else:
            # Unknown extension, use program name as-is.
            basename = os.path.basename(src)

        # Source is a file, use it.
        srcdir, srcfiles = os.path.split(src)
        srcfiles = [srcfiles]

        # Read application version and UID3 from file.
        version, uid3 = scandefaults(os.path.join(srcdir, srcfiles[0]))

    # Parse version string, use 1.0.0 by default.
    version = opts.get("--version", opts.get("-r", version))
    if version == None:
        version = "1.0.0"
        print("%s: warning: no application version given, "
              "using %s" % (pgmname, version))
    try:
        version = parseversion(version)
    except (ValueError, IndexError, TypeError):
        raise ValueError("invalid version string '%s'" % version)

    # Determine output SIS file name.
    if len(pargs) == 1:
        # Derive output file name from input file name.
        outfile = "%s_v%d_%d_%d.sis" % (basename, version[0], version[1],
                                        version[2])
    elif len(pargs) == 2:
        outfile = pargs[1].decode(terminalenc).encode(filesystemenc)
        if os.path.isdir(outfile):
            # Output to directory, derive output name from input file name.
            outfile = os.path.join(
                outfile, "%s_v%d_%d_%d.sis" %
                (basename, version[0], version[1], version[2]))
        if not outfile.lower().endswith(".sis"):
            outfile += ".sis"
    else:
        raise ValueError("wrong number of arguments")

    # Determine application name (install dir.), use basename by default.
    appname = opts.get("--appname", opts.get("-n", basename))
    appname = appname.decode(terminalenc)

    # Auto-generate a test-range UID from application name.
    autouid = symbianutil.uidfromname(appname)

    # Get UID3.
    uid3 = opts.get("--uid", opts.get("-u", uid3))
    if uid3 == None:
        # No UID given, use auto-generated UID.
        uid3 = autouid
        print(
            "%s: warning: no UID given, using auto-generated "
            "test-range UID 0x%08x" % (pgmname, uid3))
    elif uid3.lower().startswith("0x"):
        # Prefer hex UIDs with leading "0x".
        uid3 = long(uid3, 16)
    else:
        try:
            if len(uid3) == 8:
                # Assuming hex UID even without leading "0x".
                print(
                    '%s: warning: assuming hex UID even '
                    'without leading "0x"' % pgmname)
                uid3 = long(uid3, 16)
            else:
                # Decimal UID.
                uid3 = long(uid3)
                print('%s: warning: decimal UID converted to 0x%08x' %
                      (pgmname, uid3))
        except ValueError:
            raise ValueError("invalid UID string '%s'" % uid3)

    # Warn against specifying a test-range UID manually.
    #if uid3 & 0xf0000000L == 0xe0000000L and uid3 != autouid:
    #    print ("%s: warning: manually specifying a test-range UID is "
    #           "not recommended" % pgmname)

    # Determine application language(s), use "EN" by default.
    lang = opts.get("--lang", opts.get("-l", "EN")).split(",")
    numlang = len(lang)

    # Verify that the language codes are correct.
    for l in lang:
        try:
            symbianutil.langidtonum[l]
        except KeyError:
            raise ValueError("%s: no such language code" % l)

    # Get icon file name.
    icon = opts.get("--icon", opts.get("-i", None))
    if icon != None:
        icon = icon.decode(terminalenc).encode(filesystemenc)

        # Read icon file.
        f = file(icon, "rb")
        icondata = f.read(MAXICONFILESIZE + 1)
        f.close()

        if len(icondata) > MAXICONFILESIZE:
            raise ValueError("icon file too large")
    else:
        # No icon given, use a default icon.
        icondata = zlib.decompress(defaulticondata.decode("base-64"))

    # Determine application short caption(s).
    shortcaption = opts.get("--shortcaption", opts.get("-s", ""))
    shortcaption = shortcaption.decode(terminalenc)
    if len(shortcaption) == 0:
        # Short caption not given, use application name.
        shortcaption = [appname] * numlang
    else:
        shortcaption = shortcaption.split(",")

    # Determine application long caption(s), use short caption by default.
    caption = opts.get("--caption", opts.get("-c", ""))
    caption = caption.decode(terminalenc)
    if len(caption) == 0:
        # Caption not given, use short caption.
        caption = shortcaption
    else:
        caption = caption.split(",")

    # Compare the number of languages and captions.
    if len(shortcaption) != numlang or len(caption) != numlang:
        raise ValueError("invalid number of captions")

    # Determine installation drive, any by default.
    drive = opts.get("--drive", opts.get("-f", "any")).upper()
    if drive == "ANY" or drive == "!":
        drive = "!"
    elif drive != "C" and drive != "E":
        raise ValueError("%s: invalid drive letter" % drive)

    # Determine vendor name(s), use "Ensymble" by default.
    vendor = opts.get("--vendor", opts.get("-d", "Ensymble"))
    vendor = vendor.decode(terminalenc)
    vendor = vendor.split(",")
    if len(vendor) == 1:
        # Only one vendor name given, use it for all languages.
        vendor = vendor * numlang
    elif len(vendor) != numlang:
        raise ValueError("invalid number of vendor names")

    extrasdir = opts.get("--extrasdir", opts.get("-x", None))
    if extrasdir != None:
        extrasdir = extrasdir.decode(terminalenc).encode(filesystemenc)
        if extrasdir[-1] == os.sep:
            # Strip trailing slash (or backslash).
            extrasdir = extrasdir[:-1]

        if os.sep in extrasdir:
            raise ValueError("%s: too many path components" % extrasdir)

    # Load text files.
    texts = []
    textfile = opts.get("--textfile", opts.get("-t", None))
    if textfile != None:
        texts = readtextfiles(textfile, lang)

    # Get certificate and its private key file names.
    cert = opts.get("--cert", opts.get("-a", None))
    privkey = opts.get("--privkey", opts.get("-k", None))
    unsign = ("--unsigned" in opts)
    if unsign:
        certdata = None
        privkeydata = None
    elif cert != None and privkey != None:
        # Convert file names from terminal encoding to filesystem encoding.
        cert = cert.decode(terminalenc).encode(filesystemenc)
        privkey = privkey.decode(terminalenc).encode(filesystemenc)

        # Read certificate file.
        f = file(cert, "rb")
        certdata = f.read(MAXCERTIFICATELENGTH + 1)
        f.close()

        if len(certdata) > MAXCERTIFICATELENGTH:
            raise ValueError("certificate file too large")

        # Read private key file.
        f = file(privkey, "rb")
        privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1)
        f.close()

        if len(privkeydata) > MAXPRIVATEKEYLENGTH:
            raise ValueError("private key file too large")
    elif cert == None and privkey == None:
        # No certificate given, use the Ensymble default certificate.
        # defaultcert.py is not imported when not needed. This speeds
        # up program start-up a little.
        from utils import defaultcert
        certdata = defaultcert.cert
        privkeydata = defaultcert.privkey

        print(
            "%s: warning: no certificate given, using "
            "insecure built-in one" % pgmname)

        # Warn if the UID is in the protected range.
        # Resulting SIS file will probably not install.
        if uid3 < 0x80000000L:
            print(
                "%s: warning: UID is in the protected range "
                "(0x00000000 - 0x7ffffff)" % pgmname)
    else:
        raise ValueError("missing certificate or private key")

    # Get pass phrase. Pass phrase remains in terminal encoding.
    passphrase = opts.get("--passphrase", opts.get("-p", None))
    if passphrase == None and privkey != None:
        # Private key given without "--passphrase" option, ask it.
        if sys.stdin.isatty():
            # Standard input is a TTY, ask password interactively.
            passphrase = getpass.getpass("Enter private key pass phrase:")
        else:
            # Not connected to a TTY, read stdin non-interactively instead.
            passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1)

            if len(passphrase) > MAXPASSPHRASELENGTH:
                raise ValueError("pass phrase too long")

            passphrase = passphrase.strip()

    # Get capabilities and normalize the names.
    caps = opts.get("--caps", opts.get("-b", ""))
    capmask = symbianutil.capstringtomask(caps)
    caps = symbianutil.capmasktostring(capmask, True)

    # Determine if the application is requested to start on each device boot.
    autostart = False
    if "--autostart" in opts.keys() or "-g" in opts.keys():
        autostart = True

    runinstall = False
    if "--runinstall" in opts.keys() or "-R" in opts.keys():
        runinstall = True

    # Get heap sizes.
    heapsize = opts.get("--heapsize", opts.get("-H", "4k,1M")).split(",", 1)
    try:
        heapsizemin = symbianutil.parseintmagnitude(heapsize[0])
        if len(heapsize) == 1:
            # Only one size given, use it as both.
            heapsizemax = heapsizemin
        else:
            heapsizemax = symbianutil.parseintmagnitude(heapsize[1])
    except (ValueError, TypeError, IndexError):
        raise ValueError("%s: invalid heap size, one or two values expected" %
                         ",".join(heapsize))

    # Warn if the minimum heap size is larger than the maximum heap size.
    # Resulting SIS file will probably not install.
    if heapsizemin > heapsizemax:
        print(
            "%s: warning: minimum heap size larger than "
            "maximum heap size" % pgmname)

    # Determine verbosity.
    verbose = False
    if "--verbose" in opts.keys() or "-v" in opts.keys():
        verbose = True

    # Determine if debug output is requested.
    if "--debug" in opts.keys():
        debug = True

        # Enable debug output for OpenSSL-related functions.
        import cryptutil
        cryptutil.setdebug(True)

    # Ingredients for successful SIS generation:
    #
    # terminalenc   Terminal character encoding (autodetected)
    # filesystemenc File system name encoding (autodetected)
    # basename      Base for generated file names on host, filesystemenc encoded
    # srcdir        Directory of source files, filesystemenc encoded
    # srcfiles      List of filesystemenc encoded source file names in srcdir
    # outfile       Output SIS file name, filesystemenc encoded
    # uid3          Application UID3, long integer
    # appname       Application name and install directory in device, in Unicode
    # version       A triple-item tuple (major, minor, build)
    # lang          List of two-character language codes, ASCII strings
    # icon          Icon data, a binary string typically containing a SVG-T file
    # shortcaption  List of Unicode short captions, one per language
    # caption       List of Unicode long captions, one per language
    # drive         Installation drive letter or "!"
    # extrasdir     Path prefix for extra files, filesystemenc encoded or None
    # textfile      File name pattern of text file(s) to display during install
    # texts         Actual texts to display during install, one per language
    # cert          Certificate in PEM format
    # privkey       Certificate private key in PEM format
    # passphrase    Pass phrase of private key, terminalenc encoded string
    # caps, capmask Capability names and bitmask
    # vendor        List of Unicode vendor names, one per language
    # autostart     Boolean requesting application autostart on device boot
    # runinstall    Boolean requesting application autorun after installation
    # heapsizemin   Heap that must be available for the application to start
    # heapsizemax   Maximum amount of heap the application can allocate
    # verbose       Boolean indicating verbose terminal output

    if verbose:
        print
        print "Input file(s)       %s" % " ".join(
            [s.decode(filesystemenc).encode(terminalenc) for s in srcfiles])
        print "Output SIS file     %s" % (
            outfile.decode(filesystemenc).encode(terminalenc))
        print "UID                 0x%08x" % uid3
        print "Application name    %s" % appname.encode(terminalenc)
        print "Version             %d.%d.%d" % (version[0], version[1],
                                                version[2])
        print "Language(s)         %s" % ", ".join(lang)
        print "Icon                %s" % (
            (icon and icon.decode(filesystemenc).encode(terminalenc))
            or "<default>")
        print "Short caption(s)    %s" % ", ".join(
            [s.encode(terminalenc) for s in shortcaption])
        print "Long caption(s)     %s" % ", ".join(
            [s.encode(terminalenc) for s in caption])
        print "Install drive       %s" % ((drive == "!") and "<any>" or drive)
        print "Extras directory    %s" % (
            (extrasdir and extrasdir.decode(filesystemenc).encode(terminalenc))
            or "<none>")
        print "Text file(s)        %s" % (
            (textfile and textfile.decode(filesystemenc).encode(terminalenc))
            or "<none>")
        print "Sign?               %s" % (unsign and "no" or "yes")
        print "Certificate         %s" % (
            (cert and cert.decode(filesystemenc).encode(terminalenc))
            or "<default>")
        print "Private key         %s" % (
            (privkey and privkey.decode(filesystemenc).encode(terminalenc))
            or "<default>")
        print "Capabilities        0x%x (%s)" % (capmask, caps)
        print "Vendor name(s)      %s" % ", ".join(
            [s.encode(terminalenc) for s in vendor])
        print "Autostart on boot   %s" % ((autostart and "Yes") or "No")
        print "Run after install   %s" % ((runinstall and "Yes") or "No")
        print "Heap size in bytes  %d, %d" % (heapsizemin, heapsizemax)
        print

    # Generate SimpleSISWriter object.
    sw = sisfile.SimpleSISWriter(lang, caption, uid3, version, vendor[0],
                                 vendor)

    # Add text file or files to the SIS object. Text dialog is
    # supposed to be displayed before anything else is installed.
    if len(texts) == 1:
        sw.addfile(texts[0], operation=sisfield.EOpText)
    elif len(texts) > 1:
        sw.addlangdepfile(texts, operation=sisfield.EOpText)

    # Generate "Python for S60" resource file.
    rsctarget = u"%s:\\resource\\apps\\%s_0x%08x.rsc" % (drive, appname, uid3)
    string = zlib.decompress(pythons60rscdata.decode("base-64"))
    sw.addfile(string, rsctarget)
    del string

    # Generate application registration resource file.
    regtarget = u"%s:\\private\\10003a3f\\import\\apps\\%s_0x%08x_reg.rsc" % (
        drive, appname, uid3)
    exename = u"%s_0x%08x" % (appname, uid3)
    locpath = u"\\resource\\apps\\%s_0x%08x_loc" % (appname, uid3)
    rw = rscfile.RSCWriter(uid2=0x101f8021, uid3=uid3)
    # STRUCT APP_REGISTRATION_INFO from appinfo.rh
    res = rscfile.Resource([
        "LONG", "LLINK", "LTEXT", "LONG", "LTEXT", "LONG", "BYTE", "BYTE",
        "BYTE", "BYTE", "LTEXT", "BYTE", "WORD", "WORD", "WORD", "LLINK"
    ], 0, 0, exename, 0, locpath, 1, 0, 0, 0, 0, "", 0, 0, 0, 0, 0)
    rw.addresource(res)
    string = rw.tostring()
    del rw
    sw.addfile(string, regtarget)
    del string

    # EXE target name
    exetarget = u"%s:\\sys\\bin\\%s_0x%08x.exe" % (drive, appname, uid3)

    # Generate autostart registration resource file, if requested.
    if autostart:
        autotarget = u"%s:\\private\\101f875a\\import\\[%08x].rsc" % (drive,
                                                                      uid3)
        rw = rscfile.RSCWriter(uid2=0, offset="    ")
        # STRUCT STARTUP_ITEM_INFO from startupitem.rh
        res = rscfile.Resource(
            ["BYTE", "LTEXT", "WORD", "LONG", "BYTE", "BYTE"], 0, exetarget, 0,
            0, 0, 0)
        rw.addresource(res)
        string = rw.tostring()
        del rw
        sw.addfile(string, autotarget)
        del string

    # Generate localisable icon/caption definition resource files.
    iconpath = "\\resource\\apps\\%s_0x%08x_aif.mif" % (appname, uid3)
    for n in xrange(numlang):
        loctarget = u"%s:\\resource\\apps\\%s_0x%08x_loc.r%02d" % (
            drive, appname, uid3, symbianutil.langidtonum[lang[n]])
        rw = rscfile.RSCWriter(uid2=0, offset="    ")
        # STRUCT LOCALISABLE_APP_INFO from appinfo.rh
        res = rscfile.Resource([
            "LONG", "LLINK", "LTEXT", "LONG", "LLINK", "LTEXT", "WORD",
            "LTEXT", "WORD", "LTEXT"
        ], 0, 0, shortcaption[n], 0, 0, caption[n], 1, iconpath, 0, "")
        rw.addresource(res)
        string = rw.tostring()
        del rw
        sw.addfile(string, loctarget)
        del string

    # Generate MIF file for icon.
    icontarget = "%s:\\resource\\apps\\%s_0x%08x_aif.mif" % (drive, appname,
                                                             uid3)
    mw = miffile.MIFWriter()
    mw.addfile(icondata)
    del icondata
    string = mw.tostring()
    del mw
    sw.addfile(string, icontarget)
    del string

    # Add files to SIS object.
    if len(srcfiles) == 1:
        # Read file.
        f = file(os.path.join(srcdir, srcfiles[0]), "rb")
        string = f.read(MAXOTHERFILESIZE + 1)
        f.close()

        if len(string) > MAXOTHERFILESIZE:
            raise ValueError("%s: input file too large" % srcfiles[0])

        # Add file to the SIS object. One file only, rename it to default.py.
        target = "default.py"
        sw.addfile(string, "%s:\\private\\%08x\\%s" % (drive, uid3, target))
        del string
    else:
        if extrasdir != None:
            sysbinprefix = os.path.join(extrasdir, "sys", "bin", "")
        else:
            sysbinprefix = os.path.join(os.sep, "sys", "bin", "")

        # More than one file, use original path names.
        for srcfile in srcfiles:
            # Read file.
            f = file(os.path.join(srcdir, srcfile), "rb")
            string = f.read(MAXOTHERFILESIZE + 1)
            f.close()

            if len(string) > MAXOTHERFILESIZE:
                raise ValueError("%s: input file too large" % srcfile)

            # Split path into components.
            srcpathcomp = srcfile.split(os.sep)
            targetpathcomp = [s.decode(filesystemenc) for s in srcpathcomp]

            # Check if the file is an E32Image (EXE or DLL).
            filecapmask = symbianutil.e32imagecaps(string)

            # Warn against common mistakes when dealing with E32Image files.
            if filecapmask != None:
                if not srcfile.startswith(sysbinprefix):
                    # Warn against E32Image files outside /sys/bin.
                    print(
                        "%s: warning: %s is an E32Image (EXE or DLL) "
                        "outside %s" % (pgmname, srcfile, sysbinprefix))
                elif (symbianutil.ise32image(string) == "DLL"
                      and (filecapmask & ~capmask) != 0x00000000L):
                    # Warn about insufficient capabilities to load
                    # a DLL from the PyS60 application.
                    print(
                        "%s: warning: insufficient capabilities to "
                        "load %s" % (pgmname, srcfile))

            # Handle the extras directory.
            if extrasdir != None and extrasdir == srcpathcomp[0]:
                # Path is rooted at the drive root.
                targetfile = u"%s:\\%s" % (drive, "\\".join(
                    targetpathcomp[1:]))
            else:
                # Path is rooted at the application private directory.
                targetfile = u"%s:\\private\\%08x\\%s" % (
                    drive, uid3, "\\".join(targetpathcomp))

            # Add file to the SIS object.
            sw.addfile(string, targetfile, capabilities=filecapmask)
            del string

    # Add target device dependency.
    sw.addtargetdevice(0x101f7961L, (0, 0, 0), None,
                       ["Series60ProductID"] * numlang)

    # Add "Python for S60" dependency, version 1.4.0 onwards.
    # NOTE: Previous beta versions of Python for S60 had a
    # different UID3 (0xf0201510).
    sw.adddependency(0x2000b1a0L, (1, 4, 0), None,
                     ["Python for S60"] * numlang)

    # Add certificate.
    if certdata is not None:
        #print (privkeydata, certdata, passphrase)
        sw.addcertificate(privkeydata, certdata, passphrase)

    # Generate an EXE stub and add it to the SIS object.
    string = execstubdata.decode("base-64")
    string = symbianutil.e32imagecrc(string, uid3, uid3, None, heapsizemin,
                                     heapsizemax, capmask)
    if runinstall:
        # To avoid running without dependencies, this has to be in the end.
        sw.addfile(string,
                   exetarget,
                   None,
                   capabilities=capmask,
                   operation=sisfield.EOpRun,
                   options=sisfield.EInstFileRunOptionInstall)
    else:
        sw.addfile(string, exetarget, None, capabilities=capmask)

    del string

    # Generate SIS file out of the SimpleSISWriter object.
    sw.tofile(outfile)