python-msilib 0.2.0__cp313-cp313t-win_amd64.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,655 @@
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.2.0"
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 = (
31
+ datasizemask
32
+ | type_valid
33
+ | type_localizable
34
+ | typemask
35
+ | type_nullable
36
+ | type_key
37
+ )
38
+
39
+
40
+ class Table:
41
+ def __init__(self, name):
42
+ self.name = name
43
+ self.fields = []
44
+
45
+ def add_field(self, index, name, type):
46
+ self.fields.append((index, name, type))
47
+
48
+ def sql(self):
49
+ fields = []
50
+ keys = []
51
+ self.fields.sort()
52
+ fields = [None] * len(self.fields)
53
+ for index, name, type in self.fields:
54
+ index -= 1
55
+ unk = type & ~knownbits
56
+ if unk:
57
+ print("%s.%s unknown bits %x" % (self.name, name, unk))
58
+ size = type & datasizemask
59
+ dtype = type & typemask
60
+ if dtype == type_string:
61
+ if size:
62
+ tname = "CHAR(%d)" % size
63
+ else:
64
+ tname = "CHAR"
65
+ elif dtype == type_short:
66
+ assert size == 2
67
+ tname = "SHORT"
68
+ elif dtype == type_long:
69
+ assert size == 4
70
+ tname = "LONG"
71
+ elif dtype == type_binary:
72
+ assert size == 0
73
+ tname = "OBJECT"
74
+ else:
75
+ tname = "unknown"
76
+ print("%s.%sunknown integer type %d" % (self.name, name, size))
77
+ if type & type_nullable:
78
+ flags = ""
79
+ else:
80
+ flags = " NOT NULL"
81
+ if type & type_localizable:
82
+ flags += " LOCALIZABLE"
83
+ fields[index] = "`%s` %s%s" % (name, tname, flags)
84
+ if type & type_key:
85
+ keys.append("`%s`" % name)
86
+ fields = ", ".join(fields)
87
+ keys = ", ".join(keys)
88
+ return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (
89
+ self.name,
90
+ fields,
91
+ keys,
92
+ )
93
+
94
+ def create(self, db):
95
+ v = db.OpenView(self.sql())
96
+ v.Execute(None)
97
+ v.Close()
98
+
99
+
100
+ class _Unspecified:
101
+ pass
102
+
103
+
104
+ def change_sequence(seq, action, seqno=_Unspecified, cond=_Unspecified):
105
+ "Change the sequence number of an action in a sequence list"
106
+ for i in range(len(seq)):
107
+ if seq[i][0] == action:
108
+ if cond is _Unspecified:
109
+ cond = seq[i][1]
110
+ if seqno is _Unspecified:
111
+ seqno = seq[i][2]
112
+ seq[i] = (action, cond, seqno)
113
+ return
114
+ raise ValueError("Action not found in sequence")
115
+
116
+
117
+ def add_data(db, table, values):
118
+ v = db.OpenView("SELECT * FROM `%s`" % table)
119
+ count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
120
+ r = CreateRecord(count)
121
+ for value in values:
122
+ assert len(value) == count, value
123
+ for i in range(count):
124
+ field = value[i]
125
+ if isinstance(field, int):
126
+ r.SetInteger(i + 1, field)
127
+ elif isinstance(field, str):
128
+ r.SetString(i + 1, field)
129
+ elif field is None:
130
+ pass
131
+ elif isinstance(field, Binary):
132
+ r.SetStream(i + 1, field.name)
133
+ else:
134
+ raise TypeError(
135
+ "Unsupported type %s" % field.__class__.__name__
136
+ )
137
+ try:
138
+ v.Modify(MSIMODIFY_INSERT, r)
139
+ except Exception:
140
+ raise MSIError(
141
+ "Could not insert " + repr(values) + " into " + table
142
+ )
143
+
144
+ r.ClearData()
145
+ v.Close()
146
+
147
+
148
+ def add_stream(db, name, path):
149
+ v = db.OpenView(
150
+ "INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name
151
+ )
152
+ r = CreateRecord(1)
153
+ r.SetStream(1, path)
154
+ v.Execute(r)
155
+ v.Close()
156
+
157
+
158
+ def init_database(
159
+ name, schema, ProductName, ProductCode, ProductVersion, Manufacturer
160
+ ):
161
+ try:
162
+ os.unlink(name)
163
+ except OSError:
164
+ pass
165
+ ProductCode = ProductCode.upper()
166
+ # Create the database
167
+ db = OpenDatabase(name, MSIDBOPEN_CREATE)
168
+ # Create the tables
169
+ for t in schema.tables:
170
+ t.create(db)
171
+ # Fill the validation table
172
+ add_data(db, "_Validation", schema._Validation_records)
173
+ # Initialize the summary information, allowing at most 20 properties
174
+ si = db.GetSummaryInformation(20)
175
+ si.SetProperty(PID_TITLE, "Installation Database")
176
+ si.SetProperty(PID_SUBJECT, ProductName)
177
+ si.SetProperty(PID_AUTHOR, Manufacturer)
178
+ if AMD64:
179
+ si.SetProperty(PID_TEMPLATE, "x64;1033")
180
+ else:
181
+ si.SetProperty(PID_TEMPLATE, "Intel;1033")
182
+ si.SetProperty(PID_REVNUMBER, gen_uuid())
183
+ si.SetProperty(
184
+ PID_WORDCOUNT, 2
185
+ ) # long file names, compressed, original media
186
+ si.SetProperty(PID_PAGECOUNT, 200)
187
+ si.SetProperty(PID_APPNAME, "Python MSI Library")
188
+ # XXX more properties
189
+ si.Persist()
190
+ add_data(
191
+ db,
192
+ "Property",
193
+ [
194
+ ("ProductName", ProductName),
195
+ ("ProductCode", ProductCode),
196
+ ("ProductVersion", ProductVersion),
197
+ ("Manufacturer", Manufacturer),
198
+ ("ProductLanguage", "1033"),
199
+ ],
200
+ )
201
+ db.Commit()
202
+ return db
203
+
204
+
205
+ def add_tables(db, module):
206
+ for table in module.tables:
207
+ add_data(db, table, getattr(module, table))
208
+
209
+
210
+ def make_id(str):
211
+ identifier_chars = string.ascii_letters + string.digits + "._"
212
+ str = "".join([c if c in identifier_chars else "_" for c in str])
213
+ if str[0] in (string.digits + "."):
214
+ str = "_" + str
215
+ assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE" + str
216
+ return str
217
+
218
+
219
+ def gen_uuid():
220
+ return "{" + UuidCreate().upper() + "}"
221
+
222
+
223
+ class CAB:
224
+ def __init__(self, name):
225
+ self.name = name
226
+ self.files = []
227
+ self.filenames = set()
228
+ self.index = 0
229
+
230
+ def gen_id(self, file):
231
+ logical = _logical = make_id(file)
232
+ pos = 1
233
+ while logical in self.filenames:
234
+ logical = "%s.%d" % (_logical, pos)
235
+ pos += 1
236
+ self.filenames.add(logical)
237
+ return logical
238
+
239
+ def append(self, full, file, logical):
240
+ if os.path.isdir(full):
241
+ return
242
+ if not logical:
243
+ logical = self.gen_id(file)
244
+ self.index += 1
245
+ self.files.append((full, logical))
246
+ return self.index, logical
247
+
248
+ def commit(self, db):
249
+ from tempfile import mktemp
250
+
251
+ filename = mktemp()
252
+ FCICreate(filename, self.files)
253
+ add_data(
254
+ db, "Media", [(1, self.index, None, "#" + self.name, None, None)]
255
+ )
256
+ add_stream(db, self.name, filename)
257
+ os.unlink(filename)
258
+ db.Commit()
259
+
260
+
261
+ _directories = set()
262
+
263
+
264
+ class Directory:
265
+ def __init__(
266
+ self,
267
+ db,
268
+ cab,
269
+ basedir,
270
+ physical,
271
+ _logical,
272
+ default,
273
+ componentflags=None,
274
+ ):
275
+ """Create a new directory in the Directory table. There is a current component
276
+ at each point in time for the directory, which is either explicitly created
277
+ through start_component, or implicitly when files are added for the first
278
+ time. Files are added into the current component, and into the cab file.
279
+ To create a directory, a base directory object needs to be specified (can be
280
+ None), the path to the physical directory, and a logical directory name.
281
+ Default specifies the DefaultDir slot in the directory table. componentflags
282
+ specifies the default flags that new components get."""
283
+ index = 1
284
+ _logical = make_id(_logical)
285
+ logical = _logical
286
+ while logical in _directories:
287
+ logical = "%s%d" % (_logical, index)
288
+ index += 1
289
+ _directories.add(logical)
290
+ self.db = db
291
+ self.cab = cab
292
+ self.basedir = basedir
293
+ self.physical = physical
294
+ self.logical = logical
295
+ self.component = None
296
+ self.short_names = set()
297
+ self.ids = set()
298
+ self.keyfiles = {}
299
+ self.componentflags = componentflags
300
+ if basedir:
301
+ self.absolute = os.path.join(basedir.absolute, physical)
302
+ blogical = basedir.logical
303
+ else:
304
+ self.absolute = physical
305
+ blogical = None
306
+ add_data(db, "Directory", [(logical, blogical, default)])
307
+
308
+ def start_component(
309
+ self, component=None, feature=None, flags=None, keyfile=None, uuid=None
310
+ ):
311
+ """Add an entry to the Component table, and make this component the current for this
312
+ directory. If no component name is given, the directory name is used. If no feature
313
+ is given, the current feature is used. If no flags are given, the directory's default
314
+ flags are used. If no keyfile is given, the KeyPath is left null in the Component
315
+ table."""
316
+ if flags is None:
317
+ flags = self.componentflags
318
+ if uuid is None:
319
+ uuid = gen_uuid()
320
+ else:
321
+ uuid = uuid.upper()
322
+ if component is None:
323
+ component = self.logical
324
+ self.component = component
325
+ if AMD64:
326
+ flags |= 256
327
+ if keyfile:
328
+ keyid = self.cab.gen_id(keyfile)
329
+ self.keyfiles[keyfile] = keyid
330
+ else:
331
+ keyid = None
332
+ add_data(
333
+ self.db,
334
+ "Component",
335
+ [(component, uuid, self.logical, flags, None, keyid)],
336
+ )
337
+ if feature is None:
338
+ feature = current_feature
339
+ add_data(self.db, "FeatureComponents", [(feature.id, component)])
340
+
341
+ def make_short(self, file):
342
+ oldfile = file
343
+ file = file.replace("+", "_")
344
+ file = "".join(c for c in file if not c in r' "/\[]:;=,')
345
+ parts = file.split(".")
346
+ if len(parts) > 1:
347
+ prefix = "".join(parts[:-1]).upper()
348
+ suffix = parts[-1].upper()
349
+ if not prefix:
350
+ prefix = suffix
351
+ suffix = None
352
+ else:
353
+ prefix = file.upper()
354
+ suffix = None
355
+ if (
356
+ len(parts) < 3
357
+ and len(prefix) <= 8
358
+ and file == oldfile
359
+ and (not suffix or len(suffix) <= 3)
360
+ ):
361
+ if suffix:
362
+ file = prefix + "." + suffix
363
+ else:
364
+ file = prefix
365
+ else:
366
+ file = None
367
+ if file is None or file in self.short_names:
368
+ prefix = prefix[:6]
369
+ if suffix:
370
+ suffix = suffix[:3]
371
+ pos = 1
372
+ while 1:
373
+ if suffix:
374
+ file = "%s~%d.%s" % (prefix, pos, suffix)
375
+ else:
376
+ file = "%s~%d" % (prefix, pos)
377
+ if file not in self.short_names:
378
+ break
379
+ pos += 1
380
+ assert pos < 10000
381
+ if pos in (10, 100, 1000):
382
+ prefix = prefix[:-1]
383
+ self.short_names.add(file)
384
+ assert not re.search(
385
+ r'[\?|><:/*"+,;=\[\]]', file
386
+ ) # restrictions on short names
387
+ return file
388
+
389
+ def add_file(self, file, src=None, version=None, language=None):
390
+ """Add a file to the current component of the directory, starting a new one
391
+ if there is no current component. By default, the file name in the source
392
+ and the file table will be identical. If the src file is specified, it is
393
+ interpreted relative to the current directory. Optionally, a version and a
394
+ language can be specified for the entry in the File table."""
395
+ if not self.component:
396
+ self.start_component(self.logical, current_feature, 0)
397
+ if not src:
398
+ # Allow relative paths for file if src is not specified
399
+ src = file
400
+ file = os.path.basename(file)
401
+ absolute = os.path.join(self.absolute, src)
402
+ assert not re.search(
403
+ r'[\?|><:/*]"', file
404
+ ) # restrictions on long names
405
+ if file in self.keyfiles:
406
+ logical = self.keyfiles[file]
407
+ else:
408
+ logical = None
409
+ sequence, logical = self.cab.append(absolute, file, logical)
410
+ assert logical not in self.ids
411
+ self.ids.add(logical)
412
+ short = self.make_short(file)
413
+ full = "%s|%s" % (short, file)
414
+ filesize = os.stat(absolute).st_size
415
+ # constants.msidbFileAttributesVital
416
+ # Compressed omitted, since it is the database default
417
+ # could add r/o, system, hidden
418
+ attributes = 512
419
+ add_data(
420
+ self.db,
421
+ "File",
422
+ [
423
+ (
424
+ logical,
425
+ self.component,
426
+ full,
427
+ filesize,
428
+ version,
429
+ language,
430
+ attributes,
431
+ sequence,
432
+ )
433
+ ],
434
+ )
435
+ # if not version:
436
+ # # Add hash if the file is not versioned
437
+ # filehash = FileHash(absolute, 0)
438
+ # add_data(self.db, "MsiFileHash",
439
+ # [(logical, 0, filehash.IntegerData(1),
440
+ # filehash.IntegerData(2), filehash.IntegerData(3),
441
+ # filehash.IntegerData(4))])
442
+ # Automatically remove .pyc files on uninstall (2)
443
+ # XXX: adding so many RemoveFile entries makes installer unbelievably
444
+ # slow. So instead, we have to use wildcard remove entries
445
+ if file.endswith(".py"):
446
+ add_data(
447
+ self.db,
448
+ "RemoveFile",
449
+ [
450
+ (
451
+ logical + "c",
452
+ self.component,
453
+ "%sC|%sc" % (short, file),
454
+ self.logical,
455
+ 2,
456
+ ),
457
+ (
458
+ logical + "o",
459
+ self.component,
460
+ "%sO|%so" % (short, file),
461
+ self.logical,
462
+ 2,
463
+ ),
464
+ ],
465
+ )
466
+ return logical
467
+
468
+ def glob(self, pattern, exclude=None):
469
+ """Add a list of files to the current component as specified in the
470
+ glob pattern. Individual files can be excluded in the exclude list."""
471
+ try:
472
+ files = os.listdir(self.absolute)
473
+ except OSError:
474
+ return []
475
+ if pattern[:1] != ".":
476
+ files = (f for f in files if f[0] != ".")
477
+ files = fnmatch.filter(files, pattern)
478
+ for f in files:
479
+ if exclude and f in exclude:
480
+ continue
481
+ self.add_file(f)
482
+ return files
483
+
484
+ def remove_pyc(self):
485
+ "Remove .pyc files on uninstall"
486
+ add_data(
487
+ self.db,
488
+ "RemoveFile",
489
+ [(self.component + "c", self.component, "*.pyc", self.logical, 2)],
490
+ )
491
+
492
+
493
+ class Binary:
494
+ def __init__(self, fname):
495
+ self.name = fname
496
+
497
+ def __repr__(self):
498
+ return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
499
+
500
+
501
+ class Feature:
502
+ def __init__(
503
+ self,
504
+ db,
505
+ id,
506
+ title,
507
+ desc,
508
+ display,
509
+ level=1,
510
+ parent=None,
511
+ directory=None,
512
+ attributes=0,
513
+ ):
514
+ self.id = id
515
+ if parent:
516
+ parent = parent.id
517
+ add_data(
518
+ db,
519
+ "Feature",
520
+ [(id, parent, title, desc, display, level, directory, attributes)],
521
+ )
522
+
523
+ def set_current(self):
524
+ global current_feature
525
+ current_feature = self
526
+
527
+
528
+ class Control:
529
+ def __init__(self, dlg, name):
530
+ self.dlg = dlg
531
+ self.name = name
532
+
533
+ def event(self, event, argument, condition="1", ordering=None):
534
+ add_data(
535
+ self.dlg.db,
536
+ "ControlEvent",
537
+ [(self.dlg.name, self.name, event, argument, condition, ordering)],
538
+ )
539
+
540
+ def mapping(self, event, attribute):
541
+ add_data(
542
+ self.dlg.db,
543
+ "EventMapping",
544
+ [(self.dlg.name, self.name, event, attribute)],
545
+ )
546
+
547
+ def condition(self, action, condition):
548
+ add_data(
549
+ self.dlg.db,
550
+ "ControlCondition",
551
+ [(self.dlg.name, self.name, action, condition)],
552
+ )
553
+
554
+
555
+ class RadioButtonGroup(Control):
556
+ def __init__(self, dlg, name, property):
557
+ self.dlg = dlg
558
+ self.name = name
559
+ self.property = property
560
+ self.index = 1
561
+
562
+ def add(self, name, x, y, w, h, text, value=None):
563
+ if value is None:
564
+ value = name
565
+ add_data(
566
+ self.dlg.db,
567
+ "RadioButton",
568
+ [(self.property, self.index, value, x, y, w, h, text, None)],
569
+ )
570
+ self.index += 1
571
+
572
+
573
+ class Dialog:
574
+ def __init__(
575
+ self, db, name, x, y, w, h, attr, title, first, default, cancel
576
+ ):
577
+ self.db = db
578
+ self.name = name
579
+ self.x, self.y, self.w, self.h = x, y, w, h
580
+ add_data(
581
+ db,
582
+ "Dialog",
583
+ [(name, x, y, w, h, attr, title, first, default, cancel)],
584
+ )
585
+
586
+ def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
587
+ add_data(
588
+ self.db,
589
+ "Control",
590
+ [
591
+ (
592
+ self.name,
593
+ name,
594
+ type,
595
+ x,
596
+ y,
597
+ w,
598
+ h,
599
+ attr,
600
+ prop,
601
+ text,
602
+ next,
603
+ help,
604
+ )
605
+ ],
606
+ )
607
+ return Control(self, name)
608
+
609
+ def text(self, name, x, y, w, h, attr, text):
610
+ return self.control(
611
+ name, "Text", x, y, w, h, attr, None, text, None, None
612
+ )
613
+
614
+ def bitmap(self, name, x, y, w, h, text):
615
+ return self.control(
616
+ name, "Bitmap", x, y, w, h, 1, None, text, None, None
617
+ )
618
+
619
+ def line(self, name, x, y, w, h):
620
+ return self.control(
621
+ name, "Line", x, y, w, h, 1, None, None, None, None
622
+ )
623
+
624
+ def pushbutton(self, name, x, y, w, h, attr, text, next):
625
+ return self.control(
626
+ name, "PushButton", x, y, w, h, attr, None, text, next, None
627
+ )
628
+
629
+ def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
630
+ add_data(
631
+ self.db,
632
+ "Control",
633
+ [
634
+ (
635
+ self.name,
636
+ name,
637
+ "RadioButtonGroup",
638
+ x,
639
+ y,
640
+ w,
641
+ h,
642
+ attr,
643
+ prop,
644
+ text,
645
+ next,
646
+ None,
647
+ )
648
+ ],
649
+ )
650
+ return RadioButtonGroup(self, name, prop)
651
+
652
+ def checkbox(self, name, x, y, w, h, attr, prop, text, next):
653
+ return self.control(
654
+ name, "CheckBox", x, y, w, h, attr, prop, text, next, None
655
+ )