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