rubrapack Manual←↑→

34 The smallest package that installs, repairs, upgrades and uninstalls

Table meanings are documented on Microsoft Learn ("Database Tables", "Standard Actions", "Suggested InstallExecuteSequence"). This page is a tested recipe: a package built exactly like this installs per machine, survives repair, upgrades an older version, refuses a downgrade and uninstalls cleanly on Windows 11. [observed]

34.1 Tables and columns#

Types use the notation of The MSI database inside the compound file (s72 = CHAR(72), upper case = nullable, l = localizable, i2/i4 integers). Keys are in bold.

TableColumns
PropertyProperty s72, Value l0
DirectoryDirectory s72, Directory_Parent S72, DefaultDir l255
ComponentComponent s72, ComponentId S38, Directory_ s72, Attributes i2, Condition S255, KeyPath S72
FeatureFeature s38, Feature_Parent S38, Title L64, Description L255, Display I2, Level i2, Directory_ S72, Attributes i2
FeatureComponentsFeature_ s38, Component_ s72
FileFile s72, Component_ s72, FileName l255, FileSize i4, Version S72, Language S20, Attributes I2, Sequence i4
MsiFileHashFile_ s72, Options i2, HashPart1..HashPart4 i4
CreateFolderDirectory_ s72, Component_ s72
MediaDiskId i2, LastSequence i4, DiskPrompt L64, Cabinet S255, VolumeLabel S32, Source S72
UpgradeUpgradeCode s38, VersionMin S20, VersionMax S20, Language S255, Attributes i4, Remove S255, ActionProperty s72
CustomActionAction s72, Type i2, Source S72, Target S255
InstallExecuteSequence, InstallUISequenceAction s72, Condition S255, Sequence I2

File.Sequence and Media.LastSequence as 32-bit integers let a package hold more than 32767 files.

34.2 Property#

ProductCode, ProductName, ProductVersion (a.b.c, a and b up to 255, c up to 65535; a fourth part is allowed but ignored when versions are compared), Manufacturer, ProductLanguage (1033, 1042, ...), UpgradeCode, ALLUSERS=1 (per machine), REBOOT=ReallySuppress, MSIRESTARTMANAGERCONTROL=Disable (see "Files in use" below), and SecureCustomProperties listing the two upgrade properties below.

34.3 Directory#

34.4 Components, features, files#

34.5 Empty folders#

A folder that must exist without files gets its own Directory row, a component whose Directory_ is that folder and whose KeyPath is null (the folder itself is the key path), a CreateFolder row pointing at both, and a FeatureComponents row. CreateFolders makes it at install time; RemoveFolders removes it at uninstall if it is empty. [observed] With component attribute 16 (permanent) the folder stays after uninstall while everything else is removed. [observed]

34.6 Media and the cabinet#

One row: DiskId 1, LastSequence n, Cabinet #cab1.cab - the # means "a stream inside this package named cab1.cab". The cabinet holds the files named by their File keys, in Sequence order (Cabinets, MSZIP and deflate). Summary Word Count 2 says the files are compressed into cabinets.

34.7 Major upgrade and downgrade refusal#

Two Upgrade rows with the package's UpgradeCode:

VersionMinVersionMaxAttributesActionPropertyMeaning
this version-0x102 (only detect, min inclusive)NEWER_FOUNDsame or newer version installed
-this version0x001 (migrate features)OLDER_FOUNDolder version installed: remove it

The Upgrade table matches by UpgradeCode, version and language only - not by architecture. If an x64 and an x86 build share one UpgradeCode, installing one removes the other as "older". Give each architecture its own UpgradeCode when both may be installed side by side (verified: two architectures with separate codes install and uninstall independently). [observed]

An error custom action (type 19, Target = the message; it is a formatted string, so [ProductName] works) conditioned on NEWER_FOUND stops the installation with error 1603 when a same-or-newer version is present. Every version needs a new ProductCode (and every package file a new package code).

34.8 Running an installed program to register and unregister#

