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