msilib.py

Go to the documentation of this file.
00001 # Microsoft Installer Library
00002 # (C) 2003 Martin v. Loewis
00003 
00004 import win32com.client.gencache
00005 import win32com.client
00006 import pythoncom
00007 from win32com.client import constants
00008 import re, string, os, sets, glob, popen2, sys
00009 
00010 Win64 = 0
00011 
00012 # Partially taken from Wine
00013 datasizemask=      0x00ff
00014 type_valid=        0x0100
00015 type_localizable=  0x0200
00016 
00017 typemask=          0x0c00
00018 type_long=         0x0000
00019 type_short=        0x0400
00020 type_string=       0x0c00
00021 type_binary=       0x0800
00022 
00023 type_nullable=     0x1000
00024 type_key=          0x2000
00025 # XXX temporary, localizable?
00026 knownbits = datasizemask | type_valid | type_localizable | \
00027             typemask | type_nullable | type_key
00028 
00029 # Summary Info Property IDs
00030 PID_CODEPAGE=1
00031 PID_TITLE=2
00032 PID_SUBJECT=3
00033 PID_AUTHOR=4
00034 PID_KEYWORDS=5
00035 PID_COMMENTS=6
00036 PID_TEMPLATE=7
00037 PID_LASTAUTHOR=8
00038 PID_REVNUMBER=9
00039 PID_LASTPRINTED=11
00040 PID_CREATE_DTM=12
00041 PID_LASTSAVE_DTM=13
00042 PID_PAGECOUNT=14
00043 PID_WORDCOUNT=15
00044 PID_CHARCOUNT=16
00045 PID_APPNAME=18
00046 PID_SECURITY=19
00047 
00048 def reset():
00049     global _directories
00050     _directories = sets.Set()
00051 
00052 def EnsureMSI():
00053     win32com.client.gencache.EnsureModule('{000C1092-0000-0000-C000-000000000046}',0x409,1,0)
00054 
00055 _Installer=None
00056 def MakeInstaller():
00057     global _Installer
00058     if _Installer is None:
00059         EnsureMSI()
00060         _Installer = win32com.client.Dispatch('WindowsInstaller.Installer',
00061                      resultCLSID='{000C1090-0000-0000-C000-000000000046}')
00062     return _Installer
00063 
00064 class Table:
00065     def __init__(self, name):
00066         self.name = name
00067         self.fields = []
00068 
00069     def add_field(self, index, name, type):
00070         self.fields.append((index,name,type))
00071 
00072     def sql(self):
00073         fields = []
00074         keys = []
00075         self.fields.sort()
00076         fields = [None]*len(self.fields)
00077         for index, name, type in self.fields:
00078             index -= 1
00079             unk = type & ~knownbits
00080             if unk:
00081                 print "%s.%s unknown bits %x" % (self.name, name, unk)
00082             size = type & datasizemask
00083             dtype = type & typemask
00084             if dtype == type_string:
00085                 if size:
00086                     tname="CHAR(%d)" % size
00087                 else:
00088                     tname="CHAR"
00089             elif dtype == type_short:
00090                 assert size==2
00091                 tname = "SHORT"
00092             elif dtype == type_long:
00093                 assert size==4
00094                 tname="LONG"
00095             elif dtype == type_binary:
00096                 assert size==0
00097                 tname="OBJECT"
00098             else:
00099                 tname="unknown"
00100                 print "%s.%sunknown integer type %d" % (self.name, name, size)
00101             if type & type_nullable:
00102                 flags = ""
00103             else:
00104                 flags = " NOT NULL"
00105             if type & type_localizable:
00106                 flags += " LOCALIZABLE"
00107             fields[index] = "`%s` %s%s" % (name, tname, flags)
00108             if type & type_key:
00109                 keys.append("`%s`" % name)
00110         fields = ", ".join(fields)
00111         keys = ", ".join(keys)
00112         return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self.name, fields, keys)
00113 
00114     def create(self, db):
00115         v = db.OpenView(self.sql())
00116         v.Execute(None)
00117         v.Close()
00118 
00119 class Binary:
00120     def __init__(self, fname):
00121         self.name = fname
00122     def __repr__(self):
00123         return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
00124 
00125 def gen_schema(destpath, schemapath):
00126     d = MakeInstaller()
00127     schema = d.OpenDatabase(schemapath,
00128             win32com.client.constants.msiOpenDatabaseModeReadOnly)
00129 
00130     # XXX ORBER BY
00131     v=schema.OpenView("SELECT * FROM _Columns")
00132     curtable=None
00133     tables = []
00134     v.Execute(None)
00135     f = open(destpath, "wt")
00136     f.write("from msilib import Table\n")
00137     while 1:
00138         r=v.Fetch()
00139         if not r:break
00140         name=r.StringData(1)
00141         if curtable != name:
00142             f.write("\n%s = Table('%s')\n" % (name,name))
00143             curtable = name
00144             tables.append(name)
00145         f.write("%s.add_field(%d,'%s',%d)\n" %
00146                 (name, r.IntegerData(2), r.StringData(3), r.IntegerData(4)))
00147     v.Close()
00148 
00149     f.write("\ntables=[%s]\n\n" % (", ".join(tables)))
00150 
00151     # Fill the _Validation table
00152     f.write("_Validation_records = [\n")
00153     v = schema.OpenView("SELECT * FROM _Validation")
00154     v.Execute(None)
00155     while 1:
00156         r = v.Fetch()
00157         if not r:break
00158         # Table, Column, Nullable
00159         f.write("(%s,%s,%s," %
00160                 (`r.StringData(1)`, `r.StringData(2)`, `r.StringData(3)`))
00161         def put_int(i):
00162             if r.IsNull(i):f.write("None, ")
00163             else:f.write("%d," % r.IntegerData(i))
00164         def put_str(i):
00165             if r.IsNull(i):f.write("None, ")
00166             else:f.write("%s," % `r.StringData(i)`)
00167         put_int(4) # MinValue
00168         put_int(5) # MaxValue
00169         put_str(6) # KeyTable
00170         put_int(7) # KeyColumn
00171         put_str(8) # Category
00172         put_str(9) # Set
00173         put_str(10)# Description
00174         f.write("),\n")
00175     f.write("]\n\n")
00176 
00177     f.close()    
00178 
00179 def gen_sequence(destpath, msipath):
00180     dir = os.path.dirname(destpath)
00181     d = MakeInstaller()
00182     seqmsi = d.OpenDatabase(msipath,
00183             win32com.client.constants.msiOpenDatabaseModeReadOnly)
00184 
00185     v = seqmsi.OpenView("SELECT * FROM _Tables");
00186     v.Execute(None)
00187     f = open(destpath, "w")
00188     print >>f, "import msilib,os;dirname=os.path.dirname(__file__)"
00189     tables = []
00190     while 1:
00191         r = v.Fetch()
00192         if not r:break
00193         table = r.StringData(1)
00194         tables.append(table)
00195         f.write("%s = [\n" % table)
00196         v1 = seqmsi.OpenView("SELECT * FROM `%s`" % table)
00197         v1.Execute(None)
00198         info = v1.ColumnInfo(constants.msiColumnInfoTypes)
00199         while 1:
00200             r = v1.Fetch()
00201             if not r:break
00202             rec = []
00203             for i in range(1,r.FieldCount+1):
00204                 if r.IsNull(i):
00205                     rec.append(None)
00206                 elif info.StringData(i)[0] in "iI":
00207                     rec.append(r.IntegerData(i))
00208                 elif info.StringData(i)[0] in "slSL":
00209                     rec.append(r.StringData(i))
00210                 elif info.StringData(i)[0]=="v":
00211                     size = r.DataSize(i)
00212                     bytes = r.ReadStream(i, size, constants.msiReadStreamBytes)
00213                     bytes = bytes.encode("latin-1") # binary data represented "as-is"
00214                     if table == "Binary":
00215                         fname = rec[0]+".bin"
00216                         open(os.path.join(dir,fname),"wb").write(bytes)
00217                         rec.append(Binary(fname))
00218                     else:
00219                         rec.append(bytes)
00220                 else:
00221                     raise "Unsupported column type", info.StringData(i)
00222             f.write(repr(tuple(rec))+",\n")
00223         v1.Close()
00224         f.write("]\n\n")
00225     v.Close()
00226     f.write("tables=%s\n" % repr(map(str,tables)))
00227     f.close()
00228 
00229 def add_data(db, table, values):
00230     print '(add_data).1 :: db=(%s)' % (str(db))
00231     d = MakeInstaller()
00232     v = db.OpenView("SELECT * FROM `%s`" % table)
00233     count = v.ColumnInfo(0).FieldCount
00234     r = d.CreateRecord(count)
00235     for value in values:
00236         assert len(value) == count, value
00237         for i in range(count):
00238             field = value[i]
00239             if isinstance(field, (int, long)):
00240                 r.SetIntegerData(i+1,field)
00241             elif isinstance(field, basestring):
00242                 r.SetStringData(i+1,field)
00243             elif field is None:
00244                 pass
00245             elif isinstance(field, Binary):
00246                 r.SetStream(i+1, field.name)
00247             else:
00248                 raise TypeError, "Unsupported type %s" % field.__class__.__name__
00249         v.Modify(win32com.client.constants.msiViewModifyInsert, r)
00250         r.ClearData()
00251     v.Close()
00252 
00253 def add_stream(db, name, path):
00254     d = MakeInstaller()
00255     v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name)
00256     r = d.CreateRecord(1)
00257     r.SetStream(1, path)
00258     v.Execute(r)
00259     v.Close()
00260 
00261 def init_database(name, schema,
00262                   ProductName, ProductCode, ProductVersion,
00263                   Manufacturer):
00264     try:
00265         os.unlink(name)
00266     except OSError:
00267         pass
00268     ProductCode = ProductCode.upper()
00269     d = MakeInstaller()
00270     # Create the database
00271     db = d.OpenDatabase(name,
00272          win32com.client.constants.msiOpenDatabaseModeCreate)
00273     # Create the tables
00274     for t in schema.tables:
00275         t.create(db)
00276     # Fill the validation table
00277     add_data(db, "_Validation", schema._Validation_records)
00278     # Initialize the summary information, allowing atmost 20 properties
00279     si = db.GetSummaryInformation(20)
00280     si.SetProperty(PID_TITLE, "Installation Database")
00281     si.SetProperty(PID_SUBJECT, ProductName)
00282     si.SetProperty(PID_AUTHOR, Manufacturer)
00283     si.SetProperty(PID_TEMPLATE, "Intel;1033")
00284     si.SetProperty(PID_REVNUMBER, ProductCode) # XXX should be package code
00285     si.SetProperty(PID_WORDCOUNT, 2) # long file names, compressed, original media
00286     si.SetProperty(PID_PAGECOUNT, 200)
00287     si.SetProperty(PID_APPNAME, "Python MSI Library")
00288     # XXX more properties
00289     si.Persist()
00290     add_data(db, "Property", [
00291         ("ProductName", ProductName),
00292         ("ProductCode", ProductCode),
00293         ("ProductVersion", ProductVersion),
00294         ("Manufacturer", Manufacturer),
00295         ("ProductLanguage", "1033")])
00296     db.Commit()
00297     return db
00298 
00299 def add_tables(db, module):
00300     for table in module.tables:
00301         add_data(db, table, getattr(module, table))
00302 
00303 def make_id(str):
00304     str = str.replace(".", "_") # colons are allowed
00305     str = str.replace(" ", "_")
00306     str = str.replace("-", "_")
00307     if str[0] in string.digits:
00308         str = "_"+str
00309     assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
00310     return str
00311 
00312 def gen_uuid():
00313     return str(pythoncom.CreateGuid())
00314 
00315 class CAB:
00316     def __init__(self, name):
00317         self.name = name
00318         self.file = open(name+".txt", "wt")
00319         self.filenames = sets.Set()
00320         self.index = 0
00321 
00322     def gen_id(self, dir, file):
00323         logical = _logical = make_id(file)
00324         pos = 1
00325         while logical in self.filenames:
00326             logical = "%s.%d" % (_logical, pos)
00327             pos += 1
00328         self.filenames.add(logical)
00329         return logical
00330 
00331     def append(self, full, file, logical = None):
00332         if os.path.isdir(full):
00333             return
00334         if not logical:
00335             logical = self.gen_id(dir, file)
00336         self.index += 1
00337         if full.find(" ")!=-1:
00338             print >>self.file, '"%s" %s' % (full, logical)
00339         else:
00340             print >>self.file, '%s %s' % (full, logical)
00341         return self.index, logical
00342 
00343     def commit(self, db):
00344         self.file.close()
00345         try:
00346             os.unlink(self.name+".cab")
00347         except OSError:
00348             pass
00349         f = popen2.popen4(r"cabarc.exe n %s.cab @%s.txt" % (self.name, self.name))[0]
00350         for line in f:
00351             if line.startswith("  -- adding "):
00352                 sys.stdout.write(".")
00353             else:
00354                 sys.stdout.write(line)
00355             sys.stdout.flush()
00356         if not os.path.exists(self.name+".cab"):
00357             raise IOError, "cabarc failed"
00358         add_data(db, "Media", [(1, self.index, None, "#"+self.name, None, None)])
00359         add_stream(db, self.name, self.name+".cab")
00360         os.unlink(self.name+".txt")
00361         os.unlink(self.name+".cab")
00362         db.Commit()
00363 
00364 _directories = sets.Set()
00365 class Directory:
00366     def __init__(self, db, cab, basedir, physical, _logical, default):
00367         index = 1
00368         _logical = make_id(_logical)
00369         logical = _logical
00370         while logical in _directories:
00371             logical = "%s%d" % (_logical, index)
00372             index += 1
00373         _directories.add(logical)
00374         self.db = db
00375         self.cab = cab
00376         self.basedir = basedir
00377         self.physical = physical
00378         self.logical = logical
00379         self.component = None
00380         self.short_names = sets.Set()
00381         self.ids = sets.Set()
00382         self.keyfiles = {}
00383         if basedir:
00384             self.absolute = os.path.join(basedir.absolute, physical)
00385             blogical = basedir.logical
00386         else:
00387             self.absolute = physical
00388             blogical = None
00389         add_data(db, "Directory", [(logical, blogical, default)])
00390 
00391     def start_component(self, component, feature = None, keyfile = None):
00392         uuid = gen_uuid()
00393         self.component = component
00394         if Win64:
00395             flags = 256
00396         else:
00397             flags = 0
00398         if keyfile:
00399             keyid = self.cab.gen_id(self.absolute, keyfile)
00400             self.keyfiles[keyfile] = keyid
00401         else:
00402             keyid = None
00403         add_data(self.db, "Component", [(component, uuid, self.logical, flags, None, keyid)])
00404         if feature is None:
00405             feature = current_feature
00406         add_data(self.db, "FeatureComponents", [(feature.id, component)])
00407 
00408     def make_short(self, file):
00409         parts = file.split(".")
00410         if len(parts)>1:
00411             suffix = parts[-1].upper()
00412         else:
00413             suffix = None
00414         prefix = parts[0].upper()
00415         if len(prefix) <= 8 and (not suffix or len(suffix)<=3):
00416             if suffix:
00417                 file = prefix+"."+suffix
00418             else:
00419                 file = prefix
00420             try:
00421                 assert file not in self.short_names
00422             except Exception, details:
00423                 print '(make_short) :: ERROR "%s" because file (%s) is not unique.' % (str(details),file)
00424         else:
00425             prefix = prefix[:6]
00426             if suffix:
00427                 suffix = suffix[:3]
00428             pos = 1
00429             while 1:
00430                 if suffix:
00431                     file = "%s~%d.%s" % (prefix, pos, suffix)
00432                 else:
00433                     file = "%s~%d" % (prefix, pos)
00434                 if file not in self.short_names: break
00435                 pos += 1
00436                 assert pos < 10000
00437                 if pos in (10, 100, 1000):
00438                     prefix = prefix[:-1]
00439         self.short_names.add(file)
00440         print '(make_short) :: re.search() :: file=(%s)' % (str(file))
00441         assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
00442         return file
00443 
00444     def add_file(self, file, src=None):
00445         if not self.component:
00446             self.start_component(self.logical, current_feature)
00447         if not src:
00448             # Allow relative paths for file if src is not specified
00449             src = file
00450             file = os.path.basename(file)
00451         absolute = os.path.join(self.absolute, src)
00452         assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
00453         if self.keyfiles.has_key(file):
00454             logical = self.keyfiles[file]
00455         else:
00456             logical = None
00457         sequence, logical = self.cab.append(absolute, file, logical)
00458         assert logical not in self.ids
00459         self.ids.add(logical)
00460         short = self.make_short(file)
00461         full = "%s|%s" % (short, file)
00462         filesize = os.stat(absolute).st_size
00463         version = None
00464         language = None
00465         # constants.msidbFileAttributesVital
00466         # Compressed omitted, since it is the database default
00467         # could add r/o, system, hidden
00468         attributes = 512 
00469         add_data(self.db, "File",
00470                         [(logical, self.component, full, filesize, version,
00471                          language, attributes, sequence)])
00472         # Automatically remove .pyc/.pyo files on uninstall (2)
00473         # XXX: adding so many RemoveFile entries makes installer unbelievably
00474         # slow. So instead, we have to use wildcard remove entries
00475         # if file.endswith(".py"):
00476         #     add_data(self.db, "RemoveFile",
00477         #              [(logical+"c", self.component, "%sC|%sc" % (short, file),
00478         #                self.logical, 2),
00479         #               (logical+"o", self.component, "%sO|%so" % (short, file),
00480         #                self.logical, 2)])
00481 
00482     def glob(self, pattern):
00483         files = glob.glob1(self.absolute, pattern)
00484         for f in files:
00485             self.add_file(f)
00486         return files
00487 
00488     def remove_pyc(self):
00489         "Remove .pyc/.pyo files on uninstall"
00490         add_data(self.db, "RemoveFile",
00491                  [(self.component+"c", self.component, "*.pyc", self.logical, 2),
00492                   (self.component+"o", self.component, "*.pyo", self.logical, 2)])
00493 
00494 class Feature:
00495     def __init__(self, db, id, title, desc, display, level = 1,
00496                  parent=None, directory = None):
00497         self.id = id
00498         attributes = 0
00499         if parent:
00500             attributes |= 2 # follow parent
00501         add_data(db, "Feature",
00502                         [(id, parent, title, desc, display,
00503                           level, directory, attributes)])
00504     def set_current(self):
00505         global current_feature
00506         current_feature = self
00507 
00508 class Control:
00509     def __init__(self, dlg, name):
00510         self.dlg = dlg
00511         self.name = name
00512 
00513     def event(self, ev, arg, cond = "1", order = None):
00514         add_data(self.dlg.db, "ControlEvent",
00515                  [(self.dlg.name, self.name, ev, arg, cond, order)])
00516 
00517     def mapping(self, ev, attr):
00518         add_data(self.dlg.db, "EventMapping",
00519                  [(self.dlg.name, self.name, ev, attr)])
00520 
00521 class RadioButtonGroup(Control):
00522     def __init__(self, dlg, name, property):
00523         self.dlg = dlg
00524         self.name = name
00525         self.property = property
00526         self.index = 1
00527 
00528     def add(self, name, x, y, w, h, text):
00529         add_data(self.dlg.db, "RadioButton",
00530                  [(self.property, self.index, name,
00531                    x, y, w, h, text, None)])
00532         self.index += 1
00533 
00534 class Dialog:
00535     def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
00536         self.db = db
00537         self.name = name
00538         self.x, self.y, self.w, self.h = x,y,w,h
00539         add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
00540 
00541     def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
00542         add_data(self.db, "Control",
00543                  [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
00544         return Control(self, name)
00545 
00546     def text(self, name, x, y, w, h, attr, text):
00547         return self.control(name, "Text", x, y, w, h, attr, None,
00548                      text, None, None)
00549 
00550     def bitmap(self, name, x, y, w, h, text):
00551         return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
00552         
00553     def line(self, name, x, y, w, h):
00554         return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
00555 
00556     def pushbutton(self, name, x, y, w, h, attr, text, next):
00557         return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
00558 
00559     def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
00560         add_data(self.db, "Control",
00561                  [(self.name, name, "RadioButtonGroup",
00562                    x, y, w, h, attr, prop, text, next, None)])
00563         return RadioButtonGroup(self, name, prop)
00564 
00565 

© Copyright 2008-2009 Vyper Logix Corp., All Right Reserved; If you reference this document or any part of this document you must use the citation verbatim (including the link) "© Copyright 2008-2009 Vyper Logix Corp., All Right Reserved."

Notice: This source code contained in this document is NOT open source and is NOT being distributed as open source.

122,241 lines of code and growing...