python-msilib 0.1.1__cp313-cp313-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of python-msilib might be problematic. Click here for more details.

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