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