Forgot to add a `versionadded` tag
[python.git] / Lib / msilib / __init__.py
blob1aa0637ab868ef2f06c2c2eec21ce5267847c2de
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
7 AMD64 = "AMD64" in sys.version
8 Itanium = "Itanium" in sys.version
9 Win64 = AMD64 or Itanium
11 # Partially taken from Wine
12 datasizemask= 0x00ff
13 type_valid= 0x0100
14 type_localizable= 0x0200
16 typemask= 0x0c00
17 type_long= 0x0000
18 type_short= 0x0400
19 type_string= 0x0c00
20 type_binary= 0x0800
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
28 class Table:
29 def __init__(self, name):
30 self.name = name
31 self.fields = []
33 def add_field(self, index, name, type):
34 self.fields.append((index,name,type))
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)
78 def create(self, db):
79 v = db.OpenView(self.sql())
80 v.Execute(None)
81 v.Close()
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"
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)
119 r.ClearData()
120 v.Close()
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()
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
171 def add_tables(db, module):
172 for table in module.tables:
173 add_data(db, table, getattr(module, table))
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
184 def gen_uuid():
185 return "{"+UuidCreate().upper()+"}"
187 class CAB:
188 def __init__(self, name):
189 self.name = name
190 self.files = []
191 self.filenames = set()
192 self.index = 0
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
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
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()
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)])
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)])
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
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
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
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)])
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
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
402 class Control:
403 def __init__(self, dlg, name):
404 self.dlg = dlg
405 self.name = name
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)])
412 def mapping(self, event, attribute):
413 add_data(self.dlg.db, "EventMapping",
414 [(self.dlg.name, self.name, event, attribute)])
416 def condition(self, action, condition):
417 add_data(self.dlg.db, "ControlCondition",
418 [(self.dlg.name, self.name, action, condition)])
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
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
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)])
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)
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)
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)
454 def line(self, name, x, y, w, h):
455 return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
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)
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)
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)