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