src/tools/py2sis/ensymble/cmd_simplesis.py
changeset 0 ca70ae20a155
equal deleted inserted replaced
-1:000000000000 0:ca70ae20a155
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 
       
     4 ##############################################################################
       
     5 # cmd_simplesis.py - Ensymble command line tool, simplesis command
       
     6 # Copyright 2006, 2007, 2008, 2009 Jussi Ylänen
       
     7 #
       
     8 # This file is part of Ensymble developer utilities for Symbian OS(TM).
       
     9 #
       
    10 # Ensymble is free software; you can redistribute it and/or modify
       
    11 # it under the terms of the GNU General Public License as published by
       
    12 # the Free Software Foundation; either version 2 of the License, or
       
    13 # (at your option) any later version.
       
    14 #
       
    15 # Ensymble is distributed in the hope that it will be useful,
       
    16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    18 # GNU General Public License for more details.
       
    19 #
       
    20 # You should have received a copy of the GNU General Public License
       
    21 # along with Ensymble; if not, write to the Free Software
       
    22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
       
    23 ##############################################################################
       
    24 
       
    25 import sys
       
    26 import os
       
    27 import re
       
    28 import getopt
       
    29 import getpass
       
    30 import locale
       
    31 import struct
       
    32 import zlib
       
    33 
       
    34 import sisfile
       
    35 import sisfield
       
    36 import symbianutil
       
    37 import rscfile
       
    38 import miffile
       
    39 
       
    40 
       
    41 ##############################################################################
       
    42 # Help texts
       
    43 ##############################################################################
       
    44 
       
    45 shorthelp = 'Create a SIS package from a directory structure'
       
    46 longhelp  = '''simplesis
       
    47     [--uid=0x01234567] [--version=1.0.0] [--lang=EN,...]
       
    48     [--caption="Package Name",...] [--drive=C] [--textfile=mytext_%C.txt]
       
    49     [--cert=mycert.cer] [--privkey=mykey.key] [--passphrase=12345]
       
    50     [--vendor="Vendor Name",...] [--encoding=terminal,filesystem]
       
    51     [--verbose]
       
    52     <srcdir> [sisfile]
       
    53 
       
    54 Create a SIS package from a directory structure. Only supports very
       
    55 simple SIS files. There is no support for conditionally included
       
    56 files, dependencies etc.
       
    57 
       
    58 Options:
       
    59     srcdir       - Source directory
       
    60     sisfile      - Path of the created SIS file
       
    61     uid          - Symbian OS UID for the SIS package
       
    62     version      - SIS package version: X.Y.Z or X,Y,Z (major, minor, build)
       
    63     lang         - Comma separated list of two-character language codes
       
    64     caption      - Comma separated list of package names in all languages
       
    65     drive        - Drive where the package will be installed (any by default)
       
    66     textfile     - Text file (or pattern, see below) to display during install
       
    67     cert         - Certificate to use for signing (PEM format)
       
    68     privkey      - Private key of the certificate (PEM format)
       
    69     passphrase   - Pass phrase of the private key (insecure, use stdin instead)
       
    70     vendor       - Vendor name or a comma separated list of names in all lang.
       
    71     encoding     - Local character encodings for terminal and filesystem
       
    72     verbose      - Print extra statistics
       
    73 
       
    74 If no certificate and its private key are given, a default self-signed
       
    75 certificate is used to sign the SIS file. Software authors are encouraged
       
    76 to create their own unique certificates for SIS packages that are to be
       
    77 distributed.
       
    78 
       
    79 Text to display uses UTF-8 encoding. The file name may contain formatting
       
    80 characters that are substituted for each selected language. If no formatting
       
    81 characters are present, the same text will be used for all languages.
       
    82 
       
    83     %%           - literal %
       
    84     %n           - language number (01 - 99)
       
    85     %c           - two-character language code in lowercase letters
       
    86     %C           - two-character language code in capital letters
       
    87     %l           - language name in English, using only lowercase letters
       
    88     %l           - language name in English, using mixed case letters
       
    89 '''
       
    90 
       
    91 
       
    92 ##############################################################################
       
    93 # Parameters
       
    94 ##############################################################################
       
    95 
       
    96 MAXPASSPHRASELENGTH     = 256
       
    97 MAXCERTIFICATELENGTH    = 65536
       
    98 MAXPRIVATEKEYLENGTH     = 65536
       
    99 MAXFILESIZE             = 1024 * 1024 * 8   # Eight megabytes
       
   100 MAXTEXTFILELENGTH       = 1024
       
   101 
       
   102 
       
   103 ##############################################################################
       
   104 # Global variables
       
   105 ##############################################################################
       
   106 
       
   107 debug = False
       
   108 
       
   109 
       
   110 ##############################################################################
       
   111 # Public module-level functions
       
   112 ##############################################################################
       
   113 
       
   114 def run(pgmname, argv):
       
   115     global debug
       
   116 
       
   117     # Determine system character encodings.
       
   118     try:
       
   119         # getdefaultlocale() may sometimes return None.
       
   120         # Fall back to ASCII encoding in that case.
       
   121         terminalenc = locale.getdefaultlocale()[1] + ""
       
   122     except TypeError:
       
   123         # Invalid locale, fall back to ASCII terminal encoding.
       
   124         terminalenc = "ascii"
       
   125 
       
   126     try:
       
   127         # sys.getfilesystemencoding() was introduced in Python v2.3 and
       
   128         # it can sometimes return None. Fall back to ASCII if something
       
   129         # goes wrong.
       
   130         filesystemenc = sys.getfilesystemencoding() + ""
       
   131     except (AttributeError, TypeError):
       
   132         filesystemenc = "ascii"
       
   133 
       
   134     try:
       
   135         gopt = getopt.gnu_getopt
       
   136     except:
       
   137         # Python <v2.3, GNU-style parameter ordering not supported.
       
   138         gopt = getopt.getopt
       
   139 
       
   140     # Parse command line arguments.
       
   141     short_opts = "u:r:l:c:f:t:a:k:p:d:e:vh"
       
   142     long_opts = [
       
   143         "uid=", "version=", "lang=", "caption=",
       
   144         "drive=", "textfile=", "cert=", "privkey=", "passphrase=", "vendor=",
       
   145         "encoding=", "verbose", "debug", "help"
       
   146     ]
       
   147     args = gopt(argv, short_opts, long_opts)
       
   148 
       
   149     opts = dict(args[0])
       
   150     pargs = args[1]
       
   151 
       
   152     if len(pargs) == 0:
       
   153         raise ValueError("no source file name given")
       
   154 
       
   155     # Override character encoding of command line and filesystem.
       
   156     encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc,
       
   157                                                             filesystemenc)))
       
   158     try:
       
   159         terminalenc, filesystemenc = encs.split(",")
       
   160     except (ValueError, TypeError):
       
   161         raise ValueError("invalid encoding string '%s'" % encs)
       
   162 
       
   163     # Get source directory name.
       
   164     src = pargs[0].decode(terminalenc).encode(filesystemenc)
       
   165     if os.path.isdir(src):
       
   166         # Remove trailing slashes (or whatever the separator is).
       
   167         src = os.path.split(src + os.sep)[0]
       
   168 
       
   169         # Use last directory component as the name.
       
   170         basename = os.path.basename(src)
       
   171 
       
   172         # Source is a directory, recursively collect files it contains.
       
   173         srcdir = src
       
   174         srcfiles = []
       
   175         prefixlen = len(srcdir) + len(os.sep)
       
   176         def getfiles(arg, dirname, names):
       
   177             for name in names:
       
   178                 path = os.path.join(dirname, name)
       
   179                 if not os.path.isdir(path):
       
   180                     arg.append(path[prefixlen:])
       
   181         os.path.walk(srcdir, getfiles, srcfiles)
       
   182     else:
       
   183         raise ValueError("%s: not a directory" % src)
       
   184 
       
   185     # Parse version string, use 1.0.0 by default.
       
   186     version = opts.get("--version", opts.get("-r", None))
       
   187     if version == None:
       
   188         version = "1.0.0"
       
   189         print ("%s: warning: no package version given, "
       
   190                "using %s" % (pgmname, version))
       
   191     try:
       
   192         version = parseversion(version)
       
   193     except (ValueError, IndexError, TypeError):
       
   194         raise ValueError("invalid version string '%s'" % version)
       
   195 
       
   196     # Determine output SIS file name.
       
   197     if len(pargs) == 1:
       
   198         # Derive output file name from input file name.
       
   199         outfile = "%s_v%d_%d_%d.sis" % (basename, version[0],
       
   200                                         version[1], version[2])
       
   201     elif len(pargs) == 2:
       
   202         outfile = pargs[1].decode(terminalenc).encode(filesystemenc)
       
   203         if os.path.isdir(outfile):
       
   204             # Output to directory, derive output name from input file name.
       
   205             outfile = os.path.join(outfile, "%s_v%d_%d_%d.sis" % (
       
   206                 basename, version[0], version[1], version[2]))
       
   207         if not outfile.lower().endswith(".sis"):
       
   208             outfile += ".sis"
       
   209     else:
       
   210         raise ValueError("wrong number of arguments")
       
   211 
       
   212     # Auto-generate a test-range UID from basename.
       
   213     autouid = symbianutil.uidfromname(basename.decode(filesystemenc))
       
   214 
       
   215     # Get package UID.
       
   216     puid = opts.get("--uid", opts.get("-u", None))
       
   217     if puid == None:
       
   218         # No UID given, use auto-generated UID.
       
   219         puid = autouid
       
   220         print ("%s: warning: no UID given, using auto-generated "
       
   221                "test-range UID 0x%08x" % (pgmname, puid))
       
   222     elif puid.lower().startswith("0x"):
       
   223         # Prefer hex UIDs with leading "0x".
       
   224         puid = long(puid, 16)
       
   225     else:
       
   226         try:
       
   227             if len(puid) == 8:
       
   228                 # Assuming hex UID even without leading "0x".
       
   229                 print ('%s: warning: assuming hex UID even '
       
   230                        'without leading "0x"' % pgmname)
       
   231                 puid = long(puid, 16)
       
   232             else:
       
   233                 # Decimal UID.
       
   234                 puid = long(puid)
       
   235                 print ('%s: warning: decimal UID converted to 0x%08x' %
       
   236                        (pgmname, puid))
       
   237         except ValueError:
       
   238             raise ValueError("invalid UID string '%s'" % puid)
       
   239 
       
   240     # Warn against specifying a test-range UID manually.
       
   241     if puid & 0xf0000000L == 0xe0000000L and puid != autouid:
       
   242         print ("%s: warning: manually specifying a test-range UID is "
       
   243                "not recommended" % pgmname)
       
   244 
       
   245     # Determine package language(s), use "EN" by default.
       
   246     lang = opts.get("--lang", opts.get("-l", "EN")).split(",")
       
   247     numlang = len(lang)
       
   248 
       
   249     # Verify that the language codes are correct.
       
   250     for l in lang:
       
   251         try:
       
   252             symbianutil.langidtonum[l]
       
   253         except KeyError:
       
   254             raise ValueError("%s: no such language code" % l)
       
   255 
       
   256     # Determine package caption(s), use basename by default.
       
   257     caption = opts.get("--caption", opts.get("-c", ""))
       
   258     caption = caption.decode(terminalenc)
       
   259     if len(caption) == 0:
       
   260         # Caption not given, use basename.
       
   261         caption = [basename] * numlang
       
   262     else:
       
   263         caption = caption.split(",")
       
   264 
       
   265     # Compare the number of languages and captions.
       
   266     if len(caption) != numlang:
       
   267         raise ValueError("invalid number of captions")
       
   268 
       
   269     # Determine installation drive, any by default.
       
   270     drive = opts.get("--drive", opts.get("-f", "any")).upper()
       
   271     if drive == "ANY" or drive == "!":
       
   272         drive = "!"
       
   273     elif drive != "C" and drive != "E":
       
   274         raise ValueError("%s: invalid drive letter" % drive)
       
   275 
       
   276     # Determine vendor name(s), use "Ensymble" by default.
       
   277     vendor = opts.get("--vendor", opts.get("-d", "Ensymble"))
       
   278     vendor = vendor.decode(terminalenc)
       
   279     vendor = vendor.split(",")
       
   280     if len(vendor) == 1:
       
   281         # Only one vendor name given, use it for all languages.
       
   282         vendor = vendor * numlang
       
   283     elif len(vendor) != numlang:
       
   284         raise ValueError("invalid number of vendor names")
       
   285 
       
   286     # Load text files.
       
   287     texts = []
       
   288     textfile = opts.get("--textfile", opts.get("-t", None))
       
   289     if textfile != None:
       
   290         texts = readtextfiles(textfile, lang)
       
   291 
       
   292     # Get certificate and its private key file names.
       
   293     cert = opts.get("--cert", opts.get("-a", None))
       
   294     privkey = opts.get("--privkey", opts.get("-k", None))
       
   295     if cert != None and privkey != None:
       
   296         # Convert file names from terminal encoding to filesystem encoding.
       
   297         cert = cert.decode(terminalenc).encode(filesystemenc)
       
   298         privkey = privkey.decode(terminalenc).encode(filesystemenc)
       
   299 
       
   300         # Read certificate file.
       
   301         f = file(cert, "rb")
       
   302         certdata = f.read(MAXCERTIFICATELENGTH + 1)
       
   303         f.close()
       
   304 
       
   305         if len(certdata) > MAXCERTIFICATELENGTH:
       
   306             raise ValueError("certificate file too large")
       
   307 
       
   308         # Read private key file.
       
   309         f = file(privkey, "rb")
       
   310         privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1)
       
   311         f.close()
       
   312 
       
   313         if len(privkeydata) > MAXPRIVATEKEYLENGTH:
       
   314             raise ValueError("private key file too large")
       
   315     elif cert == None and privkey == None:
       
   316         # No certificate given, use the Ensymble default certificate.
       
   317         # defaultcert.py is not imported when not needed. This speeds
       
   318         # up program start-up a little.
       
   319         import defaultcert
       
   320         certdata = defaultcert.cert
       
   321         privkeydata = defaultcert.privkey
       
   322 
       
   323         print ("%s: warning: no certificate given, using "
       
   324                "insecure built-in one" % pgmname)
       
   325 
       
   326         # Warn if the UID is in the protected range.
       
   327         # Resulting SIS file will probably not install.
       
   328         if puid < 0x80000000L:
       
   329             print ("%s: warning: UID is in the protected range "
       
   330                    "(0x00000000 - 0x7ffffff)" % pgmname)
       
   331     else:
       
   332         raise ValueError("missing certificate or private key")
       
   333 
       
   334     # Get pass phrase. Pass phrase remains in terminal encoding.
       
   335     passphrase = opts.get("--passphrase", opts.get("-p", None))
       
   336     if passphrase == None and privkey != None:
       
   337         # Private key given without "--passphrase" option, ask it.
       
   338         if sys.stdin.isatty():
       
   339             # Standard input is a TTY, ask password interactively.
       
   340             passphrase = getpass.getpass("Enter private key pass phrase:")
       
   341         else:
       
   342             # Not connected to a TTY, read stdin non-interactively instead.
       
   343             passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1)
       
   344 
       
   345             if len(passphrase) > MAXPASSPHRASELENGTH:
       
   346                 raise ValueError("pass phrase too long")
       
   347 
       
   348             passphrase = passphrase.strip()
       
   349 
       
   350     # Determine verbosity.
       
   351     verbose = False
       
   352     if "--verbose" in opts.keys() or "-v" in opts.keys():
       
   353         verbose = True
       
   354 
       
   355     # Determine if debug output is requested.
       
   356     if "--debug" in opts.keys():
       
   357         debug = True
       
   358 
       
   359         # Enable debug output for OpenSSL-related functions.
       
   360         import cryptutil
       
   361         cryptutil.setdebug(True)
       
   362 
       
   363     # Ingredients for successful SIS generation:
       
   364     #
       
   365     # terminalenc   Terminal character encoding (autodetected)
       
   366     # filesystemenc File system name encoding (autodetected)
       
   367     # basename      Base for generated file names on host, filesystemenc encoded
       
   368     # srcdir        Directory of source files, filesystemenc encoded
       
   369     # srcfiles      List of filesystemenc encoded source file names in srcdir
       
   370     # outfile       Output SIS file name, filesystemenc encoded
       
   371     # puid          Package UID, long integer
       
   372     # version       A triple-item tuple (major, minor, build)
       
   373     # lang          List of two-character language codes, ASCII strings
       
   374     # caption       List of Unicode package captions, one per language
       
   375     # drive         Installation drive letter or "!"
       
   376     # textfile      File name pattern of text file(s) to display during install
       
   377     # texts         Actual texts to display during install, one per language
       
   378     # cert          Certificate in PEM format
       
   379     # privkey       Certificate private key in PEM format
       
   380     # passphrase    Pass phrase of private key, terminalenc encoded string
       
   381     # vendor        List of Unicode vendor names, one per language
       
   382     # verbose       Boolean indicating verbose terminal output
       
   383 
       
   384     if verbose:
       
   385         print
       
   386         print "Input files         %s"          % " ".join(
       
   387             [s.decode(filesystemenc).encode(terminalenc) for s in srcfiles])
       
   388         print "Output SIS file     %s"          % (
       
   389             outfile.decode(filesystemenc).encode(terminalenc))
       
   390         print "UID                 0x%08x"      % puid
       
   391         print "Version             %d.%d.%d"    % (
       
   392             version[0], version[1], version[2])
       
   393         print "Language(s)         %s"          % ", ".join(lang)
       
   394         print "Package caption(s)  %s"          % ", ".join(
       
   395             [s.encode(terminalenc) for s in caption])
       
   396         print "Install drive       %s"        % ((drive == "!") and
       
   397             "<any>" or drive)
       
   398         print "Text file(s)        %s"          % ((textfile and
       
   399             textfile.decode(filesystemenc).encode(terminalenc)) or "<none>")
       
   400         print "Certificate         %s"          % ((cert and
       
   401             cert.decode(filesystemenc).encode(terminalenc)) or "<default>")
       
   402         print "Private key         %s"          % ((privkey and
       
   403             privkey.decode(filesystemenc).encode(terminalenc)) or "<default>")
       
   404         print "Vendor name(s)      %s"          % ", ".join(
       
   405             [s.encode(terminalenc) for s in vendor])
       
   406         print
       
   407 
       
   408     # Generate SimpleSISWriter object.
       
   409     sw = sisfile.SimpleSISWriter(lang, caption, puid, version,
       
   410                                  vendor[0], vendor)
       
   411 
       
   412     # Add text file or files to the SIS object. Text dialog is
       
   413     # supposed to be displayed before anything else is installed.
       
   414     if len(texts) == 1:
       
   415         sw.addfile(texts[0], operation = sisfield.EOpText)
       
   416     elif len(texts) > 1:
       
   417         sw.addlangdepfile(texts, operation = sisfield.EOpText)
       
   418 
       
   419     # Add files to SIS object.
       
   420     sysbinprefix = os.path.join("sys", "bin", "")
       
   421     for srcfile in srcfiles:
       
   422         # Read file.
       
   423         f = file(os.path.join(srcdir, srcfile), "rb")
       
   424         string = f.read(MAXFILESIZE + 1)
       
   425         f.close()
       
   426 
       
   427         if len(string) > MAXFILESIZE:
       
   428             raise ValueError("input file too large")
       
   429 
       
   430         # Check if the file is an E32Image (EXE or DLL).
       
   431         caps = symbianutil.e32imagecaps(string)
       
   432 
       
   433         if caps != None and not srcfile.startswith(sysbinprefix):
       
   434             print ("%s: warning: %s is an E32Image (EXE or DLL) outside %s%s" %
       
   435                     (pgmname, srcfile, os.sep, sysbinprefix))
       
   436 
       
   437         # Add file to the SIS object.
       
   438         target = srcfile.decode(filesystemenc).replace(os.sep, "\\")
       
   439         sw.addfile(string, "%s:\\%s" % (drive, target), capabilities = caps)
       
   440         del string
       
   441 
       
   442     # Add target device dependency.
       
   443     sw.addtargetdevice(0x101f7961L, (0, 0, 0), None,
       
   444                        ["Series60ProductID"] * numlang)
       
   445 
       
   446     # Add certificate.
       
   447     sw.addcertificate(privkeydata, certdata, passphrase)
       
   448 
       
   449     # Generate SIS file out of the SimpleSISWriter object.
       
   450     sw.tofile(outfile)
       
   451 
       
   452 
       
   453 ##############################################################################
       
   454 # Module-level functions which are normally only used by this module
       
   455 ##############################################################################
       
   456 
       
   457 def parseversion(version):
       
   458     '''Parse a version string: "v1.2.3" or similar.
       
   459 
       
   460     Initial "v" can optionally be a capital "V" or omitted altogether. Minor
       
   461     and build numbers can also be omitted. Separator can be a comma or a
       
   462     period.'''
       
   463 
       
   464     version = version.strip().lower()
       
   465 
       
   466     # Strip initial "v" or "V".
       
   467     if version[0] == "v":
       
   468         version = version[1:]
       
   469 
       
   470     if "." in version:
       
   471         parts = [int(n) for n in version.split(".")]
       
   472     else:
       
   473         parts = [int(n) for n in version.split(",")]
       
   474 
       
   475     # Allow missing minor and build numbers.
       
   476     parts.extend([0, 0])
       
   477 
       
   478     return parts[0:3]
       
   479 
       
   480 def readtextfiles(pattern, languages):
       
   481     '''Read language dependent text files.
       
   482 
       
   483     Files are assumed to be in UTF-8 encoding and re-encoded
       
   484     in UCS-2 (UTF-16LE) for Symbian OS to display during installation.'''
       
   485 
       
   486     if "%" not in pattern:
       
   487         # Only one file, read it.
       
   488         filenames = [pattern]
       
   489     else:
       
   490         filenames = []
       
   491         for langid in languages:
       
   492             langnum  = symbianutil.langidtonum[langid]
       
   493             langname = symbianutil.langnumtoname[langnum]
       
   494 
       
   495             # Replace formatting characters in file name pattern.
       
   496             filename = pattern
       
   497             filename = filename.replace("%n", "%02d" % langnum)
       
   498             filename = filename.replace("%c", langid.lower())
       
   499             filename = filename.replace("%C", langid.upper())
       
   500             filename = filename.replace("%l", langname.lower())
       
   501             filename = filename.replace("%L", langname)
       
   502             filename = filename.replace("%%", "%")
       
   503 
       
   504             filenames.append(filename)
       
   505 
       
   506     texts = []
       
   507 
       
   508     for filename in filenames:
       
   509         f = file(filename, "r") # Read as text.
       
   510         text = f.read(MAXTEXTFILELENGTH + 1)
       
   511         f.close()
       
   512 
       
   513         if len(text) > MAXTEXTFILELENGTH:
       
   514             raise ValueError("%s: text file too large" % filename)
       
   515 
       
   516         texts.append(text.decode("UTF-8").encode("UTF-16LE"))
       
   517 
       
   518     return texts