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