source: python/trunk/Lib/msilib/__init__.py

Last change on this file was 391, checked in by dmik, 11 years ago

python: Merge vendor 2.7.6 to trunk.

  • Property svn:eol-style set to native
File size: 17.2 KB
Line 
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 os, string, re, sys
6
7AMD64 = "AMD64" in sys.version
8Itanium = "Itanium" in sys.version
9Win64 = AMD64 or Itanium
10
11# Partially taken from Wine
12datasizemask= 0x00ff
13type_valid= 0x0100
14type_localizable= 0x0200
15
16typemask= 0x0c00
17type_long= 0x0000
18type_short= 0x0400
19type_string= 0x0c00
20type_binary= 0x0800
21
22type_nullable= 0x1000
23type_key= 0x2000
24# XXX temporary, localizable?
25knownbits = datasizemask | type_valid | type_localizable | \
26 typemask | type_nullable | type_key
27
28class 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
83class _Unspecified:pass
84def 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
96def 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
123def 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
130def 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
171def add_tables(db, module):
172 for table in module.tables:
173 add_data(db, table, getattr(module, table))
174
175def make_id(str):
176 identifier_chars = string.ascii_letters + string.digits + "._"
177 str = "".join([c if c in identifier_chars else "_" for c in str])
178 if str[0] in (string.digits + "."):
179 str = "_" + str
180 assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
181 return str
182
183def gen_uuid():
184 return "{"+UuidCreate().upper()+"}"
185
186class CAB:
187 def __init__(self, name):
188 self.name = name
189 self.files = []
190 self.filenames = set()
191 self.index = 0
192
193 def gen_id(self, file):
194 logical = _logical = make_id(file)
195 pos = 1
196 while logical in self.filenames:
197 logical = "%s.%d" % (_logical, pos)
198 pos += 1
199 self.filenames.add(logical)
200 return logical
201
202 def append(self, full, file, logical):
203 if os.path.isdir(full):
204 return
205 if not logical:
206 logical = self.gen_id(file)
207 self.index += 1
208 self.files.append((full, logical))
209 return self.index, logical
210
211 def commit(self, db):
212 from tempfile import mktemp
213 filename = mktemp()
214 FCICreate(filename, self.files)
215 add_data(db, "Media",
216 [(1, self.index, None, "#"+self.name, None, None)])
217 add_stream(db, self.name, filename)
218 os.unlink(filename)
219 db.Commit()
220
221_directories = set()
222class Directory:
223 def __init__(self, db, cab, basedir, physical, _logical, default, componentflags=None):
224 """Create a new directory in the Directory table. There is a current component
225 at each point in time for the directory, which is either explicitly created
226 through start_component, or implicitly when files are added for the first
227 time. Files are added into the current component, and into the cab file.
228 To create a directory, a base directory object needs to be specified (can be
229 None), the path to the physical directory, and a logical directory name.
230 Default specifies the DefaultDir slot in the directory table. componentflags
231 specifies the default flags that new components get."""
232 index = 1
233 _logical = make_id(_logical)
234 logical = _logical
235 while logical in _directories:
236 logical = "%s%d" % (_logical, index)
237 index += 1
238 _directories.add(logical)
239 self.db = db
240 self.cab = cab
241 self.basedir = basedir
242 self.physical = physical
243 self.logical = logical
244 self.component = None
245 self.short_names = set()
246 self.ids = set()
247 self.keyfiles = {}
248 self.componentflags = componentflags
249 if basedir:
250 self.absolute = os.path.join(basedir.absolute, physical)
251 blogical = basedir.logical
252 else:
253 self.absolute = physical
254 blogical = None
255 add_data(db, "Directory", [(logical, blogical, default)])
256
257 def start_component(self, component = None, feature = None, flags = None, keyfile = None, uuid=None):
258 """Add an entry to the Component table, and make this component the current for this
259 directory. If no component name is given, the directory name is used. If no feature
260 is given, the current feature is used. If no flags are given, the directory's default
261 flags are used. If no keyfile is given, the KeyPath is left null in the Component
262 table."""
263 if flags is None:
264 flags = self.componentflags
265 if uuid is None:
266 uuid = gen_uuid()
267 else:
268 uuid = uuid.upper()
269 if component is None:
270 component = self.logical
271 self.component = component
272 if Win64:
273 flags |= 256
274 if keyfile:
275 keyid = self.cab.gen_id(self.absolute, keyfile)
276 self.keyfiles[keyfile] = keyid
277 else:
278 keyid = None
279 add_data(self.db, "Component",
280 [(component, uuid, self.logical, flags, None, keyid)])
281 if feature is None:
282 feature = current_feature
283 add_data(self.db, "FeatureComponents",
284 [(feature.id, component)])
285
286 def make_short(self, file):
287 oldfile = file
288 file = file.replace('+', '_')
289 file = ''.join(c for c in file if not c in ' "/\[]:;=,')
290 parts = file.split(".")
291 if len(parts) > 1:
292 prefix = "".join(parts[:-1]).upper()
293 suffix = parts[-1].upper()
294 if not prefix:
295 prefix = suffix
296 suffix = None
297 else:
298 prefix = file.upper()
299 suffix = None
300 if len(parts) < 3 and len(prefix) <= 8 and file == oldfile and (
301 not suffix or len(suffix) <= 3):
302 if suffix:
303 file = prefix+"."+suffix
304 else:
305 file = prefix
306 else:
307 file = None
308 if file is None or file in self.short_names:
309 prefix = prefix[:6]
310 if suffix:
311 suffix = suffix[:3]
312 pos = 1
313 while 1:
314 if suffix:
315 file = "%s~%d.%s" % (prefix, pos, suffix)
316 else:
317 file = "%s~%d" % (prefix, pos)
318 if file not in self.short_names: break
319 pos += 1
320 assert pos < 10000
321 if pos in (10, 100, 1000):
322 prefix = prefix[:-1]
323 self.short_names.add(file)
324 assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
325 return file
326
327 def add_file(self, file, src=None, version=None, language=None):
328 """Add a file to the current component of the directory, starting a new one
329 if there is no current component. By default, the file name in the source
330 and the file table will be identical. If the src file is specified, it is
331 interpreted relative to the current directory. Optionally, a version and a
332 language can be specified for the entry in the File table."""
333 if not self.component:
334 self.start_component(self.logical, current_feature, 0)
335 if not src:
336 # Allow relative paths for file if src is not specified
337 src = file
338 file = os.path.basename(file)
339 absolute = os.path.join(self.absolute, src)
340 assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
341 if file in self.keyfiles:
342 logical = self.keyfiles[file]
343 else:
344 logical = None
345 sequence, logical = self.cab.append(absolute, file, logical)
346 assert logical not in self.ids
347 self.ids.add(logical)
348 short = self.make_short(file)
349 full = "%s|%s" % (short, file)
350 filesize = os.stat(absolute).st_size
351 # constants.msidbFileAttributesVital
352 # Compressed omitted, since it is the database default
353 # could add r/o, system, hidden
354 attributes = 512
355 add_data(self.db, "File",
356 [(logical, self.component, full, filesize, version,
357 language, attributes, sequence)])
358 #if not version:
359 # # Add hash if the file is not versioned
360 # filehash = FileHash(absolute, 0)
361 # add_data(self.db, "MsiFileHash",
362 # [(logical, 0, filehash.IntegerData(1),
363 # filehash.IntegerData(2), filehash.IntegerData(3),
364 # filehash.IntegerData(4))])
365 # Automatically remove .pyc/.pyo files on uninstall (2)
366 # XXX: adding so many RemoveFile entries makes installer unbelievably
367 # slow. So instead, we have to use wildcard remove entries
368 if file.endswith(".py"):
369 add_data(self.db, "RemoveFile",
370 [(logical+"c", self.component, "%sC|%sc" % (short, file),
371 self.logical, 2),
372 (logical+"o", self.component, "%sO|%so" % (short, file),
373 self.logical, 2)])
374 return logical
375
376 def glob(self, pattern, exclude = None):
377 """Add a list of files to the current component as specified in the
378 glob pattern. Individual files can be excluded in the exclude list."""
379 files = glob.glob1(self.absolute, pattern)
380 for f in files:
381 if exclude and f in exclude: continue
382 self.add_file(f)
383 return files
384
385 def remove_pyc(self):
386 "Remove .pyc/.pyo files on uninstall"
387 add_data(self.db, "RemoveFile",
388 [(self.component+"c", self.component, "*.pyc", self.logical, 2),
389 (self.component+"o", self.component, "*.pyo", self.logical, 2)])
390
391class Binary:
392 def __init__(self, fname):
393 self.name = fname
394 def __repr__(self):
395 return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
396
397class Feature:
398 def __init__(self, db, id, title, desc, display, level = 1,
399 parent=None, directory = None, attributes=0):
400 self.id = id
401 if parent:
402 parent = parent.id
403 add_data(db, "Feature",
404 [(id, parent, title, desc, display,
405 level, directory, attributes)])
406 def set_current(self):
407 global current_feature
408 current_feature = self
409
410class Control:
411 def __init__(self, dlg, name):
412 self.dlg = dlg
413 self.name = name
414
415 def event(self, event, argument, condition = "1", ordering = None):
416 add_data(self.dlg.db, "ControlEvent",
417 [(self.dlg.name, self.name, event, argument,
418 condition, ordering)])
419
420 def mapping(self, event, attribute):
421 add_data(self.dlg.db, "EventMapping",
422 [(self.dlg.name, self.name, event, attribute)])
423
424 def condition(self, action, condition):
425 add_data(self.dlg.db, "ControlCondition",
426 [(self.dlg.name, self.name, action, condition)])
427
428class RadioButtonGroup(Control):
429 def __init__(self, dlg, name, property):
430 self.dlg = dlg
431 self.name = name
432 self.property = property
433 self.index = 1
434
435 def add(self, name, x, y, w, h, text, value = None):
436 if value is None:
437 value = name
438 add_data(self.dlg.db, "RadioButton",
439 [(self.property, self.index, value,
440 x, y, w, h, text, None)])
441 self.index += 1
442
443class Dialog:
444 def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
445 self.db = db
446 self.name = name
447 self.x, self.y, self.w, self.h = x,y,w,h
448 add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
449
450 def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
451 add_data(self.db, "Control",
452 [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
453 return Control(self, name)
454
455 def text(self, name, x, y, w, h, attr, text):
456 return self.control(name, "Text", x, y, w, h, attr, None,
457 text, None, None)
458
459 def bitmap(self, name, x, y, w, h, text):
460 return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
461
462 def line(self, name, x, y, w, h):
463 return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
464
465 def pushbutton(self, name, x, y, w, h, attr, text, next):
466 return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
467
468 def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
469 add_data(self.db, "Control",
470 [(self.name, name, "RadioButtonGroup",
471 x, y, w, h, attr, prop, text, next, None)])
472 return RadioButtonGroup(self, name, prop)
473
474 def checkbox(self, name, x, y, w, h, attr, prop, text, next):
475 return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)
Note: See TracBrowser for help on using the repository browser.