blob: 4595246880c1963b5e169ca4bcdd0e49a771f55f [file] [log] [blame]
Martin v. Löwis8ffe9ab2004-08-22 13:34:34 +00001# Microsoft Installer Library
2# (C) 2003 Martin v. Loewis
3
4import win32com.client.gencache
5import win32com.client
6import pythoncom, pywintypes
7from win32com.client import constants
8import re, string, os, sets, glob, popen2, sys, _winreg
9
10Win64 = 0
11
12# Partially taken from Wine
13datasizemask= 0x00ff
14type_valid= 0x0100
15type_localizable= 0x0200
16
17typemask= 0x0c00
18type_long= 0x0000
19type_short= 0x0400
20type_string= 0x0c00
21type_binary= 0x0800
22
23type_nullable= 0x1000
24type_key= 0x2000
25# XXX temporary, localizable?
26knownbits = datasizemask | type_valid | type_localizable | \
27 typemask | type_nullable | type_key
28
29# Summary Info Property IDs
30PID_CODEPAGE=1
31PID_TITLE=2
32PID_SUBJECT=3
33PID_AUTHOR=4
34PID_KEYWORDS=5
35PID_COMMENTS=6
36PID_TEMPLATE=7
37PID_LASTAUTHOR=8
38PID_REVNUMBER=9
39PID_LASTPRINTED=11
40PID_CREATE_DTM=12
41PID_LASTSAVE_DTM=13
42PID_PAGECOUNT=14
43PID_WORDCOUNT=15
44PID_CHARCOUNT=16
45PID_APPNAME=18
46PID_SECURITY=19
47
48def reset():
49 global _directories
50 _directories = sets.Set()
51
52def EnsureMSI():
53 win32com.client.gencache.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0)
54
55def EnsureMSM():
56 try:
57 win32com.client.gencache.EnsureModule('{0ADDA82F-2C26-11D2-AD65-00A0C9AF11A6}', 0, 1, 0)
58 except pywintypes.com_error:
59 win32com.client.gencache.EnsureModule('{0ADDA82F-2C26-11D2-AD65-00A0C9AF11A6}', 0, 2, 0)
60
61_Installer=None
62def MakeInstaller():
63 global _Installer
64 if _Installer is None:
65 EnsureMSI()
66 _Installer = win32com.client.Dispatch('WindowsInstaller.Installer',
67 resultCLSID='{000C1090-0000-0000-C000-000000000046}')
68 return _Installer
69
70_Merge=None
71def MakeMerge2():
72 global _Merge
73 if _Merge is None:
74 EnsureMSM()
75 _Merge = win32com.client.Dispatch("Msm.Merge2.1")
76 return _Merge
77
78class Table:
79 def __init__(self, name):
80 self.name = name
81 self.fields = []
82
83 def add_field(self, index, name, type):
84 self.fields.append((index,name,type))
85
86 def sql(self):
87 fields = []
88 keys = []
89 self.fields.sort()
90 fields = [None]*len(self.fields)
91 for index, name, type in self.fields:
92 index -= 1
93 unk = type & ~knownbits
94 if unk:
95 print "%s.%s unknown bits %x" % (self.name, name, unk)
96 size = type & datasizemask
97 dtype = type & typemask
98 if dtype == type_string:
99 if size:
100 tname="CHAR(%d)" % size
101 else:
102 tname="CHAR"
103 elif dtype == type_short:
104 assert size==2
105 tname = "SHORT"
106 elif dtype == type_long:
107 assert size==4
108 tname="LONG"
109 elif dtype == type_binary:
110 assert size==0
111 tname="OBJECT"
112 else:
113 tname="unknown"
114 print "%s.%sunknown integer type %d" % (self.name, name, size)
115 if type & type_nullable:
116 flags = ""
117 else:
118 flags = " NOT NULL"
119 if type & type_localizable:
120 flags += " LOCALIZABLE"
121 fields[index] = "`%s` %s%s" % (name, tname, flags)
122 if type & type_key:
123 keys.append("`%s`" % name)
124 fields = ", ".join(fields)
125 keys = ", ".join(keys)
126 return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self.name, fields, keys)
127
128 def create(self, db):
129 v = db.OpenView(self.sql())
130 v.Execute(None)
131 v.Close()
132
133class Binary:
134 def __init__(self, fname):
135 self.name = fname
136 def __repr__(self):
137 return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
138
139def gen_schema(destpath, schemapath):
140 d = MakeInstaller()
141 schema = d.OpenDatabase(schemapath,
142 win32com.client.constants.msiOpenDatabaseModeReadOnly)
143
144 # XXX ORBER BY
145 v=schema.OpenView("SELECT * FROM _Columns")
146 curtable=None
147 tables = []
148 v.Execute(None)
149 f = open(destpath, "wt")
150 f.write("from msilib import Table\n")
151 while 1:
152 r=v.Fetch()
153 if not r:break
154 name=r.StringData(1)
155 if curtable != name:
156 f.write("\n%s = Table('%s')\n" % (name,name))
157 curtable = name
158 tables.append(name)
159 f.write("%s.add_field(%d,'%s',%d)\n" %
160 (name, r.IntegerData(2), r.StringData(3), r.IntegerData(4)))
161 v.Close()
162
163 f.write("\ntables=[%s]\n\n" % (", ".join(tables)))
164
165 # Fill the _Validation table
166 f.write("_Validation_records = [\n")
167 v = schema.OpenView("SELECT * FROM _Validation")
168 v.Execute(None)
169 while 1:
170 r = v.Fetch()
171 if not r:break
172 # Table, Column, Nullable
173 f.write("(%s,%s,%s," %
174 (`r.StringData(1)`, `r.StringData(2)`, `r.StringData(3)`))
175 def put_int(i):
176 if r.IsNull(i):f.write("None, ")
177 else:f.write("%d," % r.IntegerData(i))
178 def put_str(i):
179 if r.IsNull(i):f.write("None, ")
180 else:f.write("%s," % `r.StringData(i)`)
181 put_int(4) # MinValue
182 put_int(5) # MaxValue
183 put_str(6) # KeyTable
184 put_int(7) # KeyColumn
185 put_str(8) # Category
186 put_str(9) # Set
187 put_str(10)# Description
188 f.write("),\n")
189 f.write("]\n\n")
190
Tim Peters94607dd2004-08-22 19:42:56 +0000191 f.close()
Martin v. Löwis8ffe9ab2004-08-22 13:34:34 +0000192
193def gen_sequence(destpath, msipath):
194 dir = os.path.dirname(destpath)
195 d = MakeInstaller()
196 seqmsi = d.OpenDatabase(msipath,
197 win32com.client.constants.msiOpenDatabaseModeReadOnly)
198
199 v = seqmsi.OpenView("SELECT * FROM _Tables");
200 v.Execute(None)
201 f = open(destpath, "w")
202 print >>f, "import msilib,os;dirname=os.path.dirname(__file__)"
203 tables = []
204 while 1:
205 r = v.Fetch()
206 if not r:break
207 table = r.StringData(1)
208 tables.append(table)
209 f.write("%s = [\n" % table)
210 v1 = seqmsi.OpenView("SELECT * FROM `%s`" % table)
211 v1.Execute(None)
212 info = v1.ColumnInfo(constants.msiColumnInfoTypes)
213 while 1:
214 r = v1.Fetch()
215 if not r:break
216 rec = []
217 for i in range(1,r.FieldCount+1):
218 if r.IsNull(i):
219 rec.append(None)
220 elif info.StringData(i)[0] in "iI":
221 rec.append(r.IntegerData(i))
222 elif info.StringData(i)[0] in "slSL":
223 rec.append(r.StringData(i))
224 elif info.StringData(i)[0]=="v":
225 size = r.DataSize(i)
226 bytes = r.ReadStream(i, size, constants.msiReadStreamBytes)
227 bytes = bytes.encode("latin-1") # binary data represented "as-is"
228 if table == "Binary":
229 fname = rec[0]+".bin"
230 open(os.path.join(dir,fname),"wb").write(bytes)
231 rec.append(Binary(fname))
232 else:
233 rec.append(bytes)
234 else:
235 raise "Unsupported column type", info.StringData(i)
236 f.write(repr(tuple(rec))+",\n")
237 v1.Close()
238 f.write("]\n\n")
239 v.Close()
240 f.write("tables=%s\n" % repr(map(str,tables)))
241 f.close()
242
243class _Unspecified:pass
244def change_sequence(seq, action, seqno=_Unspecified, cond = _Unspecified):
245 "Change the sequence number of an action in a sequence list"
246 for i in range(len(seq)):
247 if seq[i][0] == action:
248 if cond is _Unspecified:
249 cond = seq[i][1]
250 if seqno is _Unspecified:
251 seqno = seq[i][2]
252 seq[i] = (action, cond, seqno)
253 return
254 raise ValueError, "Action not found in sequence"
255
256def add_data(db, table, values):
257 d = MakeInstaller()
258 v = db.OpenView("SELECT * FROM `%s`" % table)
259 count = v.ColumnInfo(0).FieldCount
260 r = d.CreateRecord(count)
261 for value in values:
262 assert len(value) == count, value
263 for i in range(count):
264 field = value[i]
265 if isinstance(field, (int, long)):
266 r.SetIntegerData(i+1,field)
267 elif isinstance(field, basestring):
268 r.SetStringData(i+1,field)
269 elif field is None:
270 pass
271 elif isinstance(field, Binary):
272 r.SetStream(i+1, field.name)
273 else:
274 raise TypeError, "Unsupported type %s" % field.__class__.__name__
275 v.Modify(win32com.client.constants.msiViewModifyInsert, r)
276 r.ClearData()
277 v.Close()
278
279def add_stream(db, name, path):
280 d = MakeInstaller()
281 v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name)
282 r = d.CreateRecord(1)
283 r.SetStream(1, path)
284 v.Execute(r)
285 v.Close()
286
287def init_database(name, schema,
288 ProductName, ProductCode, ProductVersion,
289 Manufacturer):
290 try:
291 os.unlink(name)
292 except OSError:
293 pass
294 ProductCode = ProductCode.upper()
295 d = MakeInstaller()
296 # Create the database
297 db = d.OpenDatabase(name,
298 win32com.client.constants.msiOpenDatabaseModeCreate)
299 # Create the tables
300 for t in schema.tables:
301 t.create(db)
302 # Fill the validation table
303 add_data(db, "_Validation", schema._Validation_records)
304 # Initialize the summary information, allowing atmost 20 properties
305 si = db.GetSummaryInformation(20)
306 si.SetProperty(PID_TITLE, "Installation Database")
307 si.SetProperty(PID_SUBJECT, ProductName)
308 si.SetProperty(PID_AUTHOR, Manufacturer)
309 if Win64:
310 si.SetProperty(PID_TEMPLATE, "Intel64;1033")
311 else:
312 si.SetProperty(PID_TEMPLATE, "Intel;1033")
313 si.SetProperty(PID_REVNUMBER, ProductCode) # XXX should be package code
314 si.SetProperty(PID_WORDCOUNT, 2) # long file names, compressed, original media
315 si.SetProperty(PID_PAGECOUNT, 200)
316 si.SetProperty(PID_APPNAME, "Python MSI Library")
317 # XXX more properties
318 si.Persist()
319 add_data(db, "Property", [
320 ("ProductName", ProductName),
321 ("ProductCode", ProductCode),
322 ("ProductVersion", ProductVersion),
323 ("Manufacturer", Manufacturer),
324 ("ProductLanguage", "1033")])
325 db.Commit()
326 return db
327
328def add_tables(db, module):
329 for table in module.tables:
330 add_data(db, table, getattr(module, table))
331
332def make_id(str):
333 #str = str.replace(".", "_") # colons are allowed
334 str = str.replace(" ", "_")
335 str = str.replace("-", "_")
336 if str[0] in string.digits:
337 str = "_"+str
338 assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
339 return str
340
341def gen_uuid():
342 return str(pythoncom.CreateGuid())
343
344class CAB:
345 def __init__(self, name):
346 self.name = name
347 self.file = open(name+".txt", "wt")
348 self.filenames = sets.Set()
349 self.index = 0
350
351 def gen_id(self, dir, file):
352 logical = _logical = make_id(file)
353 pos = 1
354 while logical in self.filenames:
355 logical = "%s.%d" % (_logical, pos)
356 pos += 1
357 self.filenames.add(logical)
358 return logical
359
360 def append(self, full, file, logical = None):
361 if os.path.isdir(full):
362 return
363 if not logical:
364 logical = self.gen_id(dir, file)
365 self.index += 1
366 if full.find(" ")!=-1:
367 print >>self.file, '"%s" %s' % (full, logical)
368 else:
369 print >>self.file, '%s %s' % (full, logical)
370 return self.index, logical
371
372 def commit(self, db):
373 self.file.close()
374 try:
375 os.unlink(self.name+".cab")
376 except OSError:
377 pass
378 for k, v in [(r"Software\Microsoft\VisualStudio\7.1\Setup\VS", "VS7CommonBinDir"),
379 (r"Software\Microsoft\Win32SDK\Directories", "Install Dir")]:
380 try:
381 key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, k)
382 except WindowsError:
383 continue
384 cabarc = os.path.join(_winreg.QueryValueEx(key, v)[0], r"Bin", "cabarc.exe")
385 _winreg.CloseKey(key)
386 if not os.path.exists(cabarc):continue
387 break
388 else:
389 print "WARNING: cabarc.exe not found in registry"
390 cabarc = "cabarc.exe"
391 f = popen2.popen4(r'"%s" n %s.cab @%s.txt' % (cabarc, self.name, self.name))[0]
392 for line in f:
393 if line.startswith(" -- adding "):
394 sys.stdout.write(".")
395 else:
396 sys.stdout.write(line)
397 sys.stdout.flush()
398 if not os.path.exists(self.name+".cab"):
399 raise IOError, "cabarc failed"
400 add_data(db, "Media",
401 [(1, self.index, None, "#"+self.name, None, None)])
402 add_stream(db, self.name, self.name+".cab")
403 os.unlink(self.name+".txt")
404 os.unlink(self.name+".cab")
405 db.Commit()
406
407_directories = sets.Set()
408class Directory:
409 def __init__(self, db, cab, basedir, physical, _logical, default, componentflags=None):
410 """Create a new directory in the Directory table. There is a current component
411 at each point in time for the directory, which is either explicitly created
412 through start_component, or implicitly when files are added for the first
413 time. Files are added into the current component, and into the cab file.
414 To create a directory, a base directory object needs to be specified (can be
415 None), the path to the physical directory, and a logical directory name.
416 Default specifies the DefaultDir slot in the directory table. componentflags
417 specifies the default flags that new components get."""
418 index = 1
419 _logical = make_id(_logical)
420 logical = _logical
421 while logical in _directories:
422 logical = "%s%d" % (_logical, index)
423 index += 1
424 _directories.add(logical)
425 self.db = db
426 self.cab = cab
427 self.basedir = basedir
428 self.physical = physical
429 self.logical = logical
430 self.component = None
431 self.short_names = sets.Set()
432 self.ids = sets.Set()
433 self.keyfiles = {}
434 self.componentflags = componentflags
435 if basedir:
436 self.absolute = os.path.join(basedir.absolute, physical)
437 blogical = basedir.logical
438 else:
439 self.absolute = physical
440 blogical = None
441 add_data(db, "Directory", [(logical, blogical, default)])
442
443 def start_component(self, component = None, feature = None, flags = None, keyfile = None):
444 """Add an entry to the Component table, and make this component the current for this
445 directory. If no component name is given, the directory name is used. If no feature
446 is given, the current feature is used. If no flags are given, the directory's default
447 flags are used. If no keyfile is given, the KeyPath is left null in the Component
448 table."""
449 if flags is None:
450 flags = self.componentflags
451 uuid = gen_uuid()
452 if component is None:
453 component = self.logical
454 self.component = component
455 if Win64:
456 flags |= 256
457 if keyfile:
458 keyid = self.cab.gen_id(self.absolute, keyfile)
459 self.keyfiles[keyfile] = keyid
460 else:
461 keyid = None
462 add_data(self.db, "Component",
463 [(component, uuid, self.logical, flags, None, keyid)])
464 if feature is None:
465 feature = current_feature
466 add_data(self.db, "FeatureComponents",
467 [(feature.id, component)])
468
469 def make_short(self, file):
470 parts = file.split(".")
471 if len(parts)>1:
472 suffix = parts[-1].upper()
473 else:
474 suffix = None
475 prefix = parts[0].upper()
476 if len(prefix) <= 8 and (not suffix or len(suffix)<=3):
477 if suffix:
478 file = prefix+"."+suffix
479 else:
480 file = prefix
481 assert file not in self.short_names
482 else:
483 prefix = prefix[:6]
484 if suffix:
485 suffix = suffix[:3]
486 pos = 1
487 while 1:
488 if suffix:
489 file = "%s~%d.%s" % (prefix, pos, suffix)
490 else:
491 file = "%s~%d" % (prefix, pos)
492 if file not in self.short_names: break
493 pos += 1
494 assert pos < 10000
495 if pos in (10, 100, 1000):
496 prefix = prefix[:-1]
497 self.short_names.add(file)
498 assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
499 return file
500
501 def add_file(self, file, src=None, version=None, language=None):
502 """Add a file to the current component of the directory, starting a new one
503 one if there is no current component. By default, the file name in the source
504 and the file table will be identical. If the src file is specified, it is
505 interpreted relative to the current directory. Optionally, a version and a
506 language can be specified for the entry in the File table."""
507 if not self.component:
508 self.start_component(self.logical, current_feature)
509 if not src:
510 # Allow relative paths for file if src is not specified
511 src = file
512 file = os.path.basename(file)
513 absolute = os.path.join(self.absolute, src)
514 assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
515 if self.keyfiles.has_key(file):
516 logical = self.keyfiles[file]
517 else:
518 logical = None
519 sequence, logical = self.cab.append(absolute, file, logical)
520 assert logical not in self.ids
521 self.ids.add(logical)
522 short = self.make_short(file)
523 full = "%s|%s" % (short, file)
524 filesize = os.stat(absolute).st_size
525 # constants.msidbFileAttributesVital
526 # Compressed omitted, since it is the database default
527 # could add r/o, system, hidden
Tim Peters94607dd2004-08-22 19:42:56 +0000528 attributes = 512
Martin v. Löwis8ffe9ab2004-08-22 13:34:34 +0000529 add_data(self.db, "File",
530 [(logical, self.component, full, filesize, version,
531 language, attributes, sequence)])
532 if not version:
533 # Add hash if the file is not versioned
534 filehash = MakeInstaller().FileHash(absolute, 0)
535 add_data(self.db, "MsiFileHash",
536 [(logical, 0, filehash.IntegerData(1),
537 filehash.IntegerData(2), filehash.IntegerData(3),
538 filehash.IntegerData(4))])
539 # Automatically remove .pyc/.pyo files on uninstall (2)
540 # XXX: adding so many RemoveFile entries makes installer unbelievably
541 # slow. So instead, we have to use wildcard remove entries
542 # if file.endswith(".py"):
543 # add_data(self.db, "RemoveFile",
544 # [(logical+"c", self.component, "%sC|%sc" % (short, file),
545 # self.logical, 2),
546 # (logical+"o", self.component, "%sO|%so" % (short, file),
547 # self.logical, 2)])
548
549 def glob(self, pattern, exclude = None):
550 """Add a list of files to the current component as specified in the
551 glob pattern. Individual files can be excluded in the exclude list."""
552 files = glob.glob1(self.absolute, pattern)
553 for f in files:
554 if exclude and f in exclude: continue
555 self.add_file(f)
556 return files
557
558 def remove_pyc(self):
559 "Remove .pyc/.pyo files on uninstall"
560 add_data(self.db, "RemoveFile",
561 [(self.component+"c", self.component, "*.pyc", self.logical, 2),
562 (self.component+"o", self.component, "*.pyo", self.logical, 2)])
563
564class Feature:
565 def __init__(self, db, id, title, desc, display, level = 1,
566 parent=None, directory = None, attributes=0):
567 self.id = id
568 if parent:
569 parent = parent.id
570 add_data(db, "Feature",
571 [(id, parent, title, desc, display,
572 level, directory, attributes)])
573 def set_current(self):
574 global current_feature
575 current_feature = self
576
577class Control:
578 def __init__(self, dlg, name):
579 self.dlg = dlg
580 self.name = name
581
582 def event(self, ev, arg, cond = "1", order = None):
583 add_data(self.dlg.db, "ControlEvent",
584 [(self.dlg.name, self.name, ev, arg, cond, order)])
585
586 def mapping(self, ev, attr):
587 add_data(self.dlg.db, "EventMapping",
588 [(self.dlg.name, self.name, ev, attr)])
589
590 def condition(self, action, condition):
591 add_data(self.dlg.db, "ControlCondition",
592 [(self.dlg.name, self.name, action, condition)])
593
594class RadioButtonGroup(Control):
595 def __init__(self, dlg, name, property):
596 self.dlg = dlg
597 self.name = name
598 self.property = property
599 self.index = 1
600
601 def add(self, name, x, y, w, h, text, value = None):
602 if value is None:
603 value = name
604 add_data(self.dlg.db, "RadioButton",
605 [(self.property, self.index, value,
606 x, y, w, h, text, None)])
607 self.index += 1
608
609class Dialog:
610 def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
611 self.db = db
612 self.name = name
613 self.x, self.y, self.w, self.h = x,y,w,h
614 add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
615
616 def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
617 add_data(self.db, "Control",
618 [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
619 return Control(self, name)
620
621 def text(self, name, x, y, w, h, attr, text):
622 return self.control(name, "Text", x, y, w, h, attr, None,
623 text, None, None)
624
625 def bitmap(self, name, x, y, w, h, text):
626 return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
Tim Peters94607dd2004-08-22 19:42:56 +0000627
Martin v. Löwis8ffe9ab2004-08-22 13:34:34 +0000628 def line(self, name, x, y, w, h):
629 return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
630
631 def pushbutton(self, name, x, y, w, h, attr, text, next):
632 return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
633
634 def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
635 add_data(self.db, "Control",
636 [(self.name, name, "RadioButtonGroup",
637 x, y, w, h, attr, prop, text, next, None)])
638 return RadioButtonGroup(self, name, prop)
639
640 def checkbox(self, name, x, y, w, h, attr, prop, text, next):
Tim Peters94607dd2004-08-22 19:42:56 +0000641 return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)