mirror of
https://github.com/python/cpython.git
synced 2025-07-24 03:35:53 +00:00

Capturing exceptions into names can lead to reference cycles though the __traceback__ attribute of the exceptions in some obscure cases that have been reported previously and fixed individually. As these variables are not used anyway, we can remove the binding to reduce the chances of creating reference cycles. See for example GH-13135
481 lines
17 KiB
Python
481 lines
17 KiB
Python
# Copyright (C) 2005 Martin v. Löwis
|
|
# Licensed to PSF under a Contributor Agreement.
|
|
from _msi import *
|
|
import fnmatch
|
|
import os
|
|
import re
|
|
import string
|
|
import sys
|
|
|
|
AMD64 = "AMD64" in sys.version
|
|
# Keep msilib.Win64 around to preserve backwards compatibility.
|
|
Win64 = AMD64
|
|
|
|
# Partially taken from Wine
|
|
datasizemask= 0x00ff
|
|
type_valid= 0x0100
|
|
type_localizable= 0x0200
|
|
|
|
typemask= 0x0c00
|
|
type_long= 0x0000
|
|
type_short= 0x0400
|
|
type_string= 0x0c00
|
|
type_binary= 0x0800
|
|
|
|
type_nullable= 0x1000
|
|
type_key= 0x2000
|
|
# XXX temporary, localizable?
|
|
knownbits = datasizemask | type_valid | type_localizable | \
|
|
typemask | type_nullable | type_key
|
|
|
|
class Table:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.fields = []
|
|
|
|
def add_field(self, index, name, type):
|
|
self.fields.append((index,name,type))
|
|
|
|
def sql(self):
|
|
fields = []
|
|
keys = []
|
|
self.fields.sort()
|
|
fields = [None]*len(self.fields)
|
|
for index, name, type in self.fields:
|
|
index -= 1
|
|
unk = type & ~knownbits
|
|
if unk:
|
|
print("%s.%s unknown bits %x" % (self.name, name, unk))
|
|
size = type & datasizemask
|
|
dtype = type & typemask
|
|
if dtype == type_string:
|
|
if size:
|
|
tname="CHAR(%d)" % size
|
|
else:
|
|
tname="CHAR"
|
|
elif dtype == type_short:
|
|
assert size==2
|
|
tname = "SHORT"
|
|
elif dtype == type_long:
|
|
assert size==4
|
|
tname="LONG"
|
|
elif dtype == type_binary:
|
|
assert size==0
|
|
tname="OBJECT"
|
|
else:
|
|
tname="unknown"
|
|
print("%s.%sunknown integer type %d" % (self.name, name, size))
|
|
if type & type_nullable:
|
|
flags = ""
|
|
else:
|
|
flags = " NOT NULL"
|
|
if type & type_localizable:
|
|
flags += " LOCALIZABLE"
|
|
fields[index] = "`%s` %s%s" % (name, tname, flags)
|
|
if type & type_key:
|
|
keys.append("`%s`" % name)
|
|
fields = ", ".join(fields)
|
|
keys = ", ".join(keys)
|
|
return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self.name, fields, keys)
|
|
|
|
def create(self, db):
|
|
v = db.OpenView(self.sql())
|
|
v.Execute(None)
|
|
v.Close()
|
|
|
|
class _Unspecified:pass
|
|
def change_sequence(seq, action, seqno=_Unspecified, cond = _Unspecified):
|
|
"Change the sequence number of an action in a sequence list"
|
|
for i in range(len(seq)):
|
|
if seq[i][0] == action:
|
|
if cond is _Unspecified:
|
|
cond = seq[i][1]
|
|
if seqno is _Unspecified:
|
|
seqno = seq[i][2]
|
|
seq[i] = (action, cond, seqno)
|
|
return
|
|
raise ValueError("Action not found in sequence")
|
|
|
|
def add_data(db, table, values):
|
|
v = db.OpenView("SELECT * FROM `%s`" % table)
|
|
count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
|
|
r = CreateRecord(count)
|
|
for value in values:
|
|
assert len(value) == count, value
|
|
for i in range(count):
|
|
field = value[i]
|
|
if isinstance(field, int):
|
|
r.SetInteger(i+1,field)
|
|
elif isinstance(field, str):
|
|
r.SetString(i+1,field)
|
|
elif field is None:
|
|
pass
|
|
elif isinstance(field, Binary):
|
|
r.SetStream(i+1, field.name)
|
|
else:
|
|
raise TypeError("Unsupported type %s" % field.__class__.__name__)
|
|
try:
|
|
v.Modify(MSIMODIFY_INSERT, r)
|
|
except Exception:
|
|
raise MSIError("Could not insert "+repr(values)+" into "+table)
|
|
|
|
r.ClearData()
|
|
v.Close()
|
|
|
|
|
|
def add_stream(db, name, path):
|
|
v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name)
|
|
r = CreateRecord(1)
|
|
r.SetStream(1, path)
|
|
v.Execute(r)
|
|
v.Close()
|
|
|
|
def init_database(name, schema,
|
|
ProductName, ProductCode, ProductVersion,
|
|
Manufacturer):
|
|
try:
|
|
os.unlink(name)
|
|
except OSError:
|
|
pass
|
|
ProductCode = ProductCode.upper()
|
|
# Create the database
|
|
db = OpenDatabase(name, MSIDBOPEN_CREATE)
|
|
# Create the tables
|
|
for t in schema.tables:
|
|
t.create(db)
|
|
# Fill the validation table
|
|
add_data(db, "_Validation", schema._Validation_records)
|
|
# Initialize the summary information, allowing atmost 20 properties
|
|
si = db.GetSummaryInformation(20)
|
|
si.SetProperty(PID_TITLE, "Installation Database")
|
|
si.SetProperty(PID_SUBJECT, ProductName)
|
|
si.SetProperty(PID_AUTHOR, Manufacturer)
|
|
if AMD64:
|
|
si.SetProperty(PID_TEMPLATE, "x64;1033")
|
|
else:
|
|
si.SetProperty(PID_TEMPLATE, "Intel;1033")
|
|
si.SetProperty(PID_REVNUMBER, gen_uuid())
|
|
si.SetProperty(PID_WORDCOUNT, 2) # long file names, compressed, original media
|
|
si.SetProperty(PID_PAGECOUNT, 200)
|
|
si.SetProperty(PID_APPNAME, "Python MSI Library")
|
|
# XXX more properties
|
|
si.Persist()
|
|
add_data(db, "Property", [
|
|
("ProductName", ProductName),
|
|
("ProductCode", ProductCode),
|
|
("ProductVersion", ProductVersion),
|
|
("Manufacturer", Manufacturer),
|
|
("ProductLanguage", "1033")])
|
|
db.Commit()
|
|
return db
|
|
|
|
def add_tables(db, module):
|
|
for table in module.tables:
|
|
add_data(db, table, getattr(module, table))
|
|
|
|
def make_id(str):
|
|
identifier_chars = string.ascii_letters + string.digits + "._"
|
|
str = "".join([c if c in identifier_chars else "_" for c in str])
|
|
if str[0] in (string.digits + "."):
|
|
str = "_" + str
|
|
assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
|
|
return str
|
|
|
|
def gen_uuid():
|
|
return "{"+UuidCreate().upper()+"}"
|
|
|
|
class CAB:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.files = []
|
|
self.filenames = set()
|
|
self.index = 0
|
|
|
|
def gen_id(self, file):
|
|
logical = _logical = make_id(file)
|
|
pos = 1
|
|
while logical in self.filenames:
|
|
logical = "%s.%d" % (_logical, pos)
|
|
pos += 1
|
|
self.filenames.add(logical)
|
|
return logical
|
|
|
|
def append(self, full, file, logical):
|
|
if os.path.isdir(full):
|
|
return
|
|
if not logical:
|
|
logical = self.gen_id(file)
|
|
self.index += 1
|
|
self.files.append((full, logical))
|
|
return self.index, logical
|
|
|
|
def commit(self, db):
|
|
from tempfile import mktemp
|
|
filename = mktemp()
|
|
FCICreate(filename, self.files)
|
|
add_data(db, "Media",
|
|
[(1, self.index, None, "#"+self.name, None, None)])
|
|
add_stream(db, self.name, filename)
|
|
os.unlink(filename)
|
|
db.Commit()
|
|
|
|
_directories = set()
|
|
class Directory:
|
|
def __init__(self, db, cab, basedir, physical, _logical, default, componentflags=None):
|
|
"""Create a new directory in the Directory table. There is a current component
|
|
at each point in time for the directory, which is either explicitly created
|
|
through start_component, or implicitly when files are added for the first
|
|
time. Files are added into the current component, and into the cab file.
|
|
To create a directory, a base directory object needs to be specified (can be
|
|
None), the path to the physical directory, and a logical directory name.
|
|
Default specifies the DefaultDir slot in the directory table. componentflags
|
|
specifies the default flags that new components get."""
|
|
index = 1
|
|
_logical = make_id(_logical)
|
|
logical = _logical
|
|
while logical in _directories:
|
|
logical = "%s%d" % (_logical, index)
|
|
index += 1
|
|
_directories.add(logical)
|
|
self.db = db
|
|
self.cab = cab
|
|
self.basedir = basedir
|
|
self.physical = physical
|
|
self.logical = logical
|
|
self.component = None
|
|
self.short_names = set()
|
|
self.ids = set()
|
|
self.keyfiles = {}
|
|
self.componentflags = componentflags
|
|
if basedir:
|
|
self.absolute = os.path.join(basedir.absolute, physical)
|
|
blogical = basedir.logical
|
|
else:
|
|
self.absolute = physical
|
|
blogical = None
|
|
add_data(db, "Directory", [(logical, blogical, default)])
|
|
|
|
def start_component(self, component = None, feature = None, flags = None, keyfile = None, uuid=None):
|
|
"""Add an entry to the Component table, and make this component the current for this
|
|
directory. If no component name is given, the directory name is used. If no feature
|
|
is given, the current feature is used. If no flags are given, the directory's default
|
|
flags are used. If no keyfile is given, the KeyPath is left null in the Component
|
|
table."""
|
|
if flags is None:
|
|
flags = self.componentflags
|
|
if uuid is None:
|
|
uuid = gen_uuid()
|
|
else:
|
|
uuid = uuid.upper()
|
|
if component is None:
|
|
component = self.logical
|
|
self.component = component
|
|
if AMD64:
|
|
flags |= 256
|
|
if keyfile:
|
|
keyid = self.cab.gen_id(keyfile)
|
|
self.keyfiles[keyfile] = keyid
|
|
else:
|
|
keyid = None
|
|
add_data(self.db, "Component",
|
|
[(component, uuid, self.logical, flags, None, keyid)])
|
|
if feature is None:
|
|
feature = current_feature
|
|
add_data(self.db, "FeatureComponents",
|
|
[(feature.id, component)])
|
|
|
|
def make_short(self, file):
|
|
oldfile = file
|
|
file = file.replace('+', '_')
|
|
file = ''.join(c for c in file if not c in r' "/\[]:;=,')
|
|
parts = file.split(".")
|
|
if len(parts) > 1:
|
|
prefix = "".join(parts[:-1]).upper()
|
|
suffix = parts[-1].upper()
|
|
if not prefix:
|
|
prefix = suffix
|
|
suffix = None
|
|
else:
|
|
prefix = file.upper()
|
|
suffix = None
|
|
if len(parts) < 3 and len(prefix) <= 8 and file == oldfile and (
|
|
not suffix or len(suffix) <= 3):
|
|
if suffix:
|
|
file = prefix+"."+suffix
|
|
else:
|
|
file = prefix
|
|
else:
|
|
file = None
|
|
if file is None or file in self.short_names:
|
|
prefix = prefix[:6]
|
|
if suffix:
|
|
suffix = suffix[:3]
|
|
pos = 1
|
|
while 1:
|
|
if suffix:
|
|
file = "%s~%d.%s" % (prefix, pos, suffix)
|
|
else:
|
|
file = "%s~%d" % (prefix, pos)
|
|
if file not in self.short_names: break
|
|
pos += 1
|
|
assert pos < 10000
|
|
if pos in (10, 100, 1000):
|
|
prefix = prefix[:-1]
|
|
self.short_names.add(file)
|
|
assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
|
|
return file
|
|
|
|
def add_file(self, file, src=None, version=None, language=None):
|
|
"""Add a file to the current component of the directory, starting a new one
|
|
if there is no current component. By default, the file name in the source
|
|
and the file table will be identical. If the src file is specified, it is
|
|
interpreted relative to the current directory. Optionally, a version and a
|
|
language can be specified for the entry in the File table."""
|
|
if not self.component:
|
|
self.start_component(self.logical, current_feature, 0)
|
|
if not src:
|
|
# Allow relative paths for file if src is not specified
|
|
src = file
|
|
file = os.path.basename(file)
|
|
absolute = os.path.join(self.absolute, src)
|
|
assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
|
|
if file in self.keyfiles:
|
|
logical = self.keyfiles[file]
|
|
else:
|
|
logical = None
|
|
sequence, logical = self.cab.append(absolute, file, logical)
|
|
assert logical not in self.ids
|
|
self.ids.add(logical)
|
|
short = self.make_short(file)
|
|
full = "%s|%s" % (short, file)
|
|
filesize = os.stat(absolute).st_size
|
|
# constants.msidbFileAttributesVital
|
|
# Compressed omitted, since it is the database default
|
|
# could add r/o, system, hidden
|
|
attributes = 512
|
|
add_data(self.db, "File",
|
|
[(logical, self.component, full, filesize, version,
|
|
language, attributes, sequence)])
|
|
#if not version:
|
|
# # Add hash if the file is not versioned
|
|
# filehash = FileHash(absolute, 0)
|
|
# add_data(self.db, "MsiFileHash",
|
|
# [(logical, 0, filehash.IntegerData(1),
|
|
# filehash.IntegerData(2), filehash.IntegerData(3),
|
|
# filehash.IntegerData(4))])
|
|
# Automatically remove .pyc files on uninstall (2)
|
|
# XXX: adding so many RemoveFile entries makes installer unbelievably
|
|
# slow. So instead, we have to use wildcard remove entries
|
|
if file.endswith(".py"):
|
|
add_data(self.db, "RemoveFile",
|
|
[(logical+"c", self.component, "%sC|%sc" % (short, file),
|
|
self.logical, 2),
|
|
(logical+"o", self.component, "%sO|%so" % (short, file),
|
|
self.logical, 2)])
|
|
return logical
|
|
|
|
def glob(self, pattern, exclude = None):
|
|
"""Add a list of files to the current component as specified in the
|
|
glob pattern. Individual files can be excluded in the exclude list."""
|
|
try:
|
|
files = os.listdir(self.absolute)
|
|
except OSError:
|
|
return []
|
|
if pattern[:1] != '.':
|
|
files = (f for f in files if f[0] != '.')
|
|
files = fnmatch.filter(files, pattern)
|
|
for f in files:
|
|
if exclude and f in exclude: continue
|
|
self.add_file(f)
|
|
return files
|
|
|
|
def remove_pyc(self):
|
|
"Remove .pyc files on uninstall"
|
|
add_data(self.db, "RemoveFile",
|
|
[(self.component+"c", self.component, "*.pyc", self.logical, 2)])
|
|
|
|
class Binary:
|
|
def __init__(self, fname):
|
|
self.name = fname
|
|
def __repr__(self):
|
|
return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
|
|
|
|
class Feature:
|
|
def __init__(self, db, id, title, desc, display, level = 1,
|
|
parent=None, directory = None, attributes=0):
|
|
self.id = id
|
|
if parent:
|
|
parent = parent.id
|
|
add_data(db, "Feature",
|
|
[(id, parent, title, desc, display,
|
|
level, directory, attributes)])
|
|
def set_current(self):
|
|
global current_feature
|
|
current_feature = self
|
|
|
|
class Control:
|
|
def __init__(self, dlg, name):
|
|
self.dlg = dlg
|
|
self.name = name
|
|
|
|
def event(self, event, argument, condition = "1", ordering = None):
|
|
add_data(self.dlg.db, "ControlEvent",
|
|
[(self.dlg.name, self.name, event, argument,
|
|
condition, ordering)])
|
|
|
|
def mapping(self, event, attribute):
|
|
add_data(self.dlg.db, "EventMapping",
|
|
[(self.dlg.name, self.name, event, attribute)])
|
|
|
|
def condition(self, action, condition):
|
|
add_data(self.dlg.db, "ControlCondition",
|
|
[(self.dlg.name, self.name, action, condition)])
|
|
|
|
class RadioButtonGroup(Control):
|
|
def __init__(self, dlg, name, property):
|
|
self.dlg = dlg
|
|
self.name = name
|
|
self.property = property
|
|
self.index = 1
|
|
|
|
def add(self, name, x, y, w, h, text, value = None):
|
|
if value is None:
|
|
value = name
|
|
add_data(self.dlg.db, "RadioButton",
|
|
[(self.property, self.index, value,
|
|
x, y, w, h, text, None)])
|
|
self.index += 1
|
|
|
|
class Dialog:
|
|
def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
|
|
self.db = db
|
|
self.name = name
|
|
self.x, self.y, self.w, self.h = x,y,w,h
|
|
add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
|
|
|
|
def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
|
|
add_data(self.db, "Control",
|
|
[(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
|
|
return Control(self, name)
|
|
|
|
def text(self, name, x, y, w, h, attr, text):
|
|
return self.control(name, "Text", x, y, w, h, attr, None,
|
|
text, None, None)
|
|
|
|
def bitmap(self, name, x, y, w, h, text):
|
|
return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
|
|
|
|
def line(self, name, x, y, w, h):
|
|
return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
|
|
|
|
def pushbutton(self, name, x, y, w, h, attr, text, next):
|
|
return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
|
|
|
|
def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
|
|
add_data(self.db, "Control",
|
|
[(self.name, name, "RadioButtonGroup",
|
|
x, y, w, h, attr, prop, text, next, None)])
|
|
return RadioButtonGroup(self, name, prop)
|
|
|
|
def checkbox(self, name, x, y, w, h, attr, prop, text, next):
|
|
return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)
|