python-msilib 0.4.2__cp314-cp314-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.
- msilib/__init__.py +778 -0
- msilib/_msi.c +1260 -0
- msilib/_msi.cp314-win32.pyd +0 -0
- msilib/include/_msi.h +701 -0
- msilib/include/pythoncapi_compat.h +2666 -0
- msilib/schema.py +5762 -0
- msilib/sequence.py +137 -0
- msilib/text.py +296 -0
- python_msilib-0.4.2.dist-info/METADATA +94 -0
- python_msilib-0.4.2.dist-info/RECORD +13 -0
- python_msilib-0.4.2.dist-info/WHEEL +5 -0
- python_msilib-0.4.2.dist-info/licenses/LICENSE +43 -0
- python_msilib-0.4.2.dist-info/top_level.txt +1 -0
msilib/__init__.py
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"""Read and write Microsoft Installer files."""
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2005 Martin v. Löwis
|
|
4
|
+
# Licensed to PSF under a Contributor Agreement.
|
|
5
|
+
import contextlib
|
|
6
|
+
import fnmatch
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import re
|
|
10
|
+
import string
|
|
11
|
+
from tempfile import mkstemp
|
|
12
|
+
|
|
13
|
+
from msilib._msi import (
|
|
14
|
+
MSICOLINFO_NAMES,
|
|
15
|
+
MSICOLINFO_TYPES,
|
|
16
|
+
MSIDBOPEN_CREATE,
|
|
17
|
+
MSIDBOPEN_CREATEDIRECT,
|
|
18
|
+
MSIDBOPEN_DIRECT,
|
|
19
|
+
MSIDBOPEN_PATCHFILE,
|
|
20
|
+
MSIDBOPEN_READONLY,
|
|
21
|
+
MSIDBOPEN_TRANSACT,
|
|
22
|
+
MSIMODIFY_ASSIGN,
|
|
23
|
+
MSIMODIFY_DELETE,
|
|
24
|
+
MSIMODIFY_INSERT,
|
|
25
|
+
MSIMODIFY_INSERT_TEMPORARY,
|
|
26
|
+
MSIMODIFY_MERGE,
|
|
27
|
+
MSIMODIFY_REFRESH,
|
|
28
|
+
MSIMODIFY_REPLACE,
|
|
29
|
+
MSIMODIFY_SEEK,
|
|
30
|
+
MSIMODIFY_UPDATE,
|
|
31
|
+
MSIMODIFY_VALIDATE,
|
|
32
|
+
MSIMODIFY_VALIDATE_DELETE,
|
|
33
|
+
MSIMODIFY_VALIDATE_FIELD,
|
|
34
|
+
MSIMODIFY_VALIDATE_NEW,
|
|
35
|
+
PID_APPNAME,
|
|
36
|
+
PID_AUTHOR,
|
|
37
|
+
PID_CHARCOUNT,
|
|
38
|
+
PID_CODEPAGE,
|
|
39
|
+
PID_COMMENTS,
|
|
40
|
+
PID_CREATE_DTM,
|
|
41
|
+
PID_KEYWORDS,
|
|
42
|
+
PID_LASTAUTHOR,
|
|
43
|
+
PID_LASTPRINTED,
|
|
44
|
+
PID_LASTSAVE_DTM,
|
|
45
|
+
PID_PAGECOUNT,
|
|
46
|
+
PID_REVNUMBER,
|
|
47
|
+
PID_SECURITY,
|
|
48
|
+
PID_SUBJECT,
|
|
49
|
+
PID_TEMPLATE,
|
|
50
|
+
PID_TITLE,
|
|
51
|
+
PID_WORDCOUNT,
|
|
52
|
+
CreateRecord,
|
|
53
|
+
FCICreate,
|
|
54
|
+
MSIError,
|
|
55
|
+
OpenDatabase,
|
|
56
|
+
UuidCreate,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__version__ = "0.4.2"
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"CAB",
|
|
63
|
+
"MSICOLINFO_NAMES",
|
|
64
|
+
"MSICOLINFO_TYPES",
|
|
65
|
+
"MSIDBOPEN_CREATE",
|
|
66
|
+
"MSIDBOPEN_CREATEDIRECT",
|
|
67
|
+
"MSIDBOPEN_DIRECT",
|
|
68
|
+
"MSIDBOPEN_PATCHFILE",
|
|
69
|
+
"MSIDBOPEN_READONLY",
|
|
70
|
+
"MSIDBOPEN_TRANSACT",
|
|
71
|
+
"MSIMODIFY_ASSIGN",
|
|
72
|
+
"MSIMODIFY_DELETE",
|
|
73
|
+
"MSIMODIFY_INSERT",
|
|
74
|
+
"MSIMODIFY_INSERT_TEMPORARY",
|
|
75
|
+
"MSIMODIFY_MERGE",
|
|
76
|
+
"MSIMODIFY_REFRESH",
|
|
77
|
+
"MSIMODIFY_REPLACE",
|
|
78
|
+
"MSIMODIFY_SEEK",
|
|
79
|
+
"MSIMODIFY_UPDATE",
|
|
80
|
+
"MSIMODIFY_VALIDATE",
|
|
81
|
+
"MSIMODIFY_VALIDATE_DELETE",
|
|
82
|
+
"MSIMODIFY_VALIDATE_FIELD",
|
|
83
|
+
"MSIMODIFY_VALIDATE_NEW",
|
|
84
|
+
"PID_APPNAME",
|
|
85
|
+
"PID_AUTHOR",
|
|
86
|
+
"PID_CHARCOUNT",
|
|
87
|
+
"PID_CODEPAGE",
|
|
88
|
+
"PID_COMMENTS",
|
|
89
|
+
"PID_CREATE_DTM",
|
|
90
|
+
"PID_KEYWORDS",
|
|
91
|
+
"PID_LASTAUTHOR",
|
|
92
|
+
"PID_LASTPRINTED",
|
|
93
|
+
"PID_LASTSAVE_DTM",
|
|
94
|
+
"PID_PAGECOUNT",
|
|
95
|
+
"PID_REVNUMBER",
|
|
96
|
+
"PID_SECURITY",
|
|
97
|
+
"PID_SUBJECT",
|
|
98
|
+
"PID_TEMPLATE",
|
|
99
|
+
"PID_TITLE",
|
|
100
|
+
"PID_WORDCOUNT",
|
|
101
|
+
"Binary",
|
|
102
|
+
"Control",
|
|
103
|
+
"CreateRecord",
|
|
104
|
+
"Dialog",
|
|
105
|
+
"Directory",
|
|
106
|
+
"FCICreate",
|
|
107
|
+
"Feature",
|
|
108
|
+
"MSIError",
|
|
109
|
+
"OpenDatabase",
|
|
110
|
+
"RadioButtonGroup",
|
|
111
|
+
"Table",
|
|
112
|
+
"UuidCreate",
|
|
113
|
+
"add_data",
|
|
114
|
+
"add_stream",
|
|
115
|
+
"add_tables",
|
|
116
|
+
"change_sequence",
|
|
117
|
+
"datasizemask",
|
|
118
|
+
"gen_uuid",
|
|
119
|
+
"init_database",
|
|
120
|
+
"knownbits",
|
|
121
|
+
"make_id",
|
|
122
|
+
"type_binary",
|
|
123
|
+
"type_key",
|
|
124
|
+
"type_localizable",
|
|
125
|
+
"type_long",
|
|
126
|
+
"type_nullable",
|
|
127
|
+
"type_short",
|
|
128
|
+
"type_string",
|
|
129
|
+
"type_valid",
|
|
130
|
+
"typemask",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Should work in windows, but also in mingw, cygwin, ...
|
|
135
|
+
AMD64 = platform.machine() in ("x64", "x86_64", "AMD64")
|
|
136
|
+
ARM64 = platform.machine() in ("aarch64", "arm64", "ARM64")
|
|
137
|
+
|
|
138
|
+
# Keep msilib.Win64 around to preserve backwards compatibility.
|
|
139
|
+
Win64 = AMD64
|
|
140
|
+
|
|
141
|
+
# Partially taken from Wine
|
|
142
|
+
datasizemask = 0x00FF
|
|
143
|
+
type_valid = 0x0100
|
|
144
|
+
type_localizable = 0x0200
|
|
145
|
+
|
|
146
|
+
typemask = 0x0C00
|
|
147
|
+
type_long = 0x0000
|
|
148
|
+
type_short = 0x0400
|
|
149
|
+
type_string = 0x0C00
|
|
150
|
+
type_binary = 0x0800
|
|
151
|
+
|
|
152
|
+
type_nullable = 0x1000
|
|
153
|
+
type_key = 0x2000
|
|
154
|
+
# XXX temporary, localizable?
|
|
155
|
+
knownbits = (
|
|
156
|
+
datasizemask
|
|
157
|
+
| type_valid
|
|
158
|
+
| type_localizable
|
|
159
|
+
| typemask
|
|
160
|
+
| type_nullable
|
|
161
|
+
| type_key
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Table:
|
|
166
|
+
def __init__(self, name) -> None:
|
|
167
|
+
self.name = name
|
|
168
|
+
self.fields = []
|
|
169
|
+
|
|
170
|
+
def add_field(self, index, name, type) -> None:
|
|
171
|
+
self.fields.append((index, name, type))
|
|
172
|
+
|
|
173
|
+
def sql(self) -> str:
|
|
174
|
+
fields = []
|
|
175
|
+
keys = []
|
|
176
|
+
self.fields.sort()
|
|
177
|
+
fields = [None] * len(self.fields)
|
|
178
|
+
for index, name, ftype in self.fields:
|
|
179
|
+
idx = index - 1
|
|
180
|
+
unk = ftype & ~knownbits
|
|
181
|
+
if unk:
|
|
182
|
+
print(f"{self.name}.{name} unknown bits {unk:x}")
|
|
183
|
+
size = ftype & datasizemask
|
|
184
|
+
dtype = ftype & typemask
|
|
185
|
+
if dtype == type_string:
|
|
186
|
+
tname = f"CHAR({size})" if size else "CHAR"
|
|
187
|
+
elif dtype == type_short:
|
|
188
|
+
assert size == 2
|
|
189
|
+
tname = "SHORT"
|
|
190
|
+
elif dtype == type_long:
|
|
191
|
+
assert size == 4
|
|
192
|
+
tname = "LONG"
|
|
193
|
+
elif dtype == type_binary:
|
|
194
|
+
assert size == 0
|
|
195
|
+
tname = "OBJECT"
|
|
196
|
+
else:
|
|
197
|
+
tname = "unknown"
|
|
198
|
+
print(f"{self.name}.{name} unknown integer type {size}")
|
|
199
|
+
flags = "" if ftype & type_nullable else " NOT NULL"
|
|
200
|
+
if ftype & type_localizable:
|
|
201
|
+
flags += " LOCALIZABLE"
|
|
202
|
+
fields[idx] = f"`{name}` {tname}{flags}"
|
|
203
|
+
if ftype & type_key:
|
|
204
|
+
keys.append(f"`{name}`")
|
|
205
|
+
fields = ", ".join(fields)
|
|
206
|
+
keys = ", ".join(keys)
|
|
207
|
+
return f"CREATE TABLE {self.name} ({fields} PRIMARY KEY {keys})"
|
|
208
|
+
|
|
209
|
+
def create(self, db) -> None:
|
|
210
|
+
v = db.OpenView(self.sql())
|
|
211
|
+
v.Execute(None)
|
|
212
|
+
v.Close()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class _Unspecified:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def change_sequence(
|
|
220
|
+
seq, action, seqno=_Unspecified, cond=_Unspecified
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Change the sequence number of an action in a sequence list."""
|
|
223
|
+
for i in range(len(seq)):
|
|
224
|
+
if seq[i][0] == action:
|
|
225
|
+
if cond is _Unspecified:
|
|
226
|
+
cond = seq[i][1]
|
|
227
|
+
if seqno is _Unspecified:
|
|
228
|
+
seqno = seq[i][2]
|
|
229
|
+
seq[i] = (action, cond, seqno)
|
|
230
|
+
return
|
|
231
|
+
msg = "Action not found in sequence"
|
|
232
|
+
raise ValueError(msg)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def add_data(db, table, values) -> None:
|
|
236
|
+
v = db.OpenView(f"SELECT * FROM `{table}`")
|
|
237
|
+
count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
|
|
238
|
+
r = CreateRecord(count)
|
|
239
|
+
for value in values:
|
|
240
|
+
assert len(value) == count, value
|
|
241
|
+
for i in range(count):
|
|
242
|
+
field = value[i]
|
|
243
|
+
if isinstance(field, int):
|
|
244
|
+
r.SetInteger(i + 1, field)
|
|
245
|
+
elif isinstance(field, str):
|
|
246
|
+
r.SetString(i + 1, field)
|
|
247
|
+
elif field is None:
|
|
248
|
+
pass
|
|
249
|
+
elif isinstance(field, Binary):
|
|
250
|
+
r.SetStream(i + 1, field.name)
|
|
251
|
+
else:
|
|
252
|
+
msg = f"Unsupported type {field.__class__.__name__}"
|
|
253
|
+
raise TypeError(msg)
|
|
254
|
+
try:
|
|
255
|
+
v.Modify(MSIMODIFY_INSERT, r)
|
|
256
|
+
except Exception: # noqa: BLE001
|
|
257
|
+
msg = f"Could not insert {values!r} into {table}"
|
|
258
|
+
raise MSIError(msg) from None
|
|
259
|
+
|
|
260
|
+
r.ClearData()
|
|
261
|
+
v.Close()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def add_stream(db, name, path) -> None:
|
|
265
|
+
v = db.OpenView(f"INSERT INTO _Streams (Name, Data) VALUES ('{name}', ?)")
|
|
266
|
+
r = CreateRecord(1)
|
|
267
|
+
r.SetStream(1, path)
|
|
268
|
+
v.Execute(r)
|
|
269
|
+
v.Close()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def init_database(
|
|
273
|
+
name, schema, ProductName, ProductCode, ProductVersion, Manufacturer
|
|
274
|
+
):
|
|
275
|
+
with contextlib.suppress(OSError):
|
|
276
|
+
os.unlink(name)
|
|
277
|
+
ProductCode = ProductCode.upper()
|
|
278
|
+
# Create the database
|
|
279
|
+
db = OpenDatabase(name, MSIDBOPEN_CREATE)
|
|
280
|
+
# Create the tables
|
|
281
|
+
for t in schema.tables:
|
|
282
|
+
t.create(db)
|
|
283
|
+
# Fill the validation table
|
|
284
|
+
add_data(db, "_Validation", schema._Validation_records) # noqa: SLF001
|
|
285
|
+
# Initialize the summary information, allowing at most 20 properties
|
|
286
|
+
si = db.GetSummaryInformation(20)
|
|
287
|
+
si.SetProperty(PID_TITLE, "Installation Database")
|
|
288
|
+
si.SetProperty(PID_SUBJECT, ProductName)
|
|
289
|
+
si.SetProperty(PID_AUTHOR, Manufacturer)
|
|
290
|
+
# https://learn.microsoft.com/en-us/windows/win32/msi/template-summary
|
|
291
|
+
if AMD64:
|
|
292
|
+
si.SetProperty(PID_TEMPLATE, "x64;1033")
|
|
293
|
+
elif ARM64:
|
|
294
|
+
si.SetProperty(PID_TEMPLATE, "Arm64;1033")
|
|
295
|
+
else:
|
|
296
|
+
si.SetProperty(PID_TEMPLATE, "Intel;1033")
|
|
297
|
+
si.SetProperty(PID_REVNUMBER, gen_uuid())
|
|
298
|
+
# https://learn.microsoft.com/en-us/windows/win32/msi/word-count-summary
|
|
299
|
+
# 2 = long file names, compressed, original media
|
|
300
|
+
si.SetProperty(PID_WORDCOUNT, 2)
|
|
301
|
+
# https://learn.microsoft.com/en-us/windows/win32/msi/page-count-summary
|
|
302
|
+
# https://learn.microsoft.com/en-us/windows/win32/msi/using-64-bit-windows-installer-packages
|
|
303
|
+
if ARM64:
|
|
304
|
+
si.SetProperty(PID_PAGECOUNT, 500) # minimum of Windows Installer 5.0
|
|
305
|
+
else:
|
|
306
|
+
si.SetProperty(PID_PAGECOUNT, 200) # minimum of Windows Installer 2.0
|
|
307
|
+
si.SetProperty(PID_APPNAME, "Python MSI Library")
|
|
308
|
+
# XXX more properties
|
|
309
|
+
si.Persist()
|
|
310
|
+
add_data(
|
|
311
|
+
db,
|
|
312
|
+
"Property",
|
|
313
|
+
[
|
|
314
|
+
("ProductName", ProductName),
|
|
315
|
+
("ProductCode", ProductCode),
|
|
316
|
+
("ProductVersion", ProductVersion),
|
|
317
|
+
("Manufacturer", Manufacturer),
|
|
318
|
+
("ProductLanguage", "1033"),
|
|
319
|
+
],
|
|
320
|
+
)
|
|
321
|
+
db.Commit()
|
|
322
|
+
return db
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def add_tables(db, module) -> None:
|
|
326
|
+
for table in module.tables:
|
|
327
|
+
add_data(db, table, getattr(module, table))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def make_id(value: str) -> str:
|
|
331
|
+
identifier_chars = string.ascii_letters + string.digits + "._"
|
|
332
|
+
value = "".join([c if c in identifier_chars else "_" for c in value])
|
|
333
|
+
if value[0] in (string.digits + "."):
|
|
334
|
+
value = "_" + value
|
|
335
|
+
assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", value), "FILE" + value
|
|
336
|
+
return value
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def gen_uuid() -> str:
|
|
340
|
+
return "{" + UuidCreate().upper() + "}"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class CAB:
|
|
344
|
+
def __init__(self, name) -> None:
|
|
345
|
+
self.name = name
|
|
346
|
+
self.files = []
|
|
347
|
+
self.filenames = set()
|
|
348
|
+
self.index = 0
|
|
349
|
+
|
|
350
|
+
def gen_id(self, file) -> str:
|
|
351
|
+
logical = _logical = make_id(file)
|
|
352
|
+
pos = 1
|
|
353
|
+
while logical in self.filenames:
|
|
354
|
+
logical = f"{_logical}.{pos}"
|
|
355
|
+
pos += 1
|
|
356
|
+
self.filenames.add(logical)
|
|
357
|
+
return logical
|
|
358
|
+
|
|
359
|
+
def append(self, full, file, logical) -> tuple[int, str]:
|
|
360
|
+
if os.path.isdir(full):
|
|
361
|
+
return None
|
|
362
|
+
if not logical:
|
|
363
|
+
logical = self.gen_id(file)
|
|
364
|
+
self.index += 1
|
|
365
|
+
self.files.append((full, logical))
|
|
366
|
+
return self.index, logical
|
|
367
|
+
|
|
368
|
+
def commit(self, db) -> None:
|
|
369
|
+
fd, filename = mkstemp()
|
|
370
|
+
os.close(fd)
|
|
371
|
+
FCICreate(filename, self.files)
|
|
372
|
+
add_data(
|
|
373
|
+
db, "Media", [(1, self.index, None, "#" + self.name, None, None)]
|
|
374
|
+
)
|
|
375
|
+
add_stream(db, self.name, filename)
|
|
376
|
+
os.unlink(filename)
|
|
377
|
+
db.Commit()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
_directories = set()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class Directory:
|
|
384
|
+
def __init__(
|
|
385
|
+
self,
|
|
386
|
+
db,
|
|
387
|
+
cab,
|
|
388
|
+
basedir,
|
|
389
|
+
physical,
|
|
390
|
+
_logical,
|
|
391
|
+
default,
|
|
392
|
+
componentflags=None,
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Create a new directory in the Directory table. There is a current
|
|
395
|
+
component at each point in time for the directory, which is either
|
|
396
|
+
explicitly created through start_component, or implicitly when files
|
|
397
|
+
are added for the first time. Files are added into the current
|
|
398
|
+
component, and into the cab file. To create a directory, a base
|
|
399
|
+
directory object needs to be specified (can be None), the path to the
|
|
400
|
+
physical directory, and a logical directory name. Default specifies the
|
|
401
|
+
DefaultDir slot in the directory table. componentflags specifies the
|
|
402
|
+
default flags that new components get.
|
|
403
|
+
"""
|
|
404
|
+
index = 1
|
|
405
|
+
_logical = make_id(_logical)
|
|
406
|
+
logical = _logical
|
|
407
|
+
while logical in _directories:
|
|
408
|
+
logical = f"{_logical}{index}"
|
|
409
|
+
index += 1
|
|
410
|
+
_directories.add(logical)
|
|
411
|
+
self.db = db
|
|
412
|
+
self.cab = cab
|
|
413
|
+
self.basedir = basedir
|
|
414
|
+
self.physical = physical
|
|
415
|
+
self.logical = logical
|
|
416
|
+
self.component = None
|
|
417
|
+
self.short_names = set()
|
|
418
|
+
self.ids = set()
|
|
419
|
+
self.keyfiles = {}
|
|
420
|
+
self.componentflags = componentflags
|
|
421
|
+
if basedir:
|
|
422
|
+
self.absolute = os.path.join(basedir.absolute, physical)
|
|
423
|
+
blogical = basedir.logical
|
|
424
|
+
else:
|
|
425
|
+
self.absolute = physical
|
|
426
|
+
blogical = None
|
|
427
|
+
add_data(db, "Directory", [(logical, blogical, default)])
|
|
428
|
+
|
|
429
|
+
def start_component(
|
|
430
|
+
self, component=None, feature=None, flags=None, keyfile=None, uuid=None
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Add an entry to the Component table, and make this component the
|
|
433
|
+
current for this directory. If no component name is given, the
|
|
434
|
+
directory name is used. If no feature is given, the current feature is
|
|
435
|
+
used. If no flags are given, the directory's default flags are used. If
|
|
436
|
+
no keyfile is given, the KeyPath is left null in the Component table.
|
|
437
|
+
"""
|
|
438
|
+
if flags is None:
|
|
439
|
+
flags = self.componentflags
|
|
440
|
+
uuid = gen_uuid() if uuid is None else uuid.upper()
|
|
441
|
+
if component is None:
|
|
442
|
+
component = self.logical
|
|
443
|
+
self.component = component
|
|
444
|
+
# https://learn.microsoft.com/en-us/windows/win32/msi/component-table
|
|
445
|
+
if AMD64 or ARM64:
|
|
446
|
+
flags |= 256 # msidbComponentAttributes64bit
|
|
447
|
+
if keyfile:
|
|
448
|
+
keyid = self.cab.gen_id(keyfile)
|
|
449
|
+
self.keyfiles[keyfile] = keyid
|
|
450
|
+
else:
|
|
451
|
+
keyid = None
|
|
452
|
+
add_data(
|
|
453
|
+
self.db,
|
|
454
|
+
"Component",
|
|
455
|
+
[(component, uuid, self.logical, flags, None, keyid)],
|
|
456
|
+
)
|
|
457
|
+
if feature is None:
|
|
458
|
+
feature = current_feature
|
|
459
|
+
add_data(self.db, "FeatureComponents", [(feature.id, component)])
|
|
460
|
+
|
|
461
|
+
def make_short(self, file: str) -> str:
|
|
462
|
+
oldfile = file
|
|
463
|
+
file = file.replace("+", "_")
|
|
464
|
+
file = "".join(c for c in file if c not in r' "/\[]:;=,')
|
|
465
|
+
parts = file.split(".")
|
|
466
|
+
if len(parts) > 1:
|
|
467
|
+
prefix = "".join(parts[:-1]).upper()
|
|
468
|
+
suffix = parts[-1].upper()
|
|
469
|
+
if not prefix:
|
|
470
|
+
prefix = suffix
|
|
471
|
+
suffix = None
|
|
472
|
+
else:
|
|
473
|
+
prefix = file.upper()
|
|
474
|
+
suffix = None
|
|
475
|
+
if (
|
|
476
|
+
len(parts) < 3
|
|
477
|
+
and len(prefix) <= 8
|
|
478
|
+
and file == oldfile
|
|
479
|
+
and (not suffix or len(suffix) <= 3)
|
|
480
|
+
):
|
|
481
|
+
file = f"{prefix}.{suffix}" if suffix else prefix
|
|
482
|
+
else:
|
|
483
|
+
file = None
|
|
484
|
+
if file is None or file in self.short_names:
|
|
485
|
+
prefix = prefix[:6]
|
|
486
|
+
if suffix:
|
|
487
|
+
suffix = suffix[:3]
|
|
488
|
+
pos = 1
|
|
489
|
+
while 1:
|
|
490
|
+
if suffix:
|
|
491
|
+
file = f"{prefix}~{pos}.{suffix}"
|
|
492
|
+
else:
|
|
493
|
+
file = f"{prefix}~{pos}"
|
|
494
|
+
if file not in self.short_names:
|
|
495
|
+
break
|
|
496
|
+
pos += 1
|
|
497
|
+
assert pos < 10000
|
|
498
|
+
if pos in (10, 100, 1000):
|
|
499
|
+
prefix = prefix[:-1]
|
|
500
|
+
self.short_names.add(file)
|
|
501
|
+
# restrictions on short names
|
|
502
|
+
assert not re.search(r'[\?|><:/*"+,;=\[\]]', file)
|
|
503
|
+
return file
|
|
504
|
+
|
|
505
|
+
def add_file(
|
|
506
|
+
self,
|
|
507
|
+
file: str,
|
|
508
|
+
src: str | None = None,
|
|
509
|
+
version: str | None = None,
|
|
510
|
+
language: str | None = None,
|
|
511
|
+
) -> str:
|
|
512
|
+
"""Add a file to the current component of the directory, starting a new
|
|
513
|
+
one if there is no current component. By default, the file name in the
|
|
514
|
+
source and the file table will be identical. If the src file is
|
|
515
|
+
specified, it is interpreted relative to the current directory.
|
|
516
|
+
Optionally, a version and a language can be specified for the entry in
|
|
517
|
+
the File table.
|
|
518
|
+
"""
|
|
519
|
+
if not self.component:
|
|
520
|
+
self.start_component(self.logical, current_feature, 0)
|
|
521
|
+
if not src:
|
|
522
|
+
# Allow relative paths for file if src is not specified
|
|
523
|
+
src = file
|
|
524
|
+
file = os.path.basename(file)
|
|
525
|
+
absolute = os.path.join(self.absolute, src)
|
|
526
|
+
# restrictions on long names
|
|
527
|
+
assert not re.search(r'[\?|><:/*]"', file)
|
|
528
|
+
logical = self.keyfiles.get(file, None)
|
|
529
|
+
sequence, logical = self.cab.append(absolute, file, logical)
|
|
530
|
+
assert logical not in self.ids
|
|
531
|
+
self.ids.add(logical)
|
|
532
|
+
short = self.make_short(file)
|
|
533
|
+
full = f"{short}|{file}"
|
|
534
|
+
filesize = os.stat(absolute).st_size
|
|
535
|
+
# constants.msidbFileAttributesVital
|
|
536
|
+
# Compressed omitted, since it is the database default
|
|
537
|
+
# could add r/o, system, hidden
|
|
538
|
+
attributes = 512
|
|
539
|
+
add_data(
|
|
540
|
+
self.db,
|
|
541
|
+
"File",
|
|
542
|
+
[
|
|
543
|
+
(
|
|
544
|
+
logical,
|
|
545
|
+
self.component,
|
|
546
|
+
full,
|
|
547
|
+
filesize,
|
|
548
|
+
version,
|
|
549
|
+
language,
|
|
550
|
+
attributes,
|
|
551
|
+
sequence,
|
|
552
|
+
)
|
|
553
|
+
],
|
|
554
|
+
)
|
|
555
|
+
# if not version:
|
|
556
|
+
# # Add hash if the file is not versioned
|
|
557
|
+
# filehash = FileHash(absolute, 0)
|
|
558
|
+
# add_data(self.db, "MsiFileHash",
|
|
559
|
+
# [(logical, 0, filehash.IntegerData(1),
|
|
560
|
+
# filehash.IntegerData(2), filehash.IntegerData(3),
|
|
561
|
+
# filehash.IntegerData(4))])
|
|
562
|
+
# Automatically remove .pyc files on uninstall (2)
|
|
563
|
+
# XXX: adding so many RemoveFile entries makes installer unbelievably
|
|
564
|
+
# slow. So instead, we have to use wildcard remove entries
|
|
565
|
+
if file.endswith(".py"):
|
|
566
|
+
add_data(
|
|
567
|
+
self.db,
|
|
568
|
+
"RemoveFile",
|
|
569
|
+
[
|
|
570
|
+
(
|
|
571
|
+
logical + "c",
|
|
572
|
+
self.component,
|
|
573
|
+
f"{short}C|{file}c",
|
|
574
|
+
self.logical,
|
|
575
|
+
2,
|
|
576
|
+
),
|
|
577
|
+
(
|
|
578
|
+
logical + "o",
|
|
579
|
+
self.component,
|
|
580
|
+
f"{short}O|{file}o",
|
|
581
|
+
self.logical,
|
|
582
|
+
2,
|
|
583
|
+
),
|
|
584
|
+
],
|
|
585
|
+
)
|
|
586
|
+
return logical
|
|
587
|
+
|
|
588
|
+
def glob(self, pattern: str, exclude=None) -> list[str]:
|
|
589
|
+
"""Add a list of files to the current component as specified in the
|
|
590
|
+
glob pattern. Individual files can be excluded in the exclude list.
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
files = os.listdir(self.absolute)
|
|
594
|
+
except OSError:
|
|
595
|
+
return []
|
|
596
|
+
if pattern[:1] != ".":
|
|
597
|
+
files = (f for f in files if f[0] != ".")
|
|
598
|
+
files = fnmatch.filter(files, pattern)
|
|
599
|
+
for f in files:
|
|
600
|
+
if exclude and f in exclude:
|
|
601
|
+
continue
|
|
602
|
+
self.add_file(f)
|
|
603
|
+
return files
|
|
604
|
+
|
|
605
|
+
def remove_pyc(self) -> None:
|
|
606
|
+
"""Remove .pyc files on uninstall."""
|
|
607
|
+
add_data(
|
|
608
|
+
self.db,
|
|
609
|
+
"RemoveFile",
|
|
610
|
+
[(self.component + "c", self.component, "*.pyc", self.logical, 2)],
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class Binary:
|
|
615
|
+
def __init__(self, fname) -> None:
|
|
616
|
+
self.name = fname
|
|
617
|
+
|
|
618
|
+
def __repr__(self) -> str:
|
|
619
|
+
return f'msilib.Binary(os.path.join(dirname,"{self.name}"))'
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class Feature:
|
|
623
|
+
def __init__(
|
|
624
|
+
self,
|
|
625
|
+
db,
|
|
626
|
+
id,
|
|
627
|
+
title,
|
|
628
|
+
desc,
|
|
629
|
+
display,
|
|
630
|
+
level=1,
|
|
631
|
+
parent=None,
|
|
632
|
+
directory=None,
|
|
633
|
+
attributes=0,
|
|
634
|
+
) -> None:
|
|
635
|
+
self.id = id
|
|
636
|
+
if parent:
|
|
637
|
+
parent = parent.id
|
|
638
|
+
add_data(
|
|
639
|
+
db,
|
|
640
|
+
"Feature",
|
|
641
|
+
[(id, parent, title, desc, display, level, directory, attributes)],
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def set_current(self) -> None:
|
|
645
|
+
global current_feature
|
|
646
|
+
current_feature = self
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class Control:
|
|
650
|
+
def __init__(self, dlg, name) -> None:
|
|
651
|
+
self.dlg = dlg
|
|
652
|
+
self.name = name
|
|
653
|
+
|
|
654
|
+
def event(self, event, argument, condition="1", ordering=None) -> None:
|
|
655
|
+
add_data(
|
|
656
|
+
self.dlg.db,
|
|
657
|
+
"ControlEvent",
|
|
658
|
+
[(self.dlg.name, self.name, event, argument, condition, ordering)],
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
def mapping(self, event, attribute) -> None:
|
|
662
|
+
add_data(
|
|
663
|
+
self.dlg.db,
|
|
664
|
+
"EventMapping",
|
|
665
|
+
[(self.dlg.name, self.name, event, attribute)],
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def condition(self, action, condition) -> None:
|
|
669
|
+
add_data(
|
|
670
|
+
self.dlg.db,
|
|
671
|
+
"ControlCondition",
|
|
672
|
+
[(self.dlg.name, self.name, action, condition)],
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class RadioButtonGroup(Control):
|
|
677
|
+
def __init__(self, dlg, name, property) -> None:
|
|
678
|
+
self.dlg = dlg
|
|
679
|
+
self.name = name
|
|
680
|
+
self.property = property
|
|
681
|
+
self.index = 1
|
|
682
|
+
|
|
683
|
+
def add(self, name, x, y, w, h, text, value=None) -> None:
|
|
684
|
+
if value is None:
|
|
685
|
+
value = name
|
|
686
|
+
add_data(
|
|
687
|
+
self.dlg.db,
|
|
688
|
+
"RadioButton",
|
|
689
|
+
[(self.property, self.index, value, x, y, w, h, text, None)],
|
|
690
|
+
)
|
|
691
|
+
self.index += 1
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class Dialog:
|
|
695
|
+
def __init__(
|
|
696
|
+
self, db, name, x, y, w, h, attr, title, first, default, cancel
|
|
697
|
+
) -> None:
|
|
698
|
+
self.db = db
|
|
699
|
+
self.name = name
|
|
700
|
+
self.x, self.y, self.w, self.h = x, y, w, h
|
|
701
|
+
add_data(
|
|
702
|
+
db,
|
|
703
|
+
"Dialog",
|
|
704
|
+
[(name, x, y, w, h, attr, title, first, default, cancel)],
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
def control(
|
|
708
|
+
self, name, type, x, y, w, h, attr, prop, text, next, help
|
|
709
|
+
) -> Control:
|
|
710
|
+
add_data(
|
|
711
|
+
self.db,
|
|
712
|
+
"Control",
|
|
713
|
+
[
|
|
714
|
+
(
|
|
715
|
+
self.name,
|
|
716
|
+
name,
|
|
717
|
+
type,
|
|
718
|
+
x,
|
|
719
|
+
y,
|
|
720
|
+
w,
|
|
721
|
+
h,
|
|
722
|
+
attr,
|
|
723
|
+
prop,
|
|
724
|
+
text,
|
|
725
|
+
next,
|
|
726
|
+
help,
|
|
727
|
+
)
|
|
728
|
+
],
|
|
729
|
+
)
|
|
730
|
+
return Control(self, name)
|
|
731
|
+
|
|
732
|
+
def text(self, name, x, y, w, h, attr, text) -> Control:
|
|
733
|
+
return self.control(
|
|
734
|
+
name, "Text", x, y, w, h, attr, None, text, None, None
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def bitmap(self, name, x, y, w, h, text) -> Control:
|
|
738
|
+
return self.control(
|
|
739
|
+
name, "Bitmap", x, y, w, h, 1, None, text, None, None
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
def line(self, name, x, y, w, h) -> Control:
|
|
743
|
+
return self.control(
|
|
744
|
+
name, "Line", x, y, w, h, 1, None, None, None, None
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
def pushbutton(self, name, x, y, w, h, attr, text, next) -> Control:
|
|
748
|
+
return self.control(
|
|
749
|
+
name, "PushButton", x, y, w, h, attr, None, text, next, None
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
def radiogroup(self, name, x, y, w, h, attr, prop, text, next) -> Control:
|
|
753
|
+
add_data(
|
|
754
|
+
self.db,
|
|
755
|
+
"Control",
|
|
756
|
+
[
|
|
757
|
+
(
|
|
758
|
+
self.name,
|
|
759
|
+
name,
|
|
760
|
+
"RadioButtonGroup",
|
|
761
|
+
x,
|
|
762
|
+
y,
|
|
763
|
+
w,
|
|
764
|
+
h,
|
|
765
|
+
attr,
|
|
766
|
+
prop,
|
|
767
|
+
text,
|
|
768
|
+
next,
|
|
769
|
+
None,
|
|
770
|
+
)
|
|
771
|
+
],
|
|
772
|
+
)
|
|
773
|
+
return RadioButtonGroup(self, name, prop)
|
|
774
|
+
|
|
775
|
+
def checkbox(self, name, x, y, w, h, attr, prop, text, next) -> Control:
|
|
776
|
+
return self.control(
|
|
777
|
+
name, "CheckBox", x, y, w, h, attr, prop, text, next, None
|
|
778
|
+
)
|