Some products must register themselves (an input method, a shell extension) through their own program rather than through table rows. The pattern that keeps install, repair, upgrade and removal atomic uses the component action state of that program's component C ($C = what this installation does to it, ?C = its state before; 2 absent, 3 local) [spec], with type 18 actions (Source = the program's File key, Target = its arguments), all deferred (0x400) and not impersonated (0x800):

ActionTypeWhereConditionRuns
…UndoRollback18+0x100+0x400+0x800+0x40before RemoveFiles$C=2 AND ?C=3register (rollback of the next row)
…Undo18+0x400+0x800after it, before RemoveFiles$C=2 AND ?C=3unregister
…DoRollback18+0x100+0x400+0x800+0x40after InstallFiles$C>2 AND ?C<>3unregister
…RedoRollbacksameafter it$C>2 AND ?C=3register
…Do18+0x400+0x800after both$C>2register

A rollback action must be sequenced before the action it undoes: rollback runs the script backwards, and only rollback actions already in the script are run. Rollback actions ignore their exit code (0x40); forward actions do not, so a failing register or unregister fails the installation and rolls it back. Observed on Windows 11: repair runs …Do again; a failing unregister rolls the removal back and the program (still present) registers again; when an upgrade fails, the new package's …DoRollback runs, then the old version's files return and the old package's own …UndoRollback registers the old program again. [observed]

Nothing that writes to the script may stand between InstallInitialize and RemoveExistingProducts: with any deferred or rollback custom action there, every upgrade stops with error 2613 ("RemoveExistingProducts action sequenced incorrectly"). [observed]

34.9 Registry values#

Registry (Registry s72, Root i2, Key l255, Name L255, Value L0, Component_ s72) is written by WriteRegistryValues (5000) and undone at uninstall; RemoveRegistry (same columns without Value) by RemoveRegistryValues (2600). Root 0 = HKCR, 1 = HKCU, 2 = HKLM. The value's first characters choose its type: #x binary (hex), #% expandable string, # followed by digits a DWORD (up to #4294967295), [~] anywhere a multi-string ([~]a[~]b[~]); a plain string that starts with # is written as ##. A Name of - in RemoveRegistry deletes the whole key. A failed installation restores overwritten values and removed keys. [observed]

A component whose key path is a registry value has attribute 4 and KeyPath = the Registry row. The component's 64-bit attribute (256) chooses the registry view: without it, a 64-bit package writes to the 32-bit view (WOW6432Node). A 32-bit component in a 64-bit package is legal; a 64-bit component in a 32-bit package is not.

REG_QWORD cannot be expressed in the Registry table. rubrapack writes it with a DLL custom action from the Binary table (type 1): an immediate action reads the component action states and the current values and passes two lists as CustomActionData - one to a deferred action that writes or deletes, one to its rollback twin that restores what was there. The DLL's bitness must match the package (the engine loads it into a custom action server of the package's architecture). [observed: x64 and x86]

File types and URL schemes are plain Registry rows under Root 0 (HKCR) in the program's component, not the Extension/Verb/ProgId tables (those bring advertisement and repair on first use): .ext (default) = the ProgId; ProgId (default) = its description, ProgId\DefaultIcon = [#File],0, ProgId\shell\open\command = "[#File]" "%1"; for a scheme, scheme (default) = URL:<description>, URL Protocol = empty (a Null Value with a Name writes an empty string), and the same DefaultIcon and shell\open\command. Root 0 follows the installation: HKLM\Software\Classes per machine, HKCU\Software\Classes per user. Opening a file of a type only this program claims then starts it directly. [observed]

34.10 Shortcuts#

