| # Microsoft Installer Library |
| # (C) 2003 Martin v. Loewis |
| |
| import win32com.client.gencache |
| import win32com.client |
| import pythoncom, pywintypes |
| from win32com.client import constants |
| import re, string, os, sets, glob, subprocess, sys, _winreg, struct, _msi |
| |
| try: |
| basestring |
| except NameError: |
| basestring = (str, unicode) |
| |
| # Partially taken from Wine |
| datasizemask= 0x00ff |
| type_valid= 0x0100 |
| type_localizable= 0x0200 |
| |
| typemask= 0x0c00 |
| type_long= 0x0000 |
| type_short= 0x0400 |
| type_string= 0x0c00 |
| type_binary= 0x0800 |
| |
| type_nullable= 0x1000 |
| type_key= 0x2000 |
| # XXX temporary, localizable? |
| knownbits = datasizemask | type_valid | type_localizable | \ |
| typemask | type_nullable | type_key |
| |
| # Summary Info Property IDs |
| PID_CODEPAGE=1 |
| PID_TITLE=2 |
| PID_SUBJECT=3 |
| PID_AUTHOR=4 |
| PID_KEYWORDS=5 |
| PID_COMMENTS=6 |
| PID_TEMPLATE=7 |
| PID_LASTAUTHOR=8 |
| PID_REVNUMBER=9 |
| PID_LASTPRINTED=11 |
| PID_CREATE_DTM=12 |
| PID_LASTSAVE_DTM=13 |
| PID_PAGECOUNT=14 |
| PID_WORDCOUNT=15 |
| PID_CHARCOUNT=16 |
| PID_APPNAME=18 |
| PID_SECURITY=19 |
| |
| def reset(): |
| global _directories |
| _directories = sets.Set() |
| |
| def EnsureMSI(): |
| win32com.client.gencache.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0) |
| |
| def EnsureMSM(): |
| try: |
| win32com.client.gencache.EnsureModule('{0ADDA82F-2C26-11D2-AD65-00A0C9AF11A6}', 0, 1, 0) |
| except pywintypes.com_error: |
| win32com.client.gencache.EnsureModule('{0ADDA82F-2C26-11D2-AD65-00A0C9AF11A6}', 0, 2, 0) |
| |
| _Installer=None |
| def MakeInstaller(): |
| global _Installer |
| if _Installer is None: |
| EnsureMSI() |
| _Installer = win32com.client.Dispatch('WindowsInstaller.Installer', |
| resultCLSID='{000C1090-0000-0000-C000-000000000046}') |
| return _Installer |
| |
| _Merge=None |
| def MakeMerge2(): |
| global _Merge |
| if _Merge is None: |
| EnsureMSM() |
| _Merge = win32com.client.Dispatch("Msm.Merge2.1") |
| return _Merge |
| |
| class Table: |
| def __init__(self, name): |
| self.name = name |
| self.fields = [] |
| |
| def add_field(self, index, name, type): |
| self.fields.append((index,name,type)) |
| |
| def sql(self): |
| fields = [] |
| keys = [] |
| self.fields.sort() |
| fields = [None]*len(self.fields) |
| for index, name, type in self.fields: |
| index -= 1 |
| unk = type & ~knownbits |
| if unk: |
| print "%s.%s unknown bits %x" % (self.name, name, unk) |
| size = type & datasizemask |
| dtype = type & typemask |
| if dtype == type_string: |
| if size: |
| tname="CHAR(%d)" % size |
| else: |
| tname="CHAR" |
| elif dtype == type_short: |
| assert size==2 |
| tname = "SHORT" |
| elif dtype == type_long: |
| assert size==4 |
| tname="LONG" |
| elif dtype == type_binary: |
| assert size==0 |
| tname="OBJECT" |
| else: |
| tname="unknown" |
| print "%s.%sunknown integer type %d" % (self.name, name, size) |
| if type & type_nullable: |
| flags = "" |
| else: |
| flags = " NOT NULL" |
| if type & type_localizable: |
| flags += " LOCALIZABLE" |
| fields[index] = "`%s` %s%s" % (name, tname, flags) |
| if type & type_key: |
| keys.append("`%s`" % name) |
| fields = ", ".join(fields) |
| keys = ", ".join(keys) |
| return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self.name, fields, keys) |
| |
| def create(self, db): |
| v = db.OpenView(self.sql()) |
| v.Execute(None) |
| v.Close() |
| |
| class Binary: |
| def __init__(self, fname): |
| self.name = fname |
| def __repr__(self): |
| return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name |
| |
| def gen_schema(destpath, schemapath): |
| d = MakeInstaller() |
| schema = d.OpenDatabase(schemapath, |
| win32com.client.constants.msiOpenDatabaseModeReadOnly) |
| |
| # XXX ORBER BY |
| v=schema.OpenView("SELECT * FROM _Columns") |
| curtable=None |
| tables = [] |
| v.Execute(None) |
| f = open(destpath, "wt") |
| f.write("from msilib import Table\n") |
| while 1: |
| r=v.Fetch() |
| if not r:break |
| name=r.StringData(1) |
| if curtable != name: |
| f.write("\n%s = Table('%s')\n" % (name,name)) |
| curtable = name |
| tables.append(name) |
| f.write("%s.add_field(%d,'%s',%d)\n" % |
| (name, r.IntegerData(2), r.StringData(3), r.IntegerData(4))) |
| v.Close() |
| |
| f.write("\ntables=[%s]\n\n" % (", ".join(tables))) |
| |
| # Fill the _Validation table |
| f.write("_Validation_records = [\n") |
| v = schema.OpenView("SELECT * FROM _Validation") |
| v.Execute(None) |
| while 1: |
| r = v.Fetch() |
| if not r:break |
| # Table, Column, Nullable |
| f.write("(%s,%s,%s," % |
| (`r.StringData(1)`, `r.StringData(2)`, `r.StringData(3)`)) |
| def put_int(i): |
| if r.IsNull(i):f.write("None, ") |
| else:f.write("%d," % r.IntegerData(i)) |
| def put_str(i): |
| if r.IsNull(i):f.write("None, ") |
| else:f.write("%s," % `r.StringData(i)`) |
| put_int(4) # MinValue |
| put_int(5) # MaxValue |
| put_str(6) # KeyTable |
| put_int(7) # KeyColumn |
| put_str(8) # Category |
| put_str(9) # Set |
| put_str(10)# Description |
| f.write("),\n") |
| f.write("]\n\n") |
| |
| f.close() |
| |
| def gen_sequence(destpath, msipath): |
| dir = os.path.dirname(destpath) |
| d = MakeInstaller() |
| seqmsi = d.OpenDatabase(msipath, |
| win32com.client.constants.msiOpenDatabaseModeReadOnly) |
| |
| v = seqmsi.OpenView("SELECT * FROM _Tables"); |
| v.Execute(None) |
| f = open(destpath, "w") |
| print >>f, "import msilib,os;dirname=os.path.dirname(__file__)" |
| tables = [] |
| while 1: |
| r = v.Fetch() |
| if not r:break |
| table = r.StringData(1) |
| tables.append(table) |
| f.write("%s = [\n" % table) |
| v1 = seqmsi.OpenView("SELECT * FROM `%s`" % table) |
| v1.Execute(None) |
| info = v1.ColumnInfo(constants.msiColumnInfoTypes) |
| while 1: |
| r = v1.Fetch() |
| if not r:break |
| rec = [] |
| for i in range(1,r.FieldCount+1): |
| if r.IsNull(i): |
| rec.append(None) |
| elif info.StringData(i)[0] in "iI": |
| rec.append(r.IntegerData(i)) |
| elif info.StringData(i)[0] in "slSL": |
| rec.append(r.StringData(i)) |
| elif info.StringData(i)[0]=="v": |
| size = r.DataSize(i) |
| bytes = r.ReadStream(i, size, constants.msiReadStreamBytes) |
| bytes = bytes.encode("latin-1") # binary data represented "as-is" |
| if table == "Binary": |
| fname = rec[0]+".bin" |
| open(os.path.join(dir,fname),"wb").write(bytes) |
| rec.append(Binary(fname)) |
| else: |
| rec.append(bytes) |
| else: |
| raise "Unsupported column type", info.StringData(i) |
| f.write(repr(tuple(rec))+",\n") |
| v1.Close() |
| f.write("]\n\n") |
| v.Close() |
| f.write("tables=%s\n" % repr(map(str,tables))) |
| f.close() |
| |
| class _Unspecified:pass |
| def change_sequence(seq, action, seqno=_Unspecified, cond = _Unspecified): |
| "Change the sequence number of an action in a sequence list" |
| for i in range(len(seq)): |
| if seq[i][0] == action: |
| if cond is _Unspecified: |
| cond = seq[i][1] |
| if seqno is _Unspecified: |
| seqno = seq[i][2] |
| seq[i] = (action, cond, seqno) |
| return |
| raise ValueError, "Action not found in sequence" |
| |
| def add_data(db, table, values): |
| d = MakeInstaller() |
| v = db.OpenView("SELECT * FROM `%s`" % table) |
| count = v.ColumnInfo(0).FieldCount |
| r = d.CreateRecord(count) |
| for value in values: |
| assert len(value) == count, value |
| for i in range(count): |
| field = value[i] |
| if isinstance(field, (int, long)): |
| r.SetIntegerData(i+1,field) |
| elif isinstance(field, basestring): |
| r.SetStringData(i+1,field) |
| elif field is None: |
| pass |
| elif isinstance(field, Binary): |
| r.SetStream(i+1, field.name) |
| else: |
| raise TypeError, "Unsupported type %s" % field.__class__.__name__ |
| v.Modify(win32com.client.constants.msiViewModifyInsert, r) |
| r.ClearData() |
| v.Close() |
| |
| def add_stream(db, name, path): |
| d = MakeInstaller() |
| v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name) |
| r = d.CreateRecord(1) |
| r.SetStream(1, path) |
| v.Execute(r) |
| v.Close() |
| |
| def init_database(name, schema, |
| ProductName, ProductCode, ProductVersion, |
| Manufacturer, |
| request_uac = False): |
| try: |
| os.unlink(name) |
| except OSError: |
| pass |
| ProductCode = ProductCode.upper() |
| d = MakeInstaller() |
| # Create the database |
| db = d.OpenDatabase(name, |
| win32com.client.constants.msiOpenDatabaseModeCreate) |
| # Create the tables |
| for t in schema.tables: |
| t.create(db) |
| # Fill the validation table |
| add_data(db, "_Validation", schema._Validation_records) |
| # Initialize the summary information, allowing atmost 20 properties |
| si = db.GetSummaryInformation(20) |
| si.SetProperty(PID_TITLE, "Installation Database") |
| si.SetProperty(PID_SUBJECT, ProductName) |
| si.SetProperty(PID_AUTHOR, Manufacturer) |
| si.SetProperty(PID_TEMPLATE, msi_type) |
| si.SetProperty(PID_REVNUMBER, gen_uuid()) |
| if request_uac: |
| wc = 2 # long file names, compressed, original media |
| else: |
| wc = 2 | 8 # +never invoke UAC |
| si.SetProperty(PID_WORDCOUNT, wc) |
| si.SetProperty(PID_PAGECOUNT, 200) |
| si.SetProperty(PID_APPNAME, "Python MSI Library") |
| # XXX more properties |
| si.Persist() |
| add_data(db, "Property", [ |
| ("ProductName", ProductName), |
| ("ProductCode", ProductCode), |
| ("ProductVersion", ProductVersion), |
| ("Manufacturer", Manufacturer), |
| ("ProductLanguage", "1033")]) |
| db.Commit() |
| return db |
| |
| def add_tables(db, module): |
| for table in module.tables: |
| add_data(db, table, getattr(module, table)) |
| |
| def make_id(str): |
| #str = str.replace(".", "_") # colons are allowed |
| str = str.replace(" ", "_") |
| str = str.replace("-", "_") |
| str = str.replace("+", "_") |
| if str[0] in string.digits: |
| str = "_"+str |
| assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str |
| return str |
| |
| def gen_uuid(): |
| return str(pythoncom.CreateGuid()) |
| |
| class CAB: |
| def __init__(self, name): |
| self.name = name |
| self.files = [] |
| self.filenames = sets.Set() |
| self.index = 0 |
| |
| def gen_id(self, dir, file): |
| logical = _logical = make_id(file) |
| pos = 1 |
| while logical in self.filenames: |
| logical = "%s.%d" % (_logical, pos) |
| pos += 1 |
| self.filenames.add(logical) |
| return logical |
| |
| def append(self, full, file, logical = None): |
| if os.path.isdir(full): |
| return |
| if not logical: |
| logical = self.gen_id(dir, file) |
| self.index += 1 |
| self.files.append((full, logical)) |
| return self.index, logical |
| |
| def commit(self, db): |
| try: |
| os.unlink(self.name+".cab") |
| except OSError: |
| pass |
| _msi.FCICreate(self.name+".cab", self.files) |
| add_data(db, "Media", |
| [(1, self.index, None, "#"+self.name, None, None)]) |
| add_stream(db, self.name, self.name+".cab") |
| os.unlink(self.name+".cab") |
| db.Commit() |
| |
| _directories = sets.Set() |
| class Directory: |
| def __init__(self, db, cab, basedir, physical, _logical, default, componentflags=None): |
| """Create a new directory in the Directory table. There is a current component |
| at each point in time for the directory, which is either explicitly created |
| through start_component, or implicitly when files are added for the first |
| time. Files are added into the current component, and into the cab file. |
| To create a directory, a base directory object needs to be specified (can be |
| None), the path to the physical directory, and a logical directory name. |
| Default specifies the DefaultDir slot in the directory table. componentflags |
| specifies the default flags that new components get.""" |
| index = 1 |
| _logical = make_id(_logical) |
| logical = _logical |
| while logical in _directories: |
| logical = "%s%d" % (_logical, index) |
| index += 1 |
| _directories.add(logical) |
| self.db = db |
| self.cab = cab |
| self.basedir = basedir |
| self.physical = physical |
| self.logical = logical |
| self.component = None |
| self.short_names = sets.Set() |
| self.ids = sets.Set() |
| self.keyfiles = {} |
| self.componentflags = componentflags |
| if basedir: |
| self.absolute = os.path.join(basedir.absolute, physical) |
| blogical = basedir.logical |
| else: |
| self.absolute = physical |
| blogical = None |
| # initially assume that all files in this directory are unpackaged |
| # as files from self.absolute get added, this set is reduced |
| self.unpackaged_files = set() |
| for f in os.listdir(self.absolute): |
| if os.path.isfile(os.path.join(self.absolute, f)): |
| self.unpackaged_files.add(f) |
| add_data(db, "Directory", [(logical, blogical, default)]) |
| |
| def start_component(self, component = None, feature = None, flags = None, keyfile = None, uuid=None): |
| """Add an entry to the Component table, and make this component the current for this |
| directory. If no component name is given, the directory name is used. If no feature |
| is given, the current feature is used. If no flags are given, the directory's default |
| flags are used. If no keyfile is given, the KeyPath is left null in the Component |
| table.""" |
| if flags is None: |
| flags = self.componentflags |
| if uuid is None: |
| uuid = gen_uuid() |
| else: |
| uuid = uuid.upper() |
| if component is None: |
| component = self.logical |
| self.component = component |
| if Win64: |
| flags |= 256 |
| if keyfile: |
| keyid = self.cab.gen_id(self.absolute, keyfile) |
| self.keyfiles[keyfile] = keyid |
| else: |
| keyid = None |
| add_data(self.db, "Component", |
| [(component, uuid, self.logical, flags, None, keyid)]) |
| if feature is None: |
| feature = current_feature |
| add_data(self.db, "FeatureComponents", |
| [(feature.id, component)]) |
| |
| def make_short(self, file): |
| file = re.sub(r'[\?|><:/*"+,;=\[\]]', '_', file) # restrictions on short names |
| parts = file.split(".") |
| if len(parts)>1: |
| suffix = parts[-1].upper() |
| else: |
| suffix = None |
| prefix = parts[0].upper() |
| if len(prefix) <= 8 and (not suffix or len(suffix)<=3): |
| if suffix: |
| file = prefix+"."+suffix |
| else: |
| file = prefix |
| assert file not in self.short_names |
| else: |
| prefix = prefix[:6] |
| if suffix: |
| suffix = suffix[:3] |
| pos = 1 |
| while 1: |
| if suffix: |
| file = "%s~%d.%s" % (prefix, pos, suffix) |
| else: |
| file = "%s~%d" % (prefix, pos) |
| if file not in self.short_names: break |
| pos += 1 |
| assert pos < 10000 |
| if pos in (10, 100, 1000): |
| prefix = prefix[:-1] |
| self.short_names.add(file) |
| return file |
| |
| def add_file(self, file, src=None, version=None, language=None): |
| """Add a file to the current component of the directory, starting a new one |
| one if there is no current component. By default, the file name in the source |
| and the file table will be identical. If the src file is specified, it is |
| interpreted relative to the current directory. Optionally, a version and a |
| language can be specified for the entry in the File table.""" |
| if not self.component: |
| self.start_component(self.logical, current_feature) |
| if not src: |
| # Allow relative paths for file if src is not specified |
| src = file |
| file = os.path.basename(file) |
| absolute = os.path.join(self.absolute, src) |
| if absolute.startswith(self.absolute): |
| # mark file as packaged |
| relative = absolute[len(self.absolute)+1:] |
| if relative in self.unpackaged_files: |
| self.unpackaged_files.remove(relative) |
| assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names |
| if self.keyfiles.has_key(file): |
| logical = self.keyfiles[file] |
| else: |
| logical = None |
| sequence, logical = self.cab.append(absolute, file, logical) |
| assert logical not in self.ids |
| self.ids.add(logical) |
| short = self.make_short(file) |
| full = "%s|%s" % (short, file) |
| filesize = os.stat(absolute).st_size |
| # constants.msidbFileAttributesVital |
| # Compressed omitted, since it is the database default |
| # could add r/o, system, hidden |
| attributes = 512 |
| add_data(self.db, "File", |
| [(logical, self.component, full, filesize, version, |
| language, attributes, sequence)]) |
| if not version: |
| # Add hash if the file is not versioned |
| filehash = MakeInstaller().FileHash(absolute, 0) |
| add_data(self.db, "MsiFileHash", |
| [(logical, 0, filehash.IntegerData(1), |
| filehash.IntegerData(2), filehash.IntegerData(3), |
| filehash.IntegerData(4))]) |
| # Automatically remove .pyc/.pyo files on uninstall (2) |
| # XXX: adding so many RemoveFile entries makes installer unbelievably |
| # slow. So instead, we have to use wildcard remove entries |
| # if file.endswith(".py"): |
| # add_data(self.db, "RemoveFile", |
| # [(logical+"c", self.component, "%sC|%sc" % (short, file), |
| # self.logical, 2), |
| # (logical+"o", self.component, "%sO|%so" % (short, file), |
| # self.logical, 2)]) |
| |
| def glob(self, pattern, exclude = None): |
| """Add a list of files to the current component as specified in the |
| glob pattern. Individual files can be excluded in the exclude list.""" |
| files = glob.glob1(self.absolute, pattern) |
| for f in files: |
| if exclude and f in exclude: continue |
| self.add_file(f) |
| return files |
| |
| def remove_pyc(self): |
| "Remove .pyc/.pyo files from __pycache__ on uninstall" |
| directory = self.logical + "_pycache" |
| add_data(self.db, "Directory", [(directory, self.logical, "__PYCA~1|__pycache__")]) |
| flags = 256 if Win64 else 0 |
| add_data(self.db, "Component", |
| [(directory, gen_uuid(), directory, flags, None, None)]) |
| add_data(self.db, "FeatureComponents", [(current_feature.id, directory)]) |
| add_data(self.db, "CreateFolder", [(directory, directory)]) |
| add_data(self.db, "RemoveFile", |
| [(self.component, self.component, "*.*", directory, 2), |
| ]) |
| |
| def removefile(self, key, pattern): |
| "Add a RemoveFile entry" |
| add_data(self.db, "RemoveFile", [(self.component+key, self.component, pattern, self.logical, 2)]) |
| |
| |
| class Feature: |
| def __init__(self, db, id, title, desc, display, level = 1, |
| parent=None, directory = None, attributes=0): |
| self.id = id |
| if parent: |
| parent = parent.id |
| add_data(db, "Feature", |
| [(id, parent, title, desc, display, |
| level, directory, attributes)]) |
| def set_current(self): |
| global current_feature |
| current_feature = self |
| |
| class Control: |
| def __init__(self, dlg, name): |
| self.dlg = dlg |
| self.name = name |
| |
| def event(self, ev, arg, cond = "1", order = None): |
| add_data(self.dlg.db, "ControlEvent", |
| [(self.dlg.name, self.name, ev, arg, cond, order)]) |
| |
| def mapping(self, ev, attr): |
| add_data(self.dlg.db, "EventMapping", |
| [(self.dlg.name, self.name, ev, attr)]) |
| |
| def condition(self, action, condition): |
| add_data(self.dlg.db, "ControlCondition", |
| [(self.dlg.name, self.name, action, condition)]) |
| |
| class RadioButtonGroup(Control): |
| def __init__(self, dlg, name, property): |
| self.dlg = dlg |
| self.name = name |
| self.property = property |
| self.index = 1 |
| |
| def add(self, name, x, y, w, h, text, value = None): |
| if value is None: |
| value = name |
| add_data(self.dlg.db, "RadioButton", |
| [(self.property, self.index, value, |
| x, y, w, h, text, None)]) |
| self.index += 1 |
| |
| class Dialog: |
| def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel): |
| self.db = db |
| self.name = name |
| self.x, self.y, self.w, self.h = x,y,w,h |
| add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)]) |
| |
| def control(self, name, type, x, y, w, h, attr, prop, text, next, help): |
| add_data(self.db, "Control", |
| [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)]) |
| return Control(self, name) |
| |
| def text(self, name, x, y, w, h, attr, text): |
| return self.control(name, "Text", x, y, w, h, attr, None, |
| text, None, None) |
| |
| def bitmap(self, name, x, y, w, h, text): |
| return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None) |
| |
| def line(self, name, x, y, w, h): |
| return self.control(name, "Line", x, y, w, h, 1, None, None, None, None) |
| |
| def pushbutton(self, name, x, y, w, h, attr, text, next): |
| return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None) |
| |
| def radiogroup(self, name, x, y, w, h, attr, prop, text, next): |
| add_data(self.db, "Control", |
| [(self.name, name, "RadioButtonGroup", |
| x, y, w, h, attr, prop, text, next, None)]) |
| return RadioButtonGroup(self, name, prop) |
| |
| def checkbox(self, name, x, y, w, h, attr, prop, text, next): |
| return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None) |
| |
| def pe_type(path): |
| header = open(path, "rb").read(1000) |
| # offset of PE header is at offset 0x3c |
| pe_offset = struct.unpack("<i", header[0x3c:0x40])[0] |
| assert header[pe_offset:pe_offset+4] == "PE\0\0" |
| machine = struct.unpack("<H", header[pe_offset+4:pe_offset+6])[0] |
| return machine |
| |
| def set_arch_from_file(path): |
| global msi_type, Win64, arch_ext |
| machine = pe_type(path) |
| if machine == 0x14c: |
| # i386 |
| msi_type = "Intel" |
| Win64 = 0 |
| arch_ext = '' |
| elif machine == 0x200: |
| # Itanium |
| msi_type = "Intel64" |
| Win64 = 1 |
| arch_ext = '.ia64' |
| elif machine == 0x8664: |
| # AMD64 |
| msi_type = "x64" |
| Win64 = 1 |
| arch_ext = '.amd64' |
| else: |
| raise ValueError, "Unsupported architecture" |
| msi_type += ";1033" |