src/tools/py2sis/ensymble/cmd_simplesis.py
changeset 0 ca70ae20a155
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/py2sis/ensymble/cmd_simplesis.py	Tue Feb 16 10:07:05 2010 +0530
@@ -0,0 +1,518 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+##############################################################################
+# cmd_simplesis.py - Ensymble command line tool, simplesis command
+# Copyright 2006, 2007, 2008, 2009 Jussi Ylänen
+#
+# This file is part of Ensymble developer utilities for Symbian OS(TM).
+#
+# Ensymble is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Ensymble is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ensymble; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+##############################################################################
+
+import sys
+import os
+import re
+import getopt
+import getpass
+import locale
+import struct
+import zlib
+
+import sisfile
+import sisfield
+import symbianutil
+import rscfile
+import miffile
+
+
+##############################################################################
+# Help texts
+##############################################################################
+
+shorthelp = 'Create a SIS package from a directory structure'
+longhelp  = '''simplesis
+    [--uid=0x01234567] [--version=1.0.0] [--lang=EN,...]
+    [--caption="Package Name",...] [--drive=C] [--textfile=mytext_%C.txt]
+    [--cert=mycert.cer] [--privkey=mykey.key] [--passphrase=12345]
+    [--vendor="Vendor Name",...] [--encoding=terminal,filesystem]
+    [--verbose]
+    <srcdir> [sisfile]
+
+Create a SIS package from a directory structure. Only supports very
+simple SIS files. There is no support for conditionally included
+files, dependencies etc.
+
+Options:
+    srcdir       - Source directory
+    sisfile      - Path of the created SIS file
+    uid          - Symbian OS UID for the SIS package
+    version      - SIS package version: X.Y.Z or X,Y,Z (major, minor, build)
+    lang         - Comma separated list of two-character language codes
+    caption      - Comma separated list of package names in all languages
+    drive        - Drive where the package will be installed (any by default)
+    textfile     - Text file (or pattern, see below) to display during install
+    cert         - Certificate to use for signing (PEM format)
+    privkey      - Private key of the certificate (PEM format)
+    passphrase   - Pass phrase of the private key (insecure, use stdin instead)
+    vendor       - Vendor name or a comma separated list of names in all lang.
+    encoding     - Local character encodings for terminal and filesystem
+    verbose      - Print extra statistics
+
+If no certificate and its private key are given, a default self-signed
+certificate is used to sign the SIS file. Software authors are encouraged
+to create their own unique certificates for SIS packages that are to be
+distributed.
+
+Text to display uses UTF-8 encoding. The file name may contain formatting
+characters that are substituted for each selected language. If no formatting
+characters are present, the same text will be used for all languages.
+
+    %%           - literal %
+    %n           - language number (01 - 99)
+    %c           - two-character language code in lowercase letters
+    %C           - two-character language code in capital letters
+    %l           - language name in English, using only lowercase letters
+    %l           - language name in English, using mixed case letters
+'''
+
+
+##############################################################################
+# Parameters
+##############################################################################
+
+MAXPASSPHRASELENGTH     = 256
+MAXCERTIFICATELENGTH    = 65536
+MAXPRIVATEKEYLENGTH     = 65536
+MAXFILESIZE             = 1024 * 1024 * 8   # Eight megabytes
+MAXTEXTFILELENGTH       = 1024
+
+
+##############################################################################
+# Global variables
+##############################################################################
+
+debug = False
+
+
+##############################################################################
+# Public module-level functions
+##############################################################################
+
+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:r:l:c:f:t:a:k:p:d:e:vh"
+    long_opts = [
+        "uid=", "version=", "lang=", "caption=",
+        "drive=", "textfile=", "cert=", "privkey=", "passphrase=", "vendor=",
+        "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 directory name.
+    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)
+    else:
+        raise ValueError("%s: not a directory" % src)
+
+    # Parse version string, use 1.0.0 by default.
+    version = opts.get("--version", opts.get("-r", None))
+    if version == None:
+        version = "1.0.0"
+        print ("%s: warning: no package 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")
+
+    # Auto-generate a test-range UID from basename.
+    autouid = symbianutil.uidfromname(basename.decode(filesystemenc))
+
+    # Get package UID.
+    puid = opts.get("--uid", opts.get("-u", None))
+    if puid == None:
+        # No UID given, use auto-generated UID.
+        puid = autouid
+        print ("%s: warning: no UID given, using auto-generated "
+               "test-range UID 0x%08x" % (pgmname, puid))
+    elif puid.lower().startswith("0x"):
+        # Prefer hex UIDs with leading "0x".
+        puid = long(puid, 16)
+    else:
+        try:
+            if len(puid) == 8:
+                # Assuming hex UID even without leading "0x".
+                print ('%s: warning: assuming hex UID even '
+                       'without leading "0x"' % pgmname)
+                puid = long(puid, 16)
+            else:
+                # Decimal UID.
+                puid = long(puid)
+                print ('%s: warning: decimal UID converted to 0x%08x' %
+                       (pgmname, puid))
+        except ValueError:
+            raise ValueError("invalid UID string '%s'" % puid)
+
+    # Warn against specifying a test-range UID manually.
+    if puid & 0xf0000000L == 0xe0000000L and puid != autouid:
+        print ("%s: warning: manually specifying a test-range UID is "
+               "not recommended" % pgmname)
+
+    # Determine package 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)
+
+    # Determine package caption(s), use basename by default.
+    caption = opts.get("--caption", opts.get("-c", ""))
+    caption = caption.decode(terminalenc)
+    if len(caption) == 0:
+        # Caption not given, use basename.
+        caption = [basename] * numlang
+    else:
+        caption = caption.split(",")
+
+    # Compare the number of languages and captions.
+    if 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")
+
+    # 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))
+    if 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.
+        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 puid < 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()
+
+    # 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
+    # puid          Package UID, long integer
+    # version       A triple-item tuple (major, minor, build)
+    # lang          List of two-character language codes, ASCII strings
+    # caption       List of Unicode package captions, one per language
+    # drive         Installation drive letter or "!"
+    # 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
+    # vendor        List of Unicode vendor names, one per language
+    # verbose       Boolean indicating verbose terminal output
+
+    if verbose:
+        print
+        print "Input files         %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"      % puid
+        print "Version             %d.%d.%d"    % (
+            version[0], version[1], version[2])
+        print "Language(s)         %s"          % ", ".join(lang)
+        print "Package caption(s)  %s"          % ", ".join(
+            [s.encode(terminalenc) for s in caption])
+        print "Install drive       %s"        % ((drive == "!") and
+            "<any>" or drive)
+        print "Text file(s)        %s"          % ((textfile and
+            textfile.decode(filesystemenc).encode(terminalenc)) or "<none>")
+        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 "Vendor name(s)      %s"          % ", ".join(
+            [s.encode(terminalenc) for s in vendor])
+        print
+
+    # Generate SimpleSISWriter object.
+    sw = sisfile.SimpleSISWriter(lang, caption, puid, 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)
+
+    # Add files to SIS object.
+    sysbinprefix = os.path.join("sys", "bin", "")
+    for srcfile in srcfiles:
+        # Read file.
+        f = file(os.path.join(srcdir, srcfile), "rb")
+        string = f.read(MAXFILESIZE + 1)
+        f.close()
+
+        if len(string) > MAXFILESIZE:
+            raise ValueError("input file too large")
+
+        # Check if the file is an E32Image (EXE or DLL).
+        caps = symbianutil.e32imagecaps(string)
+
+        if caps != None and not srcfile.startswith(sysbinprefix):
+            print ("%s: warning: %s is an E32Image (EXE or DLL) outside %s%s" %
+                    (pgmname, srcfile, os.sep, sysbinprefix))
+
+        # Add file to the SIS object.
+        target = srcfile.decode(filesystemenc).replace(os.sep, "\\")
+        sw.addfile(string, "%s:\\%s" % (drive, target), capabilities = caps)
+        del string
+
+    # Add target device dependency.
+    sw.addtargetdevice(0x101f7961L, (0, 0, 0), None,
+                       ["Series60ProductID"] * numlang)
+
+    # Add certificate.
+    sw.addcertificate(privkeydata, certdata, passphrase)
+
+    # Generate SIS file out of the SimpleSISWriter object.
+    sw.tofile(outfile)
+
+
+##############################################################################
+# Module-level functions which are normally only used by this module
+##############################################################################
+
+def parseversion(version):
+    '''Parse a version string: "v1.2.3" or similar.
+
+    Initial "v" can optionally be a capital "V" or omitted altogether. Minor
+    and build numbers can also be omitted. Separator can be a comma or a
+    period.'''
+
+    version = version.strip().lower()
+
+    # Strip initial "v" or "V".
+    if version[0] == "v":
+        version = version[1:]
+
+    if "." in version:
+        parts = [int(n) for n in version.split(".")]
+    else:
+        parts = [int(n) for n in version.split(",")]
+
+    # Allow missing minor and build numbers.
+    parts.extend([0, 0])
+
+    return parts[0:3]
+
+def readtextfiles(pattern, languages):
+    '''Read language dependent text files.
+
+    Files are assumed to be in UTF-8 encoding and re-encoded
+    in UCS-2 (UTF-16LE) for Symbian OS to display during installation.'''
+
+    if "%" not in pattern:
+        # Only one file, read it.
+        filenames = [pattern]
+    else:
+        filenames = []
+        for langid in languages:
+            langnum  = symbianutil.langidtonum[langid]
+            langname = symbianutil.langnumtoname[langnum]
+
+            # Replace formatting characters in file name pattern.
+            filename = pattern
+            filename = filename.replace("%n", "%02d" % langnum)
+            filename = filename.replace("%c", langid.lower())
+            filename = filename.replace("%C", langid.upper())
+            filename = filename.replace("%l", langname.lower())
+            filename = filename.replace("%L", langname)
+            filename = filename.replace("%%", "%")
+
+            filenames.append(filename)
+
+    texts = []
+
+    for filename in filenames:
+        f = file(filename, "r") # Read as text.
+        text = f.read(MAXTEXTFILELENGTH + 1)
+        f.close()
+
+        if len(text) > MAXTEXTFILELENGTH:
+            raise ValueError("%s: text file too large" % filename)
+
+        texts.append(text.decode("UTF-8").encode("UTF-16LE"))
+
+    return texts