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