1 # Microsoft Installer Library
2 # (C) 2003 Martin v. Loewis
4 import win32com
.client
.gencache
6 import pythoncom
, pywintypes
7 from win32com
.client
import constants
8 import re
, string
, os
, sets
, glob
, popen2
, sys
, _winreg
13 basestring
= (str, unicode)
17 # Partially taken from Wine
20 type_localizable
= 0x0200
30 # XXX temporary, localizable?
31 knownbits
= datasizemask | type_valid | type_localizable | \
32 typemask | type_nullable | type_key
34 # Summary Info Property IDs
55 _directories
= sets
.Set()
58 win32com
.client
.gencache
.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0)
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)
69 if _Installer
is None:
71 _Installer
= win32com
.client
.Dispatch('WindowsInstaller.Installer',
72 resultCLSID
='{000C1090-0000-0000-C000-000000000046}')
80 _Merge
= win32com
.client
.Dispatch("Msm.Merge2.1")
84 def __init__(self
, name
):
88 def add_field(self
, index
, name
, type):
89 self
.fields
.append((index
,name
,type))
95 fields
= [None]*len(self
.fields
)
96 for index
, name
, type in self
.fields
:
98 unk
= type & ~knownbits
100 print "%s.%s unknown bits %x" % (self
.name
, name
, unk
)
101 size
= type & datasizemask
102 dtype
= type & typemask
103 if dtype
== type_string
:
105 tname
="CHAR(%d)" % size
108 elif dtype
== type_short
:
111 elif dtype
== type_long
:
114 elif dtype
== type_binary
:
119 print "%s.%sunknown integer type %d" % (self
.name
, name
, size
)
120 if type & type_nullable
:
124 if type & type_localizable
:
125 flags
+= " LOCALIZABLE"
126 fields
[index
] = "`%s` %s%s" % (name
, tname
, flags
)
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())
139 def __init__(self
, fname
):
142 return 'msilib.Binary(os.path.join(dirname,"%s"))' % self
.name
144 def gen_schema(destpath
, schemapath
):
146 schema
= d
.OpenDatabase(schemapath
,
147 win32com
.client
.constants
.msiOpenDatabaseModeReadOnly
)
150 v
=schema
.OpenView("SELECT * FROM _Columns")
154 f
= open(destpath
, "wt")
155 f
.write("from msilib import Table\n")
161 f
.write("\n%s = Table('%s')\n" % (name
,name
))
164 f
.write("%s.add_field(%d,'%s',%d)\n" %
165 (name
, r
.IntegerData(2), r
.StringData(3), r
.IntegerData(4)))
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")
177 # Table, Column, Nullable
178 f
.write("(%s,%s,%s," %
179 (`r
.StringData(1)`
, `r
.StringData(2)`
, `r
.StringData(3)`
))
181 if r
.IsNull(i
):f
.write("None, ")
182 else:f
.write("%d," % r
.IntegerData(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
192 put_str(10)# Description
198 def gen_sequence(destpath
, msipath
):
199 dir = os
.path
.dirname(destpath
)
201 seqmsi
= d
.OpenDatabase(msipath
,
202 win32com
.client
.constants
.msiOpenDatabaseModeReadOnly
)
204 v
= seqmsi
.OpenView("SELECT * FROM _Tables");
206 f
= open(destpath
, "w")
207 print >>f
, "import msilib,os;dirname=os.path.dirname(__file__)"
212 table
= r
.StringData(1)
214 f
.write("%s = [\n" % table
)
215 v1
= seqmsi
.OpenView("SELECT * FROM `%s`" % table
)
217 info
= v1
.ColumnInfo(constants
.msiColumnInfoTypes
)
222 for i
in range(1,r
.FieldCount
+1):
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":
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
))
240 raise "Unsupported column type", info
.StringData(i
)
241 f
.write(repr(tuple(rec
))+",\n")
245 f
.write("tables=%s\n" % repr(map(str,tables
)))
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
:
255 if seqno
is _Unspecified
:
257 seq
[i
] = (action
, cond
, seqno
)
259 raise ValueError, "Action not found in sequence"
261 def add_data(db
, table
, values
):
263 v
= db
.OpenView("SELECT * FROM `%s`" % table
)
264 count
= v
.ColumnInfo(0).FieldCount
265 r
= d
.CreateRecord(count
)
267 assert len(value
) == count
, value
268 for i
in range(count
):
270 if isinstance(field
, (int, long)):
271 r
.SetIntegerData(i
+1,field
)
272 elif isinstance(field
, basestring
):
273 r
.SetStringData(i
+1,field
)
276 elif isinstance(field
, Binary
):
277 r
.SetStream(i
+1, field
.name
)
279 raise TypeError, "Unsupported type %s" % field
.__class
__.__name
__
280 v
.Modify(win32com
.client
.constants
.msiViewModifyInsert
, r
)
284 def add_stream(db
, name
, path
):
286 v
= db
.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name
)
287 r
= d
.CreateRecord(1)
292 def init_database(name
, schema
,
293 ProductName
, ProductCode
, ProductVersion
,
299 ProductCode
= ProductCode
.upper()
301 # Create the database
302 db
= d
.OpenDatabase(name
,
303 win32com
.client
.constants
.msiOpenDatabaseModeCreate
)
305 for t
in schema
.tables
:
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
)
315 si
.SetProperty(PID_TEMPLATE
, "Intel64;1033")
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
324 add_data(db
, "Property", [
325 ("ProductName", ProductName
),
326 ("ProductCode", ProductCode
),
327 ("ProductVersion", ProductVersion
),
328 ("Manufacturer", Manufacturer
),
329 ("ProductLanguage", "1033")])
333 def add_tables(db
, module
):
334 for table
in module
.tables
:
335 add_data(db
, table
, getattr(module
, table
))
338 #str = str.replace(".", "_") # colons are allowed
339 str = str.replace(" ", "_")
340 str = str.replace("-", "_")
341 if str[0] in string
.digits
:
343 assert re
.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
347 return str(pythoncom
.CreateGuid())
350 def __init__(self
, name
):
352 self
.file = open(name
+".txt", "wt")
353 self
.filenames
= sets
.Set()
356 def gen_id(self
, dir, file):
357 logical
= _logical
= make_id(file)
359 while logical
in self
.filenames
:
360 logical
= "%s.%d" % (_logical
, pos
)
362 self
.filenames
.add(logical
)
365 def append(self
, full
, file, logical
= None):
366 if os
.path
.isdir(full
):
369 logical
= self
.gen_id(dir, file)
371 if full
.find(" ")!=-1:
372 print >>self
.file, '"%s" %s' % (full
, logical
)
374 print >>self
.file, '%s %s' % (full
, logical
)
375 return self
.index
, logical
377 def commit(self
, db
):
380 os
.unlink(self
.name
+".cab")
383 for k
, v
in [(r
"Software\Microsoft\VisualStudio\7.1\Setup\VS", "VS7CommonBinDir"),
384 (r
"Software\Microsoft\Win32SDK\Directories", "Install Dir")]:
386 key
= _winreg
.OpenKey(_winreg
.HKEY_LOCAL_MACHINE
, k
)
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
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]
398 if line
.startswith(" -- adding "):
399 sys
.stdout
.write(".")
401 sys
.stdout
.write(line
)
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")
412 _directories
= sets
.Set()
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."""
424 _logical
= make_id(_logical
)
426 while logical
in _directories
:
427 logical
= "%s%d" % (_logical
, index
)
429 _directories
.add(logical
)
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()
439 self
.componentflags
= componentflags
441 self
.absolute
= os
.path
.join(basedir
.absolute
, physical
)
442 blogical
= basedir
.logical
444 self
.absolute
= physical
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
455 flags
= self
.componentflags
460 if component
is None:
461 component
= self
.logical
462 self
.component
= component
466 keyid
= self
.cab
.gen_id(self
.absolute
, keyfile
)
467 self
.keyfiles
[keyfile
] = keyid
470 add_data(self
.db
, "Component",
471 [(component
, uuid
, self
.logical
, flags
, None, keyid
)])
473 feature
= current_feature
474 add_data(self
.db
, "FeatureComponents",
475 [(feature
.id, component
)])
477 def make_short(self
, file):
478 parts
= file.split(".")
480 suffix
= parts
[-1].upper()
483 prefix
= parts
[0].upper()
484 if len(prefix
) <= 8 and (not suffix
or len(suffix
)<=3):
486 file = prefix
+"."+suffix
489 assert file not in self
.short_names
497 file = "%s~%d.%s" % (prefix
, pos
, suffix
)
499 file = "%s~%d" % (prefix
, pos
)
500 if file not in self
.short_names
: break
503 if pos
in (10, 100, 1000):
505 self
.short_names
.add(file)
506 assert not re
.search(r
'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
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
)
518 # Allow relative paths for file if src is not specified
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]
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
537 add_data(self
.db
, "File",
538 [(logical
, self
.component
, full
, filesize
, version
,
539 language
, attributes
, sequence
)])
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),
554 # (logical+"o", self.component, "%sO|%so" % (short, file),
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
)
562 if exclude
and f
in exclude
: continue
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)])
573 def __init__(self
, db
, id, title
, desc
, display
, level
= 1,
574 parent
=None, directory
= None, attributes
=0):
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
586 def __init__(self
, dlg
, 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):
606 self
.property = property
609 def add(self
, name
, x
, y
, w
, h
, text
, value
= None):
612 add_data(self
.dlg
.db
, "RadioButton",
613 [(self
.property, self
.index
, value
,
614 x
, y
, w
, h
, text
, None)])
618 def __init__(self
, db
, name
, x
, y
, w
, h
, attr
, title
, first
, default
, cancel
):
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,
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)