• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: iso-8859-1 -*-
2# Copyright (C) 2005 Martin v. L�wis
3# Licensed to PSF under a Contributor Agreement.
4from _msi import *
5import glob
6import os
7import re
8import string
9import sys
10
11AMD64 = "AMD64" in sys.version
12Itanium = "Itanium" in sys.version
13Win64 = AMD64 or Itanium
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
32class Table:
33    def __init__(self, name):
34        self.name = name
35        self.fields = []
36
37    def add_field(self, index, name, type):
38        self.fields.append((index,name,type))
39
40    def sql(self):
41        fields = []
42        keys = []
43        self.fields.sort()
44        fields = [None]*len(self.fields)
45        for index, name, type in self.fields:
46            index -= 1
47            unk = type & ~knownbits
48            if unk:
49                print "%s.%s unknown bits %x" % (self.name, name, unk)
50            size = type & datasizemask
51            dtype = type & typemask
52            if dtype == type_string:
53                if size:
54                    tname="CHAR(%d)" % size
55                else:
56                    tname="CHAR"
57            elif dtype == type_short:
58                assert size==2
59                tname = "SHORT"
60            elif dtype == type_long:
61                assert size==4
62                tname="LONG"
63            elif dtype == type_binary:
64                assert size==0
65                tname="OBJECT"
66            else:
67                tname="unknown"
68                print "%s.%sunknown integer type %d" % (self.name, name, size)
69            if type & type_nullable:
70                flags = ""
71            else:
72                flags = " NOT NULL"
73            if type & type_localizable:
74                flags += " LOCALIZABLE"
75            fields[index] = "`%s` %s%s" % (name, tname, flags)
76            if type & type_key:
77                keys.append("`%s`" % name)
78        fields = ", ".join(fields)
79        keys = ", ".join(keys)
80        return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self.name, fields, keys)
81
82    def create(self, db):
83        v = db.OpenView(self.sql())
84        v.Execute(None)
85        v.Close()
86
87class _Unspecified:pass
88def change_sequence(seq, action, seqno=_Unspecified, cond = _Unspecified):
89    "Change the sequence number of an action in a sequence list"
90    for i in range(len(seq)):
91        if seq[i][0] == action:
92            if cond is _Unspecified:
93                cond = seq[i][1]
94            if seqno is _Unspecified:
95                seqno = seq[i][2]
96            seq[i] = (action, cond, seqno)
97            return
98    raise ValueError, "Action not found in sequence"
99
100def add_data(db, table, values):
101    v = db.OpenView("SELECT * FROM `%s`" % table)
102    count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
103    r = CreateRecord(count)
104    for value in values:
105        assert len(value) == count, value
106        for i in range(count):
107            field = value[i]
108            if isinstance(field, (int, long)):
109                r.SetInteger(i+1,field)
110            elif isinstance(field, basestring):
111                r.SetString(i+1,field)
112            elif field is None:
113                pass
114            elif isinstance(field, Binary):
115                r.SetStream(i+1, field.name)
116            else:
117                raise TypeError, "Unsupported type %s" % field.__class__.__name__
118        try:
119            v.Modify(MSIMODIFY_INSERT, r)
120        except Exception, e:
121            raise MSIError("Could not insert "+repr(values)+" into "+table)
122
123        r.ClearData()
124    v.Close()
125
126
127def add_stream(db, name, path):
128    v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name)
129    r = CreateRecord(1)
130    r.SetStream(1, path)
131    v.Execute(r)
132    v.Close()
133
134def init_database(name, schema,
135                  ProductName, ProductCode, ProductVersion,
136                  Manufacturer):
137    try:
138        os.unlink(name)
139    except OSError:
140        pass
141    ProductCode = ProductCode.upper()
142    # Create the database
143    db = OpenDatabase(name, MSIDBOPEN_CREATE)
144    # Create the tables
145    for t in schema.tables:
146        t.create(db)
147    # Fill the validation table
148    add_data(db, "_Validation", schema._Validation_records)
149    # Initialize the summary information, allowing atmost 20 properties
150    si = db.GetSummaryInformation(20)
151    si.SetProperty(PID_TITLE, "Installation Database")
152    si.SetProperty(PID_SUBJECT, ProductName)
153    si.SetProperty(PID_AUTHOR, Manufacturer)
154    if Itanium:
155        si.SetProperty(PID_TEMPLATE, "Intel64;1033")
156    elif AMD64:
157        si.SetProperty(PID_TEMPLATE, "x64;1033")
158    else:
159        si.SetProperty(PID_TEMPLATE, "Intel;1033")
160    si.SetProperty(PID_REVNUMBER, gen_uuid())
161    si.SetProperty(PID_WORDCOUNT, 2) # long file names, compressed, original media
162    si.SetProperty(PID_PAGECOUNT, 200)
163    si.SetProperty(PID_APPNAME, "Python MSI Library")
164    # XXX more properties
165    si.Persist()
166    add_data(db, "Property", [
167        ("ProductName", ProductName),
168        ("ProductCode", ProductCode),
169        ("ProductVersion", ProductVersion),
170        ("Manufacturer", Manufacturer),
171        ("ProductLanguage", "1033")])
172    db.Commit()
173    return db
174
175def add_tables(db, module):
176    for table in module.tables:
177        add_data(db, table, getattr(module, table))
178
179def make_id(str):
180    identifier_chars = string.ascii_letters + string.digits + "._"
181    str = "".join([c if c in identifier_chars else "_" for c in str])
182    if str[0] in (string.digits + "."):
183        str = "_" + str
184    assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
185    return str
186
187def gen_uuid():
188    return "{"+UuidCreate().upper()+"}"
189
190class CAB:
191    def __init__(self, name):
192        self.name = name
193        self.files = []
194        self.filenames = set()
195        self.index = 0
196
197    def gen_id(self, file):
198        logical = _logical = make_id(file)
199        pos = 1
200        while logical in self.filenames:
201            logical = "%s.%d" % (_logical, pos)
202            pos += 1
203        self.filenames.add(logical)
204        return logical
205
206    def append(self, full, file, logical):
207        if os.path.isdir(full):
208            return
209        if not logical:
210            logical = self.gen_id(file)
211        self.index += 1
212        self.files.append((full, logical))
213        return self.index, logical
214
215    def commit(self, db):
216        from tempfile import mktemp
217        filename = mktemp()
218        FCICreate(filename, self.files)
219        add_data(db, "Media",
220                [(1, self.index, None, "#"+self.name, None, None)])
221        add_stream(db, self.name, filename)
222        os.unlink(filename)
223        db.Commit()
224
225_directories = set()
226class Directory:
227    def __init__(self, db, cab, basedir, physical, _logical, default, componentflags=None):
228        """Create a new directory in the Directory table. There is a current component
229        at each point in time for the directory, which is either explicitly created
230        through start_component, or implicitly when files are added for the first
231        time. Files are added into the current component, and into the cab file.
232        To create a directory, a base directory object needs to be specified (can be
233        None), the path to the physical directory, and a logical directory name.
234        Default specifies the DefaultDir slot in the directory table. componentflags
235        specifies the default flags that new components get."""
236        index = 1
237        _logical = make_id(_logical)
238        logical = _logical
239        while logical in _directories:
240            logical = "%s%d" % (_logical, index)
241            index += 1
242        _directories.add(logical)
243        self.db = db
244        self.cab = cab
245        self.basedir = basedir
246        self.physical = physical
247        self.logical = logical
248        self.component = None
249        self.short_names = set()
250        self.ids = set()
251        self.keyfiles = {}
252        self.componentflags = componentflags
253        if basedir:
254            self.absolute = os.path.join(basedir.absolute, physical)
255            blogical = basedir.logical
256        else:
257            self.absolute = physical
258            blogical = None
259        add_data(db, "Directory", [(logical, blogical, default)])
260
261    def start_component(self, component = None, feature = None, flags = None, keyfile = None, uuid=None):
262        """Add an entry to the Component table, and make this component the current for this
263        directory. If no component name is given, the directory name is used. If no feature
264        is given, the current feature is used. If no flags are given, the directory's default
265        flags are used. If no keyfile is given, the KeyPath is left null in the Component
266        table."""
267        if flags is None:
268            flags = self.componentflags
269        if uuid is None:
270            uuid = gen_uuid()
271        else:
272            uuid = uuid.upper()
273        if component is None:
274            component = self.logical
275        self.component = component
276        if Win64:
277            flags |= 256
278        if keyfile:
279            keyid = self.cab.gen_id(self.absolute, keyfile)
280            self.keyfiles[keyfile] = keyid
281        else:
282            keyid = None
283        add_data(self.db, "Component",
284                        [(component, uuid, self.logical, flags, None, keyid)])
285        if feature is None:
286            feature = current_feature
287        add_data(self.db, "FeatureComponents",
288                        [(feature.id, component)])
289
290    def make_short(self, file):
291        oldfile = file
292        file = file.replace('+', '_')
293        file = ''.join(c for c in file if not c in ' "/\[]:;=,')
294        parts = file.split(".")
295        if len(parts) > 1:
296            prefix = "".join(parts[:-1]).upper()
297            suffix = parts[-1].upper()
298            if not prefix:
299                prefix = suffix
300                suffix = None
301        else:
302            prefix = file.upper()
303            suffix = None
304        if len(parts) < 3 and len(prefix) <= 8 and file == oldfile and (
305                                                not suffix or len(suffix) <= 3):
306            if suffix:
307                file = prefix+"."+suffix
308            else:
309                file = prefix
310        else:
311            file = None
312        if file is None or file in self.short_names:
313            prefix = prefix[:6]
314            if suffix:
315                suffix = suffix[:3]
316            pos = 1
317            while 1:
318                if suffix:
319                    file = "%s~%d.%s" % (prefix, pos, suffix)
320                else:
321                    file = "%s~%d" % (prefix, pos)
322                if file not in self.short_names: break
323                pos += 1
324                assert pos < 10000
325                if pos in (10, 100, 1000):
326                    prefix = prefix[:-1]
327        self.short_names.add(file)
328        assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
329        return file
330
331    def add_file(self, file, src=None, version=None, language=None):
332        """Add a file to the current component of the directory, starting a new one
333        if there is no current component. By default, the file name in the source
334        and the file table will be identical. If the src file is specified, it is
335        interpreted relative to the current directory. Optionally, a version and a
336        language can be specified for the entry in the File table."""
337        if not self.component:
338            self.start_component(self.logical, current_feature, 0)
339        if not src:
340            # Allow relative paths for file if src is not specified
341            src = file
342            file = os.path.basename(file)
343        absolute = os.path.join(self.absolute, src)
344        assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
345        if file in self.keyfiles:
346            logical = self.keyfiles[file]
347        else:
348            logical = None
349        sequence, logical = self.cab.append(absolute, file, logical)
350        assert logical not in self.ids
351        self.ids.add(logical)
352        short = self.make_short(file)
353        full = "%s|%s" % (short, file)
354        filesize = os.stat(absolute).st_size
355        # constants.msidbFileAttributesVital
356        # Compressed omitted, since it is the database default
357        # could add r/o, system, hidden
358        attributes = 512
359        add_data(self.db, "File",
360                        [(logical, self.component, full, filesize, version,
361                         language, attributes, sequence)])
362        #if not version:
363        #    # Add hash if the file is not versioned
364        #    filehash = FileHash(absolute, 0)
365        #    add_data(self.db, "MsiFileHash",
366        #             [(logical, 0, filehash.IntegerData(1),
367        #               filehash.IntegerData(2), filehash.IntegerData(3),
368        #               filehash.IntegerData(4))])
369        # Automatically remove .pyc/.pyo files on uninstall (2)
370        # XXX: adding so many RemoveFile entries makes installer unbelievably
371        # slow. So instead, we have to use wildcard remove entries
372        if file.endswith(".py"):
373            add_data(self.db, "RemoveFile",
374                      [(logical+"c", self.component, "%sC|%sc" % (short, file),
375                        self.logical, 2),
376                       (logical+"o", self.component, "%sO|%so" % (short, file),
377                        self.logical, 2)])
378        return logical
379
380    def glob(self, pattern, exclude = None):
381        """Add a list of files to the current component as specified in the
382        glob pattern. Individual files can be excluded in the exclude list."""
383        files = glob.glob1(self.absolute, pattern)
384        for f in files:
385            if exclude and f in exclude: continue
386            self.add_file(f)
387        return files
388
389    def remove_pyc(self):
390        "Remove .pyc/.pyo files on uninstall"
391        add_data(self.db, "RemoveFile",
392                 [(self.component+"c", self.component, "*.pyc", self.logical, 2),
393                  (self.component+"o", self.component, "*.pyo", self.logical, 2)])
394
395class Binary:
396    def __init__(self, fname):
397        self.name = fname
398    def __repr__(self):
399        return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
400
401class Feature:
402    def __init__(self, db, id, title, desc, display, level = 1,
403                 parent=None, directory = None, attributes=0):
404        self.id = id
405        if parent:
406            parent = parent.id
407        add_data(db, "Feature",
408                        [(id, parent, title, desc, display,
409                          level, directory, attributes)])
410    def set_current(self):
411        global current_feature
412        current_feature = self
413
414class Control:
415    def __init__(self, dlg, name):
416        self.dlg = dlg
417        self.name = name
418
419    def event(self, event, argument, condition = "1", ordering = None):
420        add_data(self.dlg.db, "ControlEvent",
421                 [(self.dlg.name, self.name, event, argument,
422                   condition, ordering)])
423
424    def mapping(self, event, attribute):
425        add_data(self.dlg.db, "EventMapping",
426                 [(self.dlg.name, self.name, event, attribute)])
427
428    def condition(self, action, condition):
429        add_data(self.dlg.db, "ControlCondition",
430                 [(self.dlg.name, self.name, action, condition)])
431
432class RadioButtonGroup(Control):
433    def __init__(self, dlg, name, property):
434        self.dlg = dlg
435        self.name = name
436        self.property = property
437        self.index = 1
438
439    def add(self, name, x, y, w, h, text, value = None):
440        if value is None:
441            value = name
442        add_data(self.dlg.db, "RadioButton",
443                 [(self.property, self.index, value,
444                   x, y, w, h, text, None)])
445        self.index += 1
446
447class Dialog:
448    def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
449        self.db = db
450        self.name = name
451        self.x, self.y, self.w, self.h = x,y,w,h
452        add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
453
454    def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
455        add_data(self.db, "Control",
456                 [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
457        return Control(self, name)
458
459    def text(self, name, x, y, w, h, attr, text):
460        return self.control(name, "Text", x, y, w, h, attr, None,
461                     text, None, None)
462
463    def bitmap(self, name, x, y, w, h, text):
464        return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
465
466    def line(self, name, x, y, w, h):
467        return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
468
469    def pushbutton(self, name, x, y, w, h, attr, text, next):
470        return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
471
472    def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
473        add_data(self.db, "Control",
474                 [(self.name, name, "RadioButtonGroup",
475                   x, y, w, h, attr, prop, text, next, None)])
476        return RadioButtonGroup(self, name, prop)
477
478    def checkbox(self, name, x, y, w, h, attr, prop, text, next):
479        return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)
480