Python msilib basics

From wikinotes

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 yield None.

The query syntax does support ORDER BY, but you cannot use DESC 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's msilib.Directory() objects have a hidden method dir.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()
General
TARGETDIR property https://docs.microsoft.com/en-us/windows/win32/msi/targetdir
setting TARGETDIR https://docs.microsoft.com/en-us/windows/win32/msi/changing-the-target-location-for-a-directory
All Properties https://docs.microsoft.com/en-us/windows/win32/msi/property-reference
CustomAction method
CustomAction-51 (before costFinalize) https://docs.microsoft.com/en-us/windows/win32/msi/custom-action-type-51
CustomAction-35 (after costFinalize) https://docs.microsoft.com/en-us/windows/win32/msi/custom-action-type-35
msiFormatRecord https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msiformatrecorda
Conditional Syntax https://docs.microsoft.com/en-us/windows/win32/msi/conditional-statement-syntax
CustomAction Table
Sequence Tables
Controlled by UI
SetTargetPath ControlEvent https://docs.microsoft.com/en-us/windows/win32/msi/settargetpath-controlevent

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/