123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- # 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
- import warnings
- warnings._deprecated(__name__, remove=(3, 13))
- 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)
|