blob: 61715fcd3b9bd91db6df65c628d069d2997de44b [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
448 def start_component(self, component = None, feature = None, flags = None, keyfile = None):
449 """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
456 uuid = gen_uuid()
457 if component is None:
458 component = self.logical
459 self.component = component
460 if Win64:
461 flags |= 256
462 if keyfile:
463 keyid = self.cab.gen_id(self.absolute, keyfile)
464 self.keyfiles[keyfile] = keyid
465 else:
466 keyid = None
467 add_data(self.db, "Component",
468 [(component, uuid, self.logical, flags, None, keyid)])
469 if feature is None:
470 feature = current_feature
471 add_data(self.db, "FeatureComponents",
472 [(feature.id, component)])
473
474 def make_short(self, file):
475 parts = file.split(".")
476 if len(parts)>1:
477 suffix = parts[-1].upper()
478 else:
479 suffix = None
480 prefix = parts[0].upper()
481 if len(prefix) <= 8 and (not suffix or len(suffix)<=3):
482 if suffix:
483 file = prefix+"."+suffix
484 else:
485 file = prefix
486 assert file not in self.short_names
487 else:
488 prefix = prefix[:6]
489 if suffix:
490 suffix = suffix[:3]
491 pos = 1
492 while 1:
493 if suffix:
494 file = "%s~%d.%s" % (prefix, pos, suffix)
495 else:
496 file = "%s~%d" % (prefix, pos)
497 if file not in self.short_names: break
498 pos += 1
499 assert pos < 10000
500 if pos in (10, 100, 1000):
501 prefix = prefix[:-1]
502 self.short_names.add(file)
503 assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
504 return file
505
506 def add_file(self, file, src=None, version=None, language=None):
507 """Add a file to the current component of the directory, starting a new one
508 one if there is no current component. By default, the file name in the source
509 and the file table will be identical. If the src file is specified, it is
510 interpreted relative to the current directory. Optionally, a version and a
511 language can be specified for the entry in the File table."""
512 if not self.component:
513 self.start_component(self.logical, current_feature)
514 if not src:
515 # Allow relative paths for file if src is not specified
516 src = file
517 file = os.path.basename(file)
518 absolute = os.path.join(self.absolute, src)
519 assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
520 if self.keyfiles.has_key(file):
521 logical = self.keyfiles[file]
522 else:
523 logical = None
524 sequence, logical = self.cab.append(absolute, file, logical)
525 assert logical not in self.ids
526 self.ids.add(logical)
527 short = self.make_short(file)
528 full = "%s|%s" % (short, file)
529 filesize = os.stat(absolute).st_size
530 # constants.msidbFileAttributesVital
531 # Compressed omitted, since it is the database default
532 # could add r/o, system, hidden
Tim Peters94607dd2004-08-22 19:42:56 +0000533 attributes = 512
Martin v. Löwis8ffe9ab2004-08-22 13:34:34 +0000534 add_data(self.db, "File",
535 [(logical, self.component, full, filesize, version,
536 language, attributes, sequence)])
537 if not version:
538 # Add hash if the file is not versioned
539 filehash = MakeInstaller().FileHash(absolute, 0)
540 add_data(self.db, "MsiFileHash",
541 [(logical, 0, filehash.IntegerData(1),
542 filehash.IntegerData(2), filehash.IntegerData(3),
543 filehash.IntegerData(4))])
544 # Automatically remove .pyc/.pyo files on uninstall (2)
545 # XXX: adding so many RemoveFile entries makes installer unbelievably
546 # slow. So instead, we have to use wildcard remove entries
547 # if file.endswith(".py"):
548 # add_data(self.db, "RemoveFile",
549 # [(logical+"c", self.component, "%sC|%sc" % (short, file),
550 # self.logical, 2),
551 # (logical+"o", self.component, "%sO|%so" % (short, file),
552 # self.logical, 2)])
553
554 def glob(self, pattern, exclude = None):
555 """Add a list of files to the current component as specified in the
556 glob pattern. Individual files can be excluded in the exclude list."""
557 files = glob.glob1(self.absolute, pattern)
558 for f in files:
559 if exclude and f in exclude: continue
560 self.add_file(f)
561 return files
562
563 def remove_pyc(self):
564 "Remove .pyc/.pyo files on uninstall"
565 add_data(self.db, "RemoveFile",
566 [(self.component+"c", self.component, "*.pyc", self.logical, 2),
567 (self.component+"o", self.component, "*.pyo", self.logical, 2)])
568
569class Feature:
570 def __init__(self, db, id, title, desc, display, level = 1,
571 parent=None, directory = None, attributes=0):
572 self.id = id
573 if parent:
574 parent = parent.id
575 add_data(db, "Feature",
576 [(id, parent, title, desc, display,
577 level, directory, attributes)])
578 def set_current(self):
579 global current_feature
580 current_feature = self
581
582class Control:
583 def __init__(self, dlg, name):
584 self.dlg = dlg
585 self.name = name
586
587 def event(self, ev, arg, cond = "1", order = None):
588 add_data(self.dlg.db, "ControlEvent",
589 [(self.dlg.name, self.name, ev, arg, cond, order)])
590
591 def mapping(self, ev, attr):
592 add_data(self.dlg.db, "EventMapping",
593 [(self.dlg.name, self.name, ev, attr)])
594
595 def condition(self, action, condition):
596 add_data(self.dlg.db, "ControlCondition",
597 [(self.dlg.name, self.name, action, condition)])
598
599class RadioButtonGroup(Control):
600 def __init__(self, dlg, name, property):
601 self.dlg = dlg
602 self.name = name
603 self.property = property
604 self.index = 1
605
606 def add(self, name, x, y, w, h, text, value = None):
607 if value is None:
608 value = name
609 add_data(self.dlg.db, "RadioButton",
610 [(self.property, self.index, value,
611 x, y, w, h, text, None)])
612 self.index += 1
613
614class Dialog:
615 def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
616 self.db = db
617 self.name = name
618 self.x, self.y, self.w, self.h = x,y,w,h
619 add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
620
621 def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
622 add_data(self.db, "Control",
623 [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
624 return Control(self, name)
625
626 def text(self, name, x, y, w, h, attr, text):
627 return self.control(name, "Text", x, y, w, h, attr, None,
628 text, None, None)
629
630 def bitmap(self, name, x, y, w, h, text):
631 return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
Tim Peters94607dd2004-08-22 19:42:56 +0000632
Martin v. Löwis8ffe9ab2004-08-22 13:34:34 +0000633 def line(self, name, x, y, w, h):
634 return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
635
636 def pushbutton(self, name, x, y, w, h, attr, text, next):
637 return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
638
639 def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
640 add_data(self.db, "Control",
641 [(self.name, name, "RadioButtonGroup",
642 x, y, w, h, attr, prop, text, next, None)])
643 return RadioButtonGroup(self, name, prop)
644
645 def checkbox(self, name, x, y, w, h, attr, prop, text, next):
Tim Peters94607dd2004-08-22 19:42:56 +0000646 return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)