1# 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, subprocess, sys, _winreg, struct 9 10try: 11 basestring 12except NameError: 13 basestring = (str, unicode) 14 15# Partially taken from Wine 16datasizemask= 0x00ff 17type_valid= 0x0100 18type_localizable= 0x0200 19 20typemask= 0x0c00 21type_long= 0x0000 22type_short= 0x0400 23type_string= 0x0c00 24type_binary= 0x0800 25 26type_nullable= 0x1000 27type_key= 0x2000 28# XXX temporary, localizable? 29knownbits = datasizemask | type_valid | type_localizable | \ 30 typemask | type_nullable | type_key 31 32# Summary Info Property IDs 33PID_CODEPAGE=1 34PID_TITLE=2 35PID_SUBJECT=3 36PID_AUTHOR=4 37PID_KEYWORDS=5 38PID_COMMENTS=6 39PID_TEMPLATE=7 40PID_LASTAUTHOR=8 41PID_REVNUMBER=9 42PID_LASTPRINTED=11 43PID_CREATE_DTM=12 44PID_LASTSAVE_DTM=13 45PID_PAGECOUNT=14 46PID_WORDCOUNT=15 47PID_CHARCOUNT=16 48PID_APPNAME=18 49PID_SECURITY=19 50 51def reset(): 52 global _directories 53 _directories = sets.Set() 54 55def EnsureMSI(): 56 win32com.client.gencache.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0) 57 58def EnsureMSM(): 59 try: 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) 63 64_Installer=None 65def MakeInstaller(): 66 global _Installer 67 if _Installer is None: 68 EnsureMSI() 69 _Installer = win32com.client.Dispatch('WindowsInstaller.Installer', 70 resultCLSID='{000C1090-0000-0000-C000-000000000046}') 71 return _Installer 72 73_Merge=None 74def MakeMerge2(): 75 global _Merge 76 if _Merge is None: 77 EnsureMSM() 78 _Merge = win32com.client.Dispatch("Msm.Merge2.1") 79 return _Merge 80 81class Table: 82 def __init__(self, name): 83 self.name = name 84 self.fields = [] 85 86 def add_field(self, index, name, type): 87 self.fields.append((index,name,type)) 88 89 def sql(self): 90 fields = [] 91 keys = [] 92 self.fields.sort() 93 fields = [None]*len(self.fields) 94 for index, name, type in self.fields: 95 index -= 1 96 unk = type & ~knownbits 97 if unk: 98 print "%s.%s unknown bits %x" % (self.name, name, unk) 99 size = type & datasizemask 100 dtype = type & typemask 101 if dtype == type_string: 102 if size: 103 tname="CHAR(%d)" % size 104 else: 105 tname="CHAR" 106 elif dtype == type_short: 107 assert size==2 108 tname = "SHORT" 109 elif dtype == type_long: 110 assert size==4 111 tname="LONG" 112 elif dtype == type_binary: 113 assert size==0 114 tname="OBJECT" 115 else: 116 tname="unknown" 117 print "%s.%sunknown integer type %d" % (self.name, name, size) 118 if type & type_nullable: 119 flags = "" 120 else: 121 flags = " NOT NULL" 122 if type & type_localizable: 123 flags += " LOCALIZABLE" 124 fields[index] = "`%s` %s%s" % (name, tname, flags) 125 if type & type_key: 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) 130 131 def create(self, db): 132 v = db.OpenView(self.sql()) 133 v.Execute(None) 134 v.Close() 135 136class Binary: 137 def __init__(self, fname): 138 self.name = fname 139 def __repr__(self): 140 return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name 141 142def gen_schema(destpath, schemapath): 143 d = MakeInstaller() 144 schema = d.OpenDatabase(schemapath, 145 win32com.client.constants.msiOpenDatabaseModeReadOnly) 146 147 # XXX ORBER BY 148 v=schema.OpenView("SELECT * FROM _Columns") 149 curtable=None 150 tables = [] 151 v.Execute(None) 152 f = open(destpath, "wt") 153 f.write("from msilib import Table\n") 154 while 1: 155 r=v.Fetch() 156 if not r:break 157 name=r.StringData(1) 158 if curtable != name: 159 f.write("\n%s = Table('%s')\n" % (name,name)) 160 curtable = name 161 tables.append(name) 162 f.write("%s.add_field(%d,'%s',%d)\n" % 163 (name, r.IntegerData(2), r.StringData(3), r.IntegerData(4))) 164 v.Close() 165 166 f.write("\ntables=[%s]\n\n" % (", ".join(tables))) 167 168 # Fill the _Validation table 169 f.write("_Validation_records = [\n") 170 v = schema.OpenView("SELECT * FROM _Validation") 171 v.Execute(None) 172 while 1: 173 r = v.Fetch() 174 if not r:break 175 # Table, Column, Nullable 176 f.write("(%s,%s,%s," % 177 (`r.StringData(1)`, `r.StringData(2)`, `r.StringData(3)`)) 178 def put_int(i): 179 if r.IsNull(i):f.write("None, ") 180 else:f.write("%d," % r.IntegerData(i)) 181 def put_str(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 189 put_str(9) # Set 190 put_str(10)# Description 191 f.write("),\n") 192 f.write("]\n\n") 193 194 f.close() 195 196def gen_sequence(destpath, msipath): 197 dir = os.path.dirname(destpath) 198 d = MakeInstaller() 199 seqmsi = d.OpenDatabase(msipath, 200 win32com.client.constants.msiOpenDatabaseModeReadOnly) 201 202 v = seqmsi.OpenView("SELECT * FROM _Tables"); 203 v.Execute(None) 204 f = open(destpath, "w") 205 print >>f, "import msilib,os;dirname=os.path.dirname(__file__)" 206 tables = [] 207 while 1: 208 r = v.Fetch() 209 if not r:break 210 table = r.StringData(1) 211 tables.append(table) 212 f.write("%s = [\n" % table) 213 v1 = seqmsi.OpenView("SELECT * FROM `%s`" % table) 214 v1.Execute(None) 215 info = v1.ColumnInfo(constants.msiColumnInfoTypes) 216 while 1: 217 r = v1.Fetch() 218 if not r:break 219 rec = [] 220 for i in range(1,r.FieldCount+1): 221 if r.IsNull(i): 222 rec.append(None) 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": 228 size = r.DataSize(i) 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)) 235 else: 236 rec.append(bytes) 237 else: 238 raise "Unsupported column type", info.StringData(i) 239 f.write(repr(tuple(rec))+",\n") 240 v1.Close() 241 f.write("]\n\n") 242 v.Close() 243 f.write("tables=%s\n" % repr(map(str,tables))) 244 f.close() 245 246class _Unspecified:pass 247def 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: 252 cond = seq[i][1] 253 if seqno is _Unspecified: 254 seqno = seq[i][2] 255 seq[i] = (action, cond, seqno) 256 return 257 raise ValueError, "Action not found in sequence" 258 259def add_data(db, table, values): 260 d = MakeInstaller() 261 v = db.OpenView("SELECT * FROM `%s`" % table) 262 count = v.ColumnInfo(0).FieldCount 263 r = d.CreateRecord(count) 264 for value in values: 265 assert len(value) == count, value 266 for i in range(count): 267 field = value[i] 268 if isinstance(field, (int, long)): 269 r.SetIntegerData(i+1,field) 270 elif isinstance(field, basestring): 271 r.SetStringData(i+1,field) 272 elif field is None: 273 pass 274 elif isinstance(field, Binary): 275 r.SetStream(i+1, field.name) 276 else: 277 raise TypeError, "Unsupported type %s" % field.__class__.__name__ 278 v.Modify(win32com.client.constants.msiViewModifyInsert, r) 279 r.ClearData() 280 v.Close() 281 282def add_stream(db, name, path): 283 d = MakeInstaller() 284 v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name) 285 r = d.CreateRecord(1) 286 r.SetStream(1, path) 287 v.Execute(r) 288 v.Close() 289 290def init_database(name, schema, 291 ProductName, ProductCode, ProductVersion, 292 Manufacturer, 293 request_uac = False): 294 try: 295 os.unlink(name) 296 except OSError: 297 pass 298 ProductCode = ProductCode.upper() 299 d = MakeInstaller() 300 # Create the database 301 db = d.OpenDatabase(name, 302 win32com.client.constants.msiOpenDatabaseModeCreate) 303 # Create the tables 304 for t in schema.tables: 305 t.create(db) 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()) 315 if request_uac: 316 wc = 2 # long file names, compressed, original media 317 else: 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 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 str = str.replace("+", "_") 342 if str[0] in string.digits: 343 str = "_"+str 344 assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str 345 return str 346 347def gen_uuid(): 348 return str(pythoncom.CreateGuid()) 349 350class CAB: 351 def __init__(self, name): 352 self.name = name 353 self.file = open(name+".txt", "wt") 354 self.filenames = sets.Set() 355 self.index = 0 356 357 def gen_id(self, dir, file): 358 logical = _logical = make_id(file) 359 pos = 1 360 while logical in self.filenames: 361 logical = "%s.%d" % (_logical, pos) 362 pos += 1 363 self.filenames.add(logical) 364 return logical 365 366 def append(self, full, file, logical = None): 367 if os.path.isdir(full): 368 return 369 if not logical: 370 logical = self.gen_id(dir, file) 371 self.index += 1 372 if full.find(" ")!=-1: 373 print >>self.file, '"%s" %s' % (full, logical) 374 else: 375 print >>self.file, '%s %s' % (full, logical) 376 return self.index, logical 377 378 def commit(self, db): 379 self.file.close() 380 try: 381 os.unlink(self.name+".cab") 382 except OSError: 383 pass 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"), 388 ]: 389 try: 390 key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, k) 391 dir = _winreg.QueryValueEx(key, v)[0] 392 _winreg.CloseKey(key) 393 except (WindowsError, IndexError): 394 continue 395 cabarc = os.path.join(dir, r"Bin", "cabarc.exe") 396 if not os.path.exists(cabarc): 397 continue 398 break 399 else: 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(".") 408 else: 409 sys.stdout.write(line) 410 sys.stdout.flush() 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") 418 db.Commit() 419 420_directories = sets.Set() 421class Directory: 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.""" 431 index = 1 432 _logical = make_id(_logical) 433 logical = _logical 434 while logical in _directories: 435 logical = "%s%d" % (_logical, index) 436 index += 1 437 _directories.add(logical) 438 self.db = db 439 self.cab = cab 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() 446 self.keyfiles = {} 447 self.componentflags = componentflags 448 if basedir: 449 self.absolute = os.path.join(basedir.absolute, physical) 450 blogical = basedir.logical 451 else: 452 self.absolute = physical 453 blogical = None 454 add_data(db, "Directory", [(logical, blogical, default)]) 455 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 461 table.""" 462 if flags is None: 463 flags = self.componentflags 464 if uuid is None: 465 uuid = gen_uuid() 466 else: 467 uuid = uuid.upper() 468 if component is None: 469 component = self.logical 470 self.component = component 471 if Win64: 472 flags |= 256 473 if keyfile: 474 keyid = self.cab.gen_id(self.absolute, keyfile) 475 self.keyfiles[keyfile] = keyid 476 else: 477 keyid = None 478 add_data(self.db, "Component", 479 [(component, uuid, self.logical, flags, None, keyid)]) 480 if feature is None: 481 feature = current_feature 482 add_data(self.db, "FeatureComponents", 483 [(feature.id, component)]) 484 485 def make_short(self, file): 486 file = re.sub(r'[\?|><:/*"+,;=\[\]]', '_', file) # restrictions on short names 487 parts = file.split(".") 488 if len(parts)>1: 489 suffix = parts[-1].upper() 490 else: 491 suffix = None 492 prefix = parts[0].upper() 493 if len(prefix) <= 8 and (not suffix or len(suffix)<=3): 494 if suffix: 495 file = prefix+"."+suffix 496 else: 497 file = prefix 498 assert file not in self.short_names 499 else: 500 prefix = prefix[:6] 501 if suffix: 502 suffix = suffix[:3] 503 pos = 1 504 while 1: 505 if suffix: 506 file = "%s~%d.%s" % (prefix, pos, suffix) 507 else: 508 file = "%s~%d" % (prefix, pos) 509 if file not in self.short_names: break 510 pos += 1 511 assert pos < 10000 512 if pos in (10, 100, 1000): 513 prefix = prefix[:-1] 514 self.short_names.add(file) 515 return file 516 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) 525 if not src: 526 # Allow relative paths for file if src is not specified 527 src = file 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] 533 else: 534 logical = None 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 544 attributes = 512 545 add_data(self.db, "File", 546 [(logical, self.component, full, filesize, version, 547 language, attributes, sequence)]) 548 if not version: 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), 561 # self.logical, 2), 562 # (logical+"o", self.component, "%sO|%so" % (short, file), 563 # self.logical, 2)]) 564 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) 569 for f in files: 570 if exclude and f in exclude: continue 571 self.add_file(f) 572 return files 573 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)]) 579 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)]) 583 584 585class Feature: 586 def __init__(self, db, id, title, desc, display, level = 1, 587 parent=None, directory = None, attributes=0): 588 self.id = id 589 if parent: 590 parent = parent.id 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 597 598class Control: 599 def __init__(self, dlg, name): 600 self.dlg = dlg 601 self.name = name 602 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)]) 606 607 def mapping(self, ev, attr): 608 add_data(self.dlg.db, "EventMapping", 609 [(self.dlg.name, self.name, ev, attr)]) 610 611 def condition(self, action, condition): 612 add_data(self.dlg.db, "ControlCondition", 613 [(self.dlg.name, self.name, action, condition)]) 614 615class RadioButtonGroup(Control): 616 def __init__(self, dlg, name, property): 617 self.dlg = dlg 618 self.name = name 619 self.property = property 620 self.index = 1 621 622 def add(self, name, x, y, w, h, text, value = None): 623 if value is None: 624 value = name 625 add_data(self.dlg.db, "RadioButton", 626 [(self.property, self.index, value, 627 x, y, w, h, text, None)]) 628 self.index += 1 629 630class Dialog: 631 def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel): 632 self.db = db 633 self.name = name 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)]) 636 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) 641 642 def text(self, name, x, y, w, h, attr, text): 643 return self.control(name, "Text", x, y, w, h, attr, None, 644 text, None, None) 645 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) 648 649 def line(self, name, x, y, w, h): 650 return self.control(name, "Line", x, y, w, h, 1, None, None, None, None) 651 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) 654 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) 660 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) 663 664def pe_type(path): 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] 670 return machine 671 672def set_arch_from_file(path): 673 global msi_type, Win64, arch_ext 674 machine = pe_type(path) 675 if machine == 0x14c: 676 # i386 677 msi_type = "Intel" 678 Win64 = 0 679 arch_ext = '' 680 elif machine == 0x200: 681 # Itanium 682 msi_type = "Intel64" 683 Win64 = 1 684 arch_ext = '.ia64' 685 elif machine == 0x8664: 686 # AMD64 687 msi_type = "x64" 688 Win64 = 1 689 arch_ext = '.amd64' 690 else: 691 raise ValueError, "Unsupported architecture" 692 msi_type += ";1033" 693