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
, subprocess
, sys
, _winreg
, struct
13 basestring
= (str, unicode)
15 # Partially taken from Wine
18 type_localizable
= 0x0200
28 # XXX temporary, localizable?
29 knownbits
= datasizemask | type_valid | type_localizable | \
30 typemask | type_nullable | type_key
32 # Summary Info Property IDs
53 _directories
= sets
.Set()
56 win32com
.client
.gencache
.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0)
60 win32com
.client
.gencache
.EnsureModule('{0ADDA82F-2C26-11D2-AD65-00A0C9AF11A6}', 0, 1, 0)
61 except pywintypes
.com_error
:
62 win32com
.client
.gencache
.EnsureModule('{0ADDA82F-2C26-11D2-AD65-00A0C9AF11A6}', 0, 2, 0)
67 if _Installer
is None:
69 _Installer
= win32com
.client
.Dispatch('WindowsInstaller.Installer',
70 resultCLSID
='{000C1090-0000-0000-C000-000000000046}')
78 _Merge
= win32com
.client
.Dispatch("Msm.Merge2.1")
82 def __init__(self
, name
):
86 def add_field(self
, index
, name
, type):
87 self
.fields
.append((index
,name
,type))
93 fields
= [None]*len(self
.fields
)
94 for index
, name
, type in self
.fields
:
96 unk
= type & ~knownbits
98 print "%s.%s unknown bits %x" % (self
.name
, name
, unk
)
99 size
= type & datasizemask
100 dtype
= type & typemask
101 if dtype
== type_string
:
103 tname
="CHAR(%d)" % size
106 elif dtype
== type_short
:
109 elif dtype
== type_long
:
112 elif dtype
== type_binary
:
117 print "%s.%sunknown integer type %d" % (self
.name
, name
, size
)
118 if type & type_nullable
:
122 if type & type_localizable
:
123 flags
+= " LOCALIZABLE"
124 fields
[index
] = "`%s` %s%s" % (name
, tname
, flags
)
126 keys
.append("`%s`" % name
)
127 fields
= ", ".join(fields
)
128 keys
= ", ".join(keys
)
129 return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self
.name
, fields
, keys
)
131 def create(self
, db
):
132 v
= db
.OpenView(self
.sql())
137 def __init__(self
, fname
):
140 return 'msilib.Binary(os.path.join(dirname,"%s"))' % self
.name
142 def gen_schema(destpath
, schemapath
):
144 schema
= d
.OpenDatabase(schemapath
,
145 win32com
.client
.constants
.msiOpenDatabaseModeReadOnly
)
148 v
=schema
.OpenView("SELECT * FROM _Columns")
152 f
= open(destpath
, "wt")
153 f
.write("from msilib import Table\n")
159 f
.write("\n%s = Table('%s')\n" % (name
,name
))
162 f
.write("%s.add_field(%d,'%s',%d)\n" %
163 (name
, r
.IntegerData(2), r
.StringData(3), r
.IntegerData(4)))
166 f
.write("\ntables=[%s]\n\n" % (", ".join(tables
)))
168 # Fill the _Validation table
169 f
.write("_Validation_records = [\n")
170 v
= schema
.OpenView("SELECT * FROM _Validation")
175 # Table, Column, Nullable
176 f
.write("(%s,%s,%s," %
177 (`r
.StringData(1)`
, `r
.StringData(2)`
, `r
.StringData(3)`
))
179 if r
.IsNull(i
):f
.write("None, ")
180 else:f
.write("%d," % r
.IntegerData(i
))
182 if r
.IsNull(i
):f
.write("None, ")
183 else:f
.write("%s," % `r
.StringData(i
)`
)
184 put_int(4) # MinValue
185 put_int(5) # MaxValue
186 put_str(6) # KeyTable
187 put_int(7) # KeyColumn
188 put_str(8) # Category
190 put_str(10)# Description
196 def gen_sequence(destpath
, msipath
):
197 dir = os
.path
.dirname(destpath
)
199 seqmsi
= d
.OpenDatabase(msipath
,
200 win32com
.client
.constants
.msiOpenDatabaseModeReadOnly
)
202 v
= seqmsi
.OpenView("SELECT * FROM _Tables");
204 f
= open(destpath
, "w")
205 print >>f
, "import msilib,os;dirname=os.path.dirname(__file__)"
210 table
= r
.StringData(1)
212 f
.write("%s = [\n" % table
)
213 v1
= seqmsi
.OpenView("SELECT * FROM `%s`" % table
)
215 info
= v1
.ColumnInfo(constants
.msiColumnInfoTypes
)
220 for i
in range(1,r
.FieldCount
+1):
223 elif info
.StringData(i
)[0] in "iI":
224 rec
.append(r
.IntegerData(i
))
225 elif info
.StringData(i
)[0] in "slSL":
226 rec
.append(r
.StringData(i
))
227 elif info
.StringData(i
)[0]=="v":
229 bytes
= r
.ReadStream(i
, size
, constants
.msiReadStreamBytes
)
230 bytes
= bytes
.encode("latin-1") # binary data represented "as-is"
231 if table
== "Binary":
232 fname
= rec
[0]+".bin"
233 open(os
.path
.join(dir,fname
),"wb").write(bytes
)
234 rec
.append(Binary(fname
))
238 raise "Unsupported column type", info
.StringData(i
)
239 f
.write(repr(tuple(rec
))+",\n")
243 f
.write("tables=%s\n" % repr(map(str,tables
)))
246 class _Unspecified
:pass
247 def change_sequence(seq
, action
, seqno
=_Unspecified
, cond
= _Unspecified
):
248 "Change the sequence number of an action in a sequence list"
249 for i
in range(len(seq
)):
250 if seq
[i
][0] == action
:
251 if cond
is _Unspecified
:
253 if seqno
is _Unspecified
:
255 seq
[i
] = (action
, cond
, seqno
)
257 raise ValueError, "Action not found in sequence"
259 def add_data(db
, table
, values
):
261 v
= db
.OpenView("SELECT * FROM `%s`" % table
)
262 count
= v
.ColumnInfo(0).FieldCount
263 r
= d
.CreateRecord(count
)
265 assert len(value
) == count
, value
266 for i
in range(count
):
268 if isinstance(field
, (int, long)):
269 r
.SetIntegerData(i
+1,field
)
270 elif isinstance(field
, basestring
):
271 r
.SetStringData(i
+1,field
)
274 elif isinstance(field
, Binary
):
275 r
.SetStream(i
+1, field
.name
)
277 raise TypeError, "Unsupported type %s" % field
.__class
__.__name
__
278 v
.Modify(win32com
.client
.constants
.msiViewModifyInsert
, r
)
282 def add_stream(db
, name
, path
):
284 v
= db
.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name
)
285 r
= d
.CreateRecord(1)
290 def init_database(name
, schema
,
291 ProductName
, ProductCode
, ProductVersion
,
293 request_uac
= False):
298 ProductCode
= ProductCode
.upper()
300 # Create the database
301 db
= d
.OpenDatabase(name
,
302 win32com
.client
.constants
.msiOpenDatabaseModeCreate
)
304 for t
in schema
.tables
:
306 # Fill the validation table
307 add_data(db
, "_Validation", schema
._Validation
_records
)
308 # Initialize the summary information, allowing atmost 20 properties
309 si
= db
.GetSummaryInformation(20)
310 si
.SetProperty(PID_TITLE
, "Installation Database")
311 si
.SetProperty(PID_SUBJECT
, ProductName
)
312 si
.SetProperty(PID_AUTHOR
, Manufacturer
)
313 si
.SetProperty(PID_TEMPLATE
, msi_type
)
314 si
.SetProperty(PID_REVNUMBER
, gen_uuid())
316 wc
= 2 # long file names, compressed, original media
318 wc
= 2 |
8 # +never invoke UAC
319 si
.SetProperty(PID_WORDCOUNT
, wc
)
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 str = str.replace("+", "_")
342 if str[0] in string
.digits
:
344 assert re
.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
348 return str(pythoncom
.CreateGuid())
351 def __init__(self
, name
):
353 self
.file = open(name
+".txt", "wt")
354 self
.filenames
= sets
.Set()
357 def gen_id(self
, dir, file):
358 logical
= _logical
= make_id(file)
360 while logical
in self
.filenames
:
361 logical
= "%s.%d" % (_logical
, pos
)
363 self
.filenames
.add(logical
)
366 def append(self
, full
, file, logical
= None):
367 if os
.path
.isdir(full
):
370 logical
= self
.gen_id(dir, file)
372 if full
.find(" ")!=-1:
373 print >>self
.file, '"%s" %s' % (full
, logical
)
375 print >>self
.file, '%s %s' % (full
, logical
)
376 return self
.index
, logical
378 def commit(self
, db
):
381 os
.unlink(self
.name
+".cab")
384 for k
, v
in [(r
"Software\Microsoft\VisualStudio\7.1\Setup\VS", "VS7CommonBinDir"),
385 (r
"Software\Microsoft\VisualStudio\8.0\Setup\VS", "VS7CommonBinDir"),
386 (r
"Software\Microsoft\VisualStudio\9.0\Setup\VS", "VS7CommonBinDir"),
387 (r
"Software\Microsoft\Win32SDK\Directories", "Install Dir"),
390 key
= _winreg
.OpenKey(_winreg
.HKEY_LOCAL_MACHINE
, k
)
391 dir = _winreg
.QueryValueEx(key
, v
)[0]
392 _winreg
.CloseKey(key
)
393 except (WindowsError, IndexError):
395 cabarc
= os
.path
.join(dir, r
"Bin", "cabarc.exe")
396 if not os
.path
.exists(cabarc
):
400 print "WARNING: cabarc.exe not found in registry"
401 cabarc
= "cabarc.exe"
402 cmd
= r
'"%s" -m lzx:21 n %s.cab @%s.txt' % (cabarc
, self
.name
, self
.name
)
403 p
= subprocess
.Popen(cmd
, shell
=True, stdin
=subprocess
.PIPE
,
404 stdout
=subprocess
.PIPE
, stderr
=subprocess
.STDOUT
)
405 for line
in p
.stdout
:
406 if line
.startswith(" -- adding "):
407 sys
.stdout
.write(".")
409 sys
.stdout
.write(line
)
411 if not os
.path
.exists(self
.name
+".cab"):
412 raise IOError, "cabarc failed"
413 add_data(db
, "Media",
414 [(1, self
.index
, None, "#"+self
.name
, None, None)])
415 add_stream(db
, self
.name
, self
.name
+".cab")
416 os
.unlink(self
.name
+".txt")
417 os
.unlink(self
.name
+".cab")
420 _directories
= sets
.Set()
422 def __init__(self
, db
, cab
, basedir
, physical
, _logical
, default
, componentflags
=None):
423 """Create a new directory in the Directory table. There is a current component
424 at each point in time for the directory, which is either explicitly created
425 through start_component, or implicitly when files are added for the first
426 time. Files are added into the current component, and into the cab file.
427 To create a directory, a base directory object needs to be specified (can be
428 None), the path to the physical directory, and a logical directory name.
429 Default specifies the DefaultDir slot in the directory table. componentflags
430 specifies the default flags that new components get."""
432 _logical
= make_id(_logical
)
434 while logical
in _directories
:
435 logical
= "%s%d" % (_logical
, index
)
437 _directories
.add(logical
)
440 self
.basedir
= basedir
441 self
.physical
= physical
442 self
.logical
= logical
443 self
.component
= None
444 self
.short_names
= sets
.Set()
445 self
.ids
= sets
.Set()
447 self
.componentflags
= componentflags
449 self
.absolute
= os
.path
.join(basedir
.absolute
, physical
)
450 blogical
= basedir
.logical
452 self
.absolute
= physical
454 add_data(db
, "Directory", [(logical
, blogical
, default
)])
456 def start_component(self
, component
= None, feature
= None, flags
= None, keyfile
= None, uuid
=None):
457 """Add an entry to the Component table, and make this component the current for this
458 directory. If no component name is given, the directory name is used. If no feature
459 is given, the current feature is used. If no flags are given, the directory's default
460 flags are used. If no keyfile is given, the KeyPath is left null in the Component
463 flags
= self
.componentflags
468 if component
is None:
469 component
= self
.logical
470 self
.component
= component
474 keyid
= self
.cab
.gen_id(self
.absolute
, keyfile
)
475 self
.keyfiles
[keyfile
] = keyid
478 add_data(self
.db
, "Component",
479 [(component
, uuid
, self
.logical
, flags
, None, keyid
)])
481 feature
= current_feature
482 add_data(self
.db
, "FeatureComponents",
483 [(feature
.id, component
)])
485 def make_short(self
, file):
486 file = re
.sub(r
'[\?|><:/*"+,;=\[\]]', '_', file) # restrictions on short names
487 parts
= file.split(".")
489 suffix
= parts
[-1].upper()
492 prefix
= parts
[0].upper()
493 if len(prefix
) <= 8 and (not suffix
or len(suffix
)<=3):
495 file = prefix
+"."+suffix
498 assert file not in self
.short_names
506 file = "%s~%d.%s" % (prefix
, pos
, suffix
)
508 file = "%s~%d" % (prefix
, pos
)
509 if file not in self
.short_names
: break
512 if pos
in (10, 100, 1000):
514 self
.short_names
.add(file)
517 def add_file(self
, file, src
=None, version
=None, language
=None):
518 """Add a file to the current component of the directory, starting a new one
519 one if there is no current component. By default, the file name in the source
520 and the file table will be identical. If the src file is specified, it is
521 interpreted relative to the current directory. Optionally, a version and a
522 language can be specified for the entry in the File table."""
523 if not self
.component
:
524 self
.start_component(self
.logical
, current_feature
)
526 # Allow relative paths for file if src is not specified
528 file = os
.path
.basename(file)
529 absolute
= os
.path
.join(self
.absolute
, src
)
530 assert not re
.search(r
'[\?|><:/*]"', file) # restrictions on long names
531 if self
.keyfiles
.has_key(file):
532 logical
= self
.keyfiles
[file]
535 sequence
, logical
= self
.cab
.append(absolute
, file, logical
)
536 assert logical
not in self
.ids
537 self
.ids
.add(logical
)
538 short
= self
.make_short(file)
539 full
= "%s|%s" % (short
, file)
540 filesize
= os
.stat(absolute
).st_size
541 # constants.msidbFileAttributesVital
542 # Compressed omitted, since it is the database default
543 # could add r/o, system, hidden
545 add_data(self
.db
, "File",
546 [(logical
, self
.component
, full
, filesize
, version
,
547 language
, attributes
, sequence
)])
549 # Add hash if the file is not versioned
550 filehash
= MakeInstaller().FileHash(absolute
, 0)
551 add_data(self
.db
, "MsiFileHash",
552 [(logical
, 0, filehash
.IntegerData(1),
553 filehash
.IntegerData(2), filehash
.IntegerData(3),
554 filehash
.IntegerData(4))])
555 # Automatically remove .pyc/.pyo files on uninstall (2)
556 # XXX: adding so many RemoveFile entries makes installer unbelievably
557 # slow. So instead, we have to use wildcard remove entries
558 # if file.endswith(".py"):
559 # add_data(self.db, "RemoveFile",
560 # [(logical+"c", self.component, "%sC|%sc" % (short, file),
562 # (logical+"o", self.component, "%sO|%so" % (short, file),
565 def glob(self
, pattern
, exclude
= None):
566 """Add a list of files to the current component as specified in the
567 glob pattern. Individual files can be excluded in the exclude list."""
568 files
= glob
.glob1(self
.absolute
, pattern
)
570 if exclude
and f
in exclude
: continue
574 def remove_pyc(self
):
575 "Remove .pyc/.pyo files on uninstall"
576 add_data(self
.db
, "RemoveFile",
577 [(self
.component
+"c", self
.component
, "*.pyc", self
.logical
, 2),
578 (self
.component
+"o", self
.component
, "*.pyo", self
.logical
, 2)])
580 def removefile(self
, key
, pattern
):
581 "Add a RemoveFile entry"
582 add_data(self
.db
, "RemoveFile", [(self
.component
+key
, self
.component
, pattern
, self
.logical
, 2)])
586 def __init__(self
, db
, id, title
, desc
, display
, level
= 1,
587 parent
=None, directory
= None, attributes
=0):
591 add_data(db
, "Feature",
592 [(id, parent
, title
, desc
, display
,
593 level
, directory
, attributes
)])
594 def set_current(self
):
595 global current_feature
596 current_feature
= self
599 def __init__(self
, dlg
, name
):
603 def event(self
, ev
, arg
, cond
= "1", order
= None):
604 add_data(self
.dlg
.db
, "ControlEvent",
605 [(self
.dlg
.name
, self
.name
, ev
, arg
, cond
, order
)])
607 def mapping(self
, ev
, attr
):
608 add_data(self
.dlg
.db
, "EventMapping",
609 [(self
.dlg
.name
, self
.name
, ev
, attr
)])
611 def condition(self
, action
, condition
):
612 add_data(self
.dlg
.db
, "ControlCondition",
613 [(self
.dlg
.name
, self
.name
, action
, condition
)])
615 class RadioButtonGroup(Control
):
616 def __init__(self
, dlg
, name
, property):
619 self
.property = property
622 def add(self
, name
, x
, y
, w
, h
, text
, value
= None):
625 add_data(self
.dlg
.db
, "RadioButton",
626 [(self
.property, self
.index
, value
,
627 x
, y
, w
, h
, text
, None)])
631 def __init__(self
, db
, name
, x
, y
, w
, h
, attr
, title
, first
, default
, cancel
):
634 self
.x
, self
.y
, self
.w
, self
.h
= x
,y
,w
,h
635 add_data(db
, "Dialog", [(name
, x
,y
,w
,h
,attr
,title
,first
,default
,cancel
)])
637 def control(self
, name
, type, x
, y
, w
, h
, attr
, prop
, text
, next
, help):
638 add_data(self
.db
, "Control",
639 [(self
.name
, name
, type, x
, y
, w
, h
, attr
, prop
, text
, next
, help)])
640 return Control(self
, name
)
642 def text(self
, name
, x
, y
, w
, h
, attr
, text
):
643 return self
.control(name
, "Text", x
, y
, w
, h
, attr
, None,
646 def bitmap(self
, name
, x
, y
, w
, h
, text
):
647 return self
.control(name
, "Bitmap", x
, y
, w
, h
, 1, None, text
, None, None)
649 def line(self
, name
, x
, y
, w
, h
):
650 return self
.control(name
, "Line", x
, y
, w
, h
, 1, None, None, None, None)
652 def pushbutton(self
, name
, x
, y
, w
, h
, attr
, text
, next
):
653 return self
.control(name
, "PushButton", x
, y
, w
, h
, attr
, None, text
, next
, None)
655 def radiogroup(self
, name
, x
, y
, w
, h
, attr
, prop
, text
, next
):
656 add_data(self
.db
, "Control",
657 [(self
.name
, name
, "RadioButtonGroup",
658 x
, y
, w
, h
, attr
, prop
, text
, next
, None)])
659 return RadioButtonGroup(self
, name
, prop
)
661 def checkbox(self
, name
, x
, y
, w
, h
, attr
, prop
, text
, next
):
662 return self
.control(name
, "CheckBox", x
, y
, w
, h
, attr
, prop
, text
, next
, None)
665 header
= open(path
, "rb").read(1000)
666 # offset of PE header is at offset 0x3c
667 pe_offset
= struct
.unpack("<i", header
[0x3c:0x40])[0]
668 assert header
[pe_offset
:pe_offset
+4] == "PE\0\0"
669 machine
= struct
.unpack("<H", header
[pe_offset
+4:pe_offset
+6])[0]
672 def set_arch_from_file(path
):
673 global msi_type
, Win64
, arch_ext
674 machine
= pe_type(path
)
680 elif machine
== 0x200:
685 elif machine
== 0x8664:
691 raise ValueError, "Unsupported architecture"