Fixed bug in time-to-midnight calculation.
[python.git] / Tools / msi / msilib.py
blob948099db5fc078ef8a1472cced90b563164fb165
1 # Microsoft Installer Library
2 # (C) 2003 Martin v. Loewis
4 import win32com.client.gencache
5 import win32com.client
6 import pythoncom, pywintypes
7 from win32com.client import constants
8 import re, string, os, sets, glob, popen2, sys, _winreg
10 try:
11 basestring
12 except NameError:
13 basestring = (str, unicode)
15 Win64 = 0
17 # Partially taken from Wine
18 datasizemask= 0x00ff
19 type_valid= 0x0100
20 type_localizable= 0x0200
22 typemask= 0x0c00
23 type_long= 0x0000
24 type_short= 0x0400
25 type_string= 0x0c00
26 type_binary= 0x0800
28 type_nullable= 0x1000
29 type_key= 0x2000
30 # XXX temporary, localizable?
31 knownbits = datasizemask | type_valid | type_localizable | \
32 typemask | type_nullable | type_key
34 # Summary Info Property IDs
35 PID_CODEPAGE=1
36 PID_TITLE=2
37 PID_SUBJECT=3
38 PID_AUTHOR=4
39 PID_KEYWORDS=5
40 PID_COMMENTS=6
41 PID_TEMPLATE=7
42 PID_LASTAUTHOR=8
43 PID_REVNUMBER=9
44 PID_LASTPRINTED=11
45 PID_CREATE_DTM=12
46 PID_LASTSAVE_DTM=13
47 PID_PAGECOUNT=14
48 PID_WORDCOUNT=15
49 PID_CHARCOUNT=16
50 PID_APPNAME=18
51 PID_SECURITY=19
53 def reset():
54 global _directories
55 _directories = sets.Set()
57 def EnsureMSI():
58 win32com.client.gencache.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0)
60 def 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)
66 _Installer=None
67 def 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
75 _Merge=None
76 def MakeMerge2():
77 global _Merge
78 if _Merge is None:
79 EnsureMSM()
80 _Merge = win32com.client.Dispatch("Msm.Merge2.1")
81 return _Merge
83 class Table:
84 def __init__(self, name):
85 self.name = name
86 self.fields = []
88 def add_field(self, index, name, type):
89 self.fields.append((index,name,type))
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)
133 def create(self, db):
134 v = db.OpenView(self.sql())
135 v.Execute(None)
136 v.Close()
138 class 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
144 def gen_schema(destpath, schemapath):
145 d = MakeInstaller()
146 schema = d.OpenDatabase(schemapath,
147 win32com.client.constants.msiOpenDatabaseModeReadOnly)
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()
168 f.write("\ntables=[%s]\n\n" % (", ".join(tables)))
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")
196 f.close()
198 def gen_sequence(destpath, msipath):
199 dir = os.path.dirname(destpath)
200 d = MakeInstaller()
201 seqmsi = d.OpenDatabase(msipath,
202 win32com.client.constants.msiOpenDatabaseModeReadOnly)
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()
248 class _Unspecified:pass
249 def 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"
261 def 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()
284 def 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()
292 def 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")
318 si.SetProperty(PID_REVNUMBER, gen_uuid())
319 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
333 def add_tables(db, module):
334 for table in module.tables:
335 add_data(db, table, getattr(module, table))
337 def 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
346 def gen_uuid():
347 return str(pythoncom.CreateGuid())
349 class 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
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
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
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" -m lzx:21 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()
412 _directories = sets.Set()
413 class 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)])
448 def start_component(self, component = None, feature = None, flags = None, keyfile = None, uuid=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 if uuid is None:
457 uuid = gen_uuid()
458 else:
459 uuid = uuid.upper()
460 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)])
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
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
536 attributes = 512
537 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)])
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
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)])
572 class 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
585 class Control:
586 def __init__(self, dlg, name):
587 self.dlg = dlg
588 self.name = name
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)])
594 def mapping(self, ev, attr):
595 add_data(self.dlg.db, "EventMapping",
596 [(self.dlg.name, self.name, ev, attr)])
598 def condition(self, action, condition):
599 add_data(self.dlg.db, "ControlCondition",
600 [(self.dlg.name, self.name, action, condition)])
602 class 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
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
617 class 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)])
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)
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)
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)
636 def line(self, name, x, y, w, h):
637 return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
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)
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)
648 def checkbox(self, name, x, y, w, h, attr, prop, text, next):
649 return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)