Python msilib basics
Create new MSI
Create new .msi, like in
distutils.command.bdist_msi
UPGRADE_CODE = '{39CFB886-7C1D-4469-A9C1-0C578E1C36D8}' # static msilib.gen_uuid() db = msilib.init_database('/path/to/installer.msi', # name: path to installer msilib.schema, # schema: pre-made database-schema 'YourPackage', # ProductName: package-name (as appears within UI, Add/Remove Programs, etc) msilib.gen_uuid(), # ProductCode: ms-UUID, unique to every release(version) of product '1.0.0', # ProductVersion: version-str 'Will Pittman') # Manufacturer: company or author # insert default install-sequence into db msilib.add_tables(db, msilib.sequence) # set basic properties property_data = [('ARPCONTACT', 'you@example.com'), ('ARPURLINFOABOUT', 'https://yourwebsite.com') ('UpgradeCode', UPGRADE_CODE)]) msilib.add_data(db, 'Property', property_data) db.Commit()
MSI Database
Python SQL interface
This is the abbreviated SQL interface created by python.
I find it much more convenient.# 1 tuple of all columns for each row to be inserted data = [(col1a, col2a, col3a, ...), # row 1 (col1b, col2b, col3b, ...), # row 2 ...] msilib.add_data(db, 'table-name', data) db.Commit()SQL interface
Add files to MSI (sql-like).
This is the long form of
msilib.add_data()
- and more closely mirrors native MSI API.sql = ("INSERT INTO Property (Property.Property, Property.Value) " "VALUES ('ARPHELPLINK', 'http://yourwebsite.com')") view = db.OpenView(sql) view.Execute(None) db.Commit()SQL can have param substitution using the character
?
# WARNING: I have not gotten this to work yet view = db.OpenView('SELECT * FROM ?') # '?' will be replaced with 'Environment' view.Execute('Environment')When performing Queries,
view.Fetch()
will yield one row(Record) at a time. Once you hit the last record, it will yieldNone
.The query syntax does support
ORDER BY
, but you cannot useDESC
so your results will always be in ascending order.
Adding Files
You could insert files using the SQL interface, but this is more concise/automatic and adds files to CAB.
NOTE:
If no TARGETDIR is set on commandline or elsewhere, files installed to C:\
NOTE:
I highly recommend recreating your full installation on disk in a tempdir first, then creating the MSI from it.
# Every directory requires it's own separate CAB file. # CAB files can only be committed once. # # `Directory()` objects have undocumented methods/attributes # that are very useful if not essential. # # dir.logical, dir.absolute, dir.db, dir.cab, dir.make_short('my_filename.txt') # cab = msilib.CAB('my_cab_name') msidir = msilib.Directory(db, # database: msi database cab, # cab: CAB archive associated with this directory None, # basedir: None(targetdir only), or parent `msilib.Directory(..)` '.', # physical: ??? 'TARGETDIR', # logical: primary-key in Directory table (str) 'SourceDir') # default: directory name # files must be tied to a feature. # (ex: install everything, just python2, python2+3, ...) feature = msilib.Feature(db, # db 'Everything', # id 'Everything', # title 'Everything', # desc 0, # display 1, # level directory='TARGETDIR') feature.set_current() # add 'README.rst' from cwd msidir.add_file('README.rst') db.Commit() cab.commit(db)Child Directories, if longer than 8 characters will require a shortname.
Conveniently, python'smsilib.Directory()
objects have a hidden methoddir.make_short('yourlongfilename.txt')
.
The syntax for Directory Table DefaultDir entries is{shortname}|{longname}
.
target_dir = msilib.Directory(db, cab, None, '.', 'TARGETDIR', 'SourceDir') # example of shortname syntax data_cab = msilib.CAB('datafiles') dirname = 'datafiles' shortname = target_dir.make_short(dirname) dirname_val = '{}|{}'.format(shortname, dirname) data_dir = msilib.Directory(db, data_cab, target_dir, '?', 'your-pri-key', dirname_val, 2)
set TARGETDIR
The
TARGETDIR
property determines where files are installed to.
This can be set on the commandline, or within the GUI.
If invalid, your install defaults to%ROOTDRIVE%
.The following snippet will
- set TARGETDIR value if unset (val accepts msi-properties/envvars)
- accept TARGETDIR value set on the commandline
msiexec /i installer.msi TARGETDIR=C:\temp
DESIRED_TARGETDIR = r'[AppDataFolder]\tma' action_data = [('SetTargetDirAction', # name 51, # type (set property from formatted string) 'TARGETDIR', # source (name of property) DESIRED_TARGETDIR)] # target (the formatted string) msilib.add_data(db, 'CustomAction', action_data) db.Commit() execute_data = [('SetTargetDirAction', 'Not TARGETDIR Or TARGETDIR=ROOTDRIVE', # evaluates if: TARGETDIR is not set, or TARGETDIR=ROOTDRIVE(default). 990)] # Sequence must be lower than 'CostFinalize' action (1000) msilib.add_data(db, 'InstallExecuteSequence', execute_data) msilib.add_data(db, 'AdminExecuteSequence', execute_data) msilib.add_data(db, 'InstallUISequence', execute_data) msilib.add_data(db, 'AdminUISequence', execute_data) db.Commit()
Embedded Scripts
type_ = 51 # target = 'cmd.exe /c {}'.format(commandstr) customaction_data = [(id_, type_, '[SystemFolder]', target)] msilib.add_data(db, 'CustomAction', customaction_data) # add to installsequence...See Also: https://www.alkanesolutions.co.uk/blog/2015/08/25/create-a-symbolic-link-from-a-custom-action/