Shortcut (Shortcut s72, Directory_ s72, Name l128 SHORT|Long without .lnk, Component_ s72, Target a formatted path for a non-advertised shortcut ([#FileKey] or [DirKey]name), Arguments (formatted), Description (plain text), Hotkey, Icon_, IconIndex, ShowCmd, WkDir = a Directory key) is written by CreateShortcuts (4500) and removed by RemoveShortcuts (3200). A shortcut in its target file's component works, but Microsoft's ICE43/ICE57 want each non-advertised shortcut in a component keyed by an HKCU value (see Checking your output against Windows); rubrapack gives every shortcut such a component and targets [DirKey]name (ICE69). A folder made only for shortcuts is not removed by itself: add a RemoveFile row (FileName null, DirProperty = the folder, InstallMode 2) for it and each parent below the standard folder. With ALLUSERS=1, ProgramMenuFolder and DesktopFolder are the all-users Start menu and the Public Desktop. A failed installation leaves no shortcut or folder; repair recreates a deleted one. [observed]

34.11 Removing files and duplicating them#

RemoveFile (FileKey s72, Component_ s72, FileName L255 with */? or null for the folder, DirProperty s72, InstallMode i2: 1 install, 2 uninstall, 3 both) runs in RemoveFiles when its component is installed (1) or removed (2). A component with no key path file and a Directory_ works as the carrier. DuplicateFile (FileKey s72, Component_ s72 = the source's component, File_ s72, DestName L255 SHORT|Long, DestFolder S72) runs in DuplicateFiles (4210) and RemoveDuplicateFiles. Files removed at install come back when the installation fails; a folder that existed before the installation is left in place at uninstall, even when empty. [observed]

34.12 Environment variables#

Environment (Environment s72, Name l255, Value L255 formatted, Component_ s72), written by WriteEnvironmentStrings (5200) and RemoveEnvironmentStrings. Name prefixes: * system variable (without it a per-user one), = create or set, - undo when the component is removed. Value [~];x appends ;x, x;[~] prepends. With -, uninstall deletes a set variable and takes only the appended or prepended part out of a variable that existed before; without - nothing is undone. A failed installation restores the previous values. [observed]

34.13 INI files#

IniFile (IniFile s72, FileName l255 SHORT|Long, DirProperty S72, Section l96, Key l128, Value l255 formatted, Action i2: 0 add line, 1 create line, 3 add tag, Component_ s72) is applied by WriteIniValues (5100) and undone when the component is removed; RemoveIniFile (same columns, Value nullable, Action 2 remove line, 4 remove tag) by RemoveIniValues, which runs before InstallFiles. An add tag appends ,value to a comma list and uninstall removes just that item. Non-ASCII file, section, key and value names work. A failed installation restores the file. [observed]

34.14 Searches and launch conditions#

AppSearch (Property, Signature_) at 50 in both sequences sets the property from a locator with the same signature: RegLocator (Root, Key, Name, Type 2 = raw value, +16 = 64-bit view), DrLocator (Path may start with a folder property, like [System64Folder]) with a Signature row for a file (FileName, MinVersion), or CompLocator (ComponentId, Type 1 = key file). The property must be public and listed in SecureCustomProperties to reach the server side.

A search can give a directory its default: a directory's property set before CostFinalize is where the directory resolves. rubrapack searches into a property of its own (RpFound_<ID>) and copies it with a type-51 action (Source = the directory, Target = [RpFound_<ID>]) at 51 in both sequences under the condition RpFound_<ID> AND NOT <DIR>, so a directory given on the command line is left alone. The registry locator then has Type 0 (a folder, which must exist; the value comes back with a trailing backslash) instead of 2: AppSearch looks a folder-type RegLocator up in the Signature table, which must exist, even empty - without it the installation stops with 2228. [observed: a /qn major upgrade installs into the folder the earlier version recorded; DIR= on the command line wins]

rubrapack's install folder guard (guard = true) is an immediate DLL custom action (type 1, the helper DLL in Binary) at 1010 in InstallExecuteSequence, after CostFinalize has resolved the directories and before InstallValidate, under NOT Installed. It reads the Directory keys from a property, gets each path with MsiGetTargetPath, and refuses a path in which an existing part is a reparse point or not a folder, or whose folder exists with an owner (GetNamedSecurityInfo) other than S-1-5-18, S-1-5-32-544 or TrustedInstaller; it then shows the message with MsiProcessMessage(INSTALLMESSAGE_ERROR) and returns ERROR_INSTALL_FAILURE, so the installation ends with 1603 before any file is written. [observed]

LaunchCondition (Condition, Description formatted) at 100 in both sequences stops the installation with the description when a condition is false. [observed]

34.15 Services, fonts, permissions#

ServiceInstall (Name, DisplayName and Description formatted, ServiceType 0x10, StartType 2/3/4, ErrorControl 1, StartName null or NT AUTHORITY\LocalService, Arguments formatted, Component_ = the exe's component) with InstallServices (5800); ServiceControl Event flags 0x1 start on install, 0x2 stop on install, 0x20 stop on removal, 0x80 delete on removal, Wait 1, with StopServices (1900), DeleteServices (2000), StartServices (5900). Font (File_, FontTitle null = read from the font) with RegisterFonts/UnregisterFonts; the file must be in FontsFolder itself. MsiLockPermissionsEx (MsiLockPermissionsEx, LockObject, Table = CreateFolder, File, Registry or ServiceInstall, SDDLText (the column name), Condition) needs Windows Installer 5.0 (summary page count 500); a folder gets it through a CreateFolder row. A failed installation leaves no service or font. [observed]

34.16 Cabinets, administrative images, advertisement#

Several cabinets: one Media row each (DiskId 1..n, LastSequence = the last file's sequence in it); embedded ones are streams named in Cabinet with #, external ones are files next to the package named in Cabinet without # (long names work). AdminExecuteSequence (CostInitialize 800, FileCost 900, CostFinalize 1000, InstallValidate 1400, InstallInitialize 1500, InstallAdminPackage 3900, InstallFiles 4000, InstallFinalize 6600) and AdminUISequence make msiexec /a write an uncompressed image that installs like the original; AdvtExecuteSequence (CostInitialize, CostFinalize, InstallValidate, InstallInitialize, PublishFeatures 6300, PublishProduct 6400, InstallFinalize) makes msiexec /jm advertise the product. [observed]

34.17 Per-user and dual packages#

The single-package form: ALLUSERS=2 with MSIINSTALLPERUSER=1 installs per user - the engine points ProgramFilesFolder/ProgramFiles64Folder at %LOCALAPPDATA%\Programs and the menu and desktop folders at the user's - and ALLUSERS=1 MSIINSTALLPERUSER="" installs per machine. Summary Word Count bit 8 says no elevation is needed. Registry Root -1 (HKMU) is HKCU per user and HKLM per machine; an Environment name without * is a user variable. Once a per-user product is installed the engine deletes MSIINSTALLPERUSER, so a launch condition that tests it must read Installed OR ... or the product can no longer be removed; in general, write every launch condition as Installed OR (...). [observed]

34.18 Files in use#

When a file to be replaced or removed is held by a running program (a DLL loaded into it):

34.19 Sequences#

InstallExecuteSequence (conditions in brackets):

FindRelatedProducts 25, <refuse-downgrade action> 30 [NEWER_FOUND], CostInitialize 800,
FileCost 900, CostFinalize 1000, MigrateFeatureStates 1200, InstallValidate 1400,
InstallInitialize 1500, RemoveExistingProducts 1501, ProcessComponents 1600,
UnpublishFeatures 1800, RemoveFiles 3500, RemoveFolders 3600, CreateFolders 3700,
InstallFiles 4000, RegisterUser 6000, RegisterProduct 6100, PublishFeatures 6300,
PublishProduct 6400, InstallFinalize 6600

InstallUISequence: FindRelatedProducts 25, the refuse-downgrade action 30 [NEWER_FOUND], CostInitialize 800, FileCost 900, CostFinalize 1000, MigrateFeatureStates 1200, ExecuteAction 1300 (and the dialogs, see Dialogs).

RemoveExistingProducts right after InstallInitialize puts the removal of the old version inside the new installation's transaction, so a failed upgrade rolls back to the old version. [observed: a deferred custom action that fails right after InstallFiles makes the upgrade end with 1603, and the old version is registered again with its files byte for byte; a failed first installation leaves no files and no registration.]

34.20 Dialogs#

A package with dialogs carries these tables (types as above):

TableColumns
DialogDialog s72, HCentering i2, VCentering i2, Width i2, Height i2, Attributes I4, Title L128, Control_First s50, Control_Default S50, Control_Cancel S50
ControlDialog_ s72, Control s50, Type s20, X i2, Y i2, Width i2, Height i2, Attributes I4, Property S72, Text L0, Control_Next S50, Help L50
ControlEventDialog_ s72, Control_ s50, Event s50, Argument s255, Condition S255, Ordering I2
ControlConditionDialog_ s72, Control_ s50, Action s50, Condition s255
EventMappingDialog_ s72, Control_ s50, Event s50, Attribute s50
TextStyleTextStyle s72, FaceName s32, Size i2, Color I4, StyleBits I2
UITextKey s72, Text L255
BinaryName s72, Data v0 (a stream)
RadioButtonProperty s72, Order i2, Value s64, X i2, Y i2, Width i2, Height i2, Text L0, Help L50
ComboBoxProperty s72, Order i2, Value s64, Text L64

What the engine checks when it shows a dialog (each seen as an error dialog carrying the number):

rubrapack's sets: every dialog is 370 x 270 dialog units with a banner strip (a Bitmap control 0,0,370,44 whose picture is Binary.RpBanner), the title in bold ({\RpTitle}, a TextStyle) and a line of description in it, a Line at 44 and at 234, and Back / Next / Cancel at y 243. The InstallUISequence adds the welcome (1230, NOT Installed) or the maintenance dialog (1240, Installed AND NOT RESUME AND NOT Preselected), the progress dialog (1280, modeless, subscribed to ActionText and SetProgress through EventMapping) and the three exit dialogs at -1 (success), -2 (cancelled) and -3 (fatal). The install folder dialog puts the folder's Directory key in a PathEdit, and its Next runs SetTargetPath before NewDialog, which checks the path. The license text is a ScrollableText control holding RTF: plain text becomes \uN? escapes (characters above U+FFFF as a surrogate pair), one \par per line.

Author pages are ordinary Dialog rows in the same frame; Control_Next runs through their controls in position order and on to Back, Next and Cancel. A radio group is a RadioButtonGroup control whose buttons are RadioButton rows (positions relative to the group); a drop-down list is a ComboBox control with ComboList (0x20000) and Sorted, filled from ComboBox rows. Their properties are added to SecureCustomProperties, so values chosen in the dialogs or given on the command line reach the execute sequence. [observed]

Choices during installation#

What rubrapack writes for the choices a user makes [observed, Windows 11 26100]:

Several languages in one package#

The dialog tables hold one language at a time, but every text they show can come from a property, so one set of dialogs can speak several languages. What the engine does [observed, Windows 11 26100]:

rubrapack's layout with [ui] languages: English is language 0. For every text X and language L, RpT_X_L in the Property table holds the text with [ProductName], [Manufacturer] and [ProductVersion] put in at build time (and the title style for headings); RpT_X holds the English one. The InstallUISequence starts with type-51 actions: RPLANGUAGE = L when unset and UserLanguageID (17), then SystemLanguageID (18), is one of L's LANGIDs, else en (19); then at 21 one action per text copies [RpT_X_L] into RpT_X under RPLANGUAGE = "L", and one sets DefaultUIFont. A text that uses other properties is copied as its formatted source instead. The language page (RpLanguageDlg, 1225, a RadioButtonGroup on RPLANGUAGE) replaces the welcome and maintenance rows; its Next runs the same copies as ControlEvents, then NewDialog to the welcome or the maintenance page. A license per language is a ScrollableText per language in the same place, each with Show/Hide ControlConditions on RPLANGUAGE. The UIText table has one language: English.

34.21 What this recipe does not cover yet#

Custom actions other than the error type, the register pair and the REG_QWORD helper above; and _Validation (needed by validation tools, not by the installer). These pages grow as rubrapack implements them.