rubrapack Manual←↑→

20 Source files and the rubrapack command

A source file describes a package. It is a strict subset of TOML 1.0 and is named *.toml, so editors, GitHub and AI assistants treat it as TOML (the older *.rpk name is read the same way). Every source is valid TOML, but rubrapack refuses TOML features outside the subset instead of ignoring them.

20.1 Getting started#

rubrapack is a single program file. Download rubrapack-<version>-windows-x64.exe (rename it rubrapack.exe if you like) or rubrapack-<version>-linux-x86_64 (chmod +x it) from the releases page and run it where it lies, or put it on the PATH. There is nothing else to install: no runtime, no SDK, no libraries. The same program builds the same packages on Windows and on Linux.

This section is the short version, for readers who know installers. Part I, the tutorial, teaches the same from the beginning, one step at a time, and goes on to every table and option: start with Before you start.

A first package#

Put what you ship in a folder dist/: the program, and whatever it needs, in dist/files/ (subfolders are kept). Then let rubrapack write a starting source:

rubrapack new app.toml      # asks, then writes app.toml and checks it

It asks for the product name, the version, the folder with the files, the main program (its architecture is read from the file), the folder under Program Files, who it installs for, which sub folders are optional parts, the dialogs, a license (a LICENSE.txt, .md or .rtf next to it is offered), Korean dialogs, and the shortcuts. Enter takes the value in brackets. At the end it prints the same answers as one command (rubrapack new app.toml --dist dist --ui features ...), which a script or an AI assistant can run without questions. rubrapack new without a name asks for the file name too, and rubrapack new app (no extension) writes a fixed starter instead.

Either way, app.toml is plain text to read and change - in any text editor, or with rubrapack edit app.toml, a menu of the same questions with the current values as defaults (version, install folder, dialogs and license, optional parts, shortcuts, and matching the file list to the program folder after it changed). It changes only those values and keeps every other line, comment and table as it was. For scripts: rubrapack edit app.toml --set define.VERSION=1.1.0 --sync. This one installs the program and its files into Program Files\My App, puts it in the Start menu, and shows a dialog that lets the user change the folder:

format = 1

[package]
name = "My App"
manufacturer = "My Company"            # shown in Settings > Installed apps
version = "$(VERSION)"
arch = "x64"                           # x64, arm64 or x86
upgrade-code = "{E8C1815C-CCD7-4F3F-B914-92A4E3F3A317}"   # yours from `new`; keep it forever
ui = "installdir"

[define]
VERSION = "1.0.0"

[dir.INSTALLDIR]
path = "$(ProgramFiles)/My App"

[file.App]
dir = "INSTALLDIR"
source = "dist/app.exe"

[files.Rest]
dir = "INSTALLDIR"
glob = "dist/files/**"

[shortcut.StartMenu]
dir = "Programs"
name = "My App"
target = "file:App"
rubrapack build app.toml -o app-1.0.0.msi
rubrapack build app.toml -o app-1.0.1.msi -D VERSION=1.0.1    # the next version

That is a complete installer. It appears in Installed apps, repairs itself, and removes everything it installed. A higher version replaces the one installed; the same or an older one is refused with a message. The component GUIDs, file keys, cabinet and tables are derived from the source, so nothing but the upgrade code needs to be remembered from one version to the next.

A license page and optional parts#

Two things most installers want: the user accepts a license before installing, and some parts are optional. license adds the license page (Next stays off until "I accept" is ticked), and ui = "features" adds a tree in which the user picks the features to install. Here the program is always installed and the samples are offered but not selected:

format = 1

[package]
name = "My App"
manufacturer = "My Company"
version = "$(VERSION)"
arch = "x64"
upgrade-code = "{E8C1815C-CCD7-4F3F-B914-92A4E3F3A317}"
ui = "features"                        # welcome, license, folder, feature tree, ready
license = "LICENSE.txt"                # Next stays off until "I accept" is ticked

[define]
VERSION = "1.0.0"

[feature.Main]
title = "My App"
description = "The program itself."
required = true                        # always installed: the tree does not offer to leave it out

[feature.Samples]
title = "Samples"
description = "Example documents to try the program with."
level = 2                              # offered in the tree, not selected by default

[dir.INSTALLDIR]
path = "$(ProgramFiles)/My App"
feature = "Main"

[dir.SamplesDir]
path = "$(INSTALLDIR)/samples"
feature = "Samples"

[file.App]
dir = "INSTALLDIR"
source = "dist/app.exe"

[files.Rest]
dir = "INSTALLDIR"
glob = "dist/files/**"

[files.Samples]
dir = "SamplesDir"
glob = "dist/samples/**"

[shortcut.StartMenu]
dir = "Programs"
name = "My App"
target = "file:App"

Put the license text in LICENSE.txt (.txt, .md or .rtf) and the samples in dist/samples/. A feature with level = 2 is not installed unless the user ticks it; required keeps the tree from offering to leave a feature out. Later the user changes the choice from Installed apps (Change), and the command line does the same without dialogs: msiexec /i app.msi /qn ADDLOCAL=Samples adds the samples, REMOVE=Samples takes them out (required binds only the tree, not the command line). Features and Conditions have the rest.

Where the rest is explained#

ToRead
install, upgrade, remove and log with msiexecA first installer, Versions and upgrades
understand an error and look inside a package (lint, inspect, extract, exit codes)Checking and looking inside, Diagnostic codes
sign a packageSigning and timestamps
build in a script or CI job, or with an AI assistantStarting fast and automating

20.2 Example#

format = 1

[define]
VERSION = "1.4.0"

[package]
name = "Example App"
manufacturer = "Example"
version = "$(VERSION)"
arch = "x64"                                        # x64, arm64 or x86 - no default
upgrade-code = "{0B9A6C1E-3D2F-4A5B-8C7D-6E5F4A3B2C1D}" # generate your own once, keep it forever
language = "en-US"                                  # or "ko-KR"

[dir.INSTALLDIR]
path = "$(ProgramFiles)/Example App"

[dir.Docs]
path = "$(INSTALLDIR)/docs"

[file.MainExe]
dir = "INSTALLDIR"
source = "dist/app.txt"

[file.Guide]
dir = "Docs"
source = "dist/guide.txt"
name = "User guide.txt"
rubrapack build example.toml -o example.msi -D VERSION=1.4.1
rubrapack inspect example.msi File

20.3 Source format#

The first key of a source, before any table, is format = 1: the version of the source format, one whole number. It goes up only when sources must be written differently; new tables and keys do not change it. A source without it is taken for one of rubrapack 0.18 or earlier and draws a warning (RP1108); what changed since is then refused where it is written - format 1 starts paths with $(...) (see Paths). A source with a higher number needs a newer rubrapack.

20.4 The TOML subset#

Accepted: tables [kind] and [kind.ID], bare keys (A-Z a-z 0-9 _ -), basic strings "..." with TOML escapes, literal strings '...' (no escapes - use them for backslashes and quotes: 'SOFTWARE\Example'), decimal and 0x integers, true/false, one-type arrays, # comments. UTF-8 with or without a BOM, or UTF-16LE with a BOM; LF or CRLF.

Refused with an error: multi-line strings, inline tables, arrays of tables, dotted and quoted keys, table names with more than two parts, floats, dates, _ in numbers, octal/binary numbers, empty or mixed arrays, keys before the first table other than format. Table and key order never matters.

20.5 Tables#

TableKeys (required in bold)
[package]name, manufacturer, version (a.b.c or a.b.c.d), arch, upgrade-code, upgrade-code-x64 / -arm64 / -x86, product-code, summary-name (ASCII), language, scope (machine, user, dual), ui (none, basic, minimal, installdir, features), license (.txt, .md, .rtf), reboot (suppress/allow), cleanup (false: no cleanup task), preflight (false: no checks before it goes on), close-programs (ask, always, never), parent (an add-on: the main product's upgrade-code), remove-addons (true: removing this product removes its add-ons), replaces (upgrade codes of products this package takes the place of), downgrade-message, compress (none, mszip, mszip:0..mszip:9, lzx, lzx:15..lzx:21; default mszip:6), cab (embed or external), cab-max-size (MiB), refuse-upgrade-below, refuse-upgrade-message
[define]variables: NAME = "value"
[feature.ID]title, description, level (1-32767), hidden, parent, required, follow-parent, when, default-when
[dir.ID]path = $(Base)/relative/path, feature, guard (true: see Guarding the install folder)
[file.ID]dir, source, name, vital (default true), any-arch, feature, component-guid, keep, when
[files.ID]dir, glob, vital, any-arch, feature, keep, when
[folder.ID]dir, name, keep, feature
[arp]no-modify, no-repair, no-remove, help (URL), about (URL), icon (.ico) - how the product shows in Installed apps
[property.ID]value, secure, hidden - an upper-case public property
[action.ID]run (file:ID of an .exe in this package), do, undo, check
[registry.ID]root (HKLM, HKCU, HKCR, HKMU), key, name, value, type, remove, keep, view, with, feature, when
[remove.ID]dir, name (* and ?; omitted = the folder itself), on (install, uninstall, both), upgrade (false: not when an upgrade removes this version), feature
[ini.ID]dir, file, section, key, value, mode (set, add, remove), feature, when
[require.ID]condition, message
[search.ID]property (or a dir ID), kind (registry: root, key, name, view; file: path, file, min-version; dir: path; component: component-guid)
[service.ID]file (file:ID of an .exe), name, display-name, description, start (auto, demand, disabled), account (LocalSystem, LocalService, NetworkService), args, start-on-install
[assoc.ID]extension (.ext, lower case), prog-id, target (file:ID of an .exe), description, icon (file:ID), args (default "%1"), content-type, perceived-type, default (false: only under "Open with")
[menu.ID]on (file types, "*", "folder", "background", "drive", "assoc:ID"; an item of a sub-menu has none), text or text-xx, target (file:ID of an .exe; none = a sub-menu), args (default "%1"), icon (file:ID), parent (a sub-menu's ID), multi (each, one, single), extended, windows11 (default true) - an item of Explorer's right-click menu: see Explorer's right-click menu
[protocol.ID]name (the scheme, lower case), target (file:ID of an .exe), description, args (default "%1")
[com.ID]file (file:ID of an .exe or .dll), class ({GUID}), description, threading (sta default, mta, both, neutral; a DLL), args (a program), prog-id, app-id ({GUID}), surrogate (a DLL in dllhost), typelib ({LIBID}), typelib-version (default "1.0"), typelib-file (default: the server), msi-only - a COM class, see COM classes
[handler.ID]kind (thumbnail, preview, property), class (a [com.*] DLL class), types ([".ext", ...]), description (a preview handler's name), msi-only - an Explorer handler, see Explorer handlers
[font.ID]file (file:ID of a file in a dir with path = "$(Fonts)"), title
[permission.ID]target (dir:ID, file:ID, registry:ID), sddl
[env.ID]name, value, mode (set, append, prepend), keep, feature, when
[copy.ID]source (file:ID), dir, name (default: the source's name)
[merge.ID]source (an .msm merge module), dir (where the module's own root goes), feature, config (["Name=value", ...] for a configurable module); in an MSIX its files and registry values only
[module]name (the module's ID: letters, digits, _; at most 35), manufacturer, version, arch, id (the module's GUID, kept in every version), language (neutral default, en-US, ko-KR), compress (none, mszip, mszip:0..mszip:9) - in place of [package]: a merge module, see Writing a merge module
[ui]install-dir (a dir ID; default INSTALLDIR), banner (.bmp), launch (file:ID), launch-args, launch-checked, save-log (default true), languages (added to English, e.g. ["ko"]), license-xx, name-xx, font-xx, langid-xx - see Several languages
[ui-text.ID]text or text-xx - replaces one built-in dialog text
[dialog.ID]after (a built-in page or another [dialog.*]), title, description, title-xx, description-xx
[dialog-control.ID]dialog, type (text, checkbox, edit, radio, combo), x, y, width, height, text, property, values, labels, text-xx, labels-xx
[shortcut.ID]dir (a dir ID, or Programs, Desktop, StartMenu, Startup), name, target (file:ID), args, description, working-dir (a dir ID), icon (.ico), when
[msix]identity-name, publisher, display-name, display-name-xx, publisher-display-name, publisher-display-name-xx, min-version, capabilities (names), file-system-virtualization, registry-virtualization (default true), main-package, main-publisher, modification, language-packs (default true), appinstaller-uri, package-uri, update-hours (0-255, default 24), update-prompt, update-blocks, update-background - see MSIX packages
[msix-app.ID]executable (a [file.*] ID), display-name, display-name-xx, description, description-xx, logo-150, logo-44, store-logo (each with optional .scale-NNN variants), background-color (transparent default, #RRGGBB), hidden (no Start menu entry)
[msix-dependency.ID]name, publisher, min-version - a framework package the MSIX needs - MSIX only
[msix-extension.ID]kind (alias: alias; startup-task: task-id, display-name, enabled; firewall: direction (in, out), protocol (tcp, udp), ports (8080 or 8000-8100), profile (all, domain, private, public), file (default: the application's program); com-server: file (an .exe or .dll), class ({GUID}), display-name, args (.exe), threading (sta default, mta, both, neutral; .dll); toast: class, file (default: the application's program), args (default -ToastActivated); context-menu: file (a .dll), class, types ([".txt", "*"]), verb (default: the table ID), threading), app (an [msix-app.*] ID; default the first) - MSIX only
[chain]name, manufacturer, version, arch (the setup program's: x64, x86, arm64), elevate (default true) - a chain source: see Several packages in one setup
[chain-package.ID]source (an .msi), properties (msiexec properties), vital (default true)

$(Base) in a dir path is another dir ID or a Windows folder: $(ProgramFiles) (64-bit for x64/arm64, 32-bit for x86), $(ProgramFiles(x86)), $(CommonProgramFiles), $(APPDATA), $(LOCALAPPDATA), $(ProgramData), $(TEMP), $(SystemRoot), and the folders without an environment variable $(StartMenu), $(Programs), $(Desktop), $(Startup), $(System), $(Fonts) - see Windows names. A path may also be a folder alone (path = "$(Fonts)") for files that go into that folder itself.

IDs are [A-Za-z_][A-Za-z0-9_]* (at most 72 characters, 38 for features) and must differ across dirs, files and features.

MSIX packages#

The same source builds an MSIX package when the output ends in .msix: one desktop application that runs with full trust, for one architecture. Two more tables say what only MSIX needs:

[msix]
identity-name = "Example.App"               # 3-50 characters: A-Z a-z 0-9 . -
publisher = "C=KR, O=Example, CN=Example"   # the signing certificate's subject, last part first
publisher-display-name = "Example"          # default: [package] manufacturer
min-version = "10.0.17763.0"                # the oldest Windows it installs on (this is the default)

[msix-app.Main]
executable = "MainExe"                      # the [file.*] that starts the app
display-name = "Example App"                # default: [package] name
description = "An example"                  # default: the display name
logo-150 = "assets/Square150x150.png"       # PNG, 150x150
logo-44 = "assets/Square44x44.png"          # PNG, 44x44
store-logo = "assets/StoreLogo.png"         # PNG, 50x50

Registering with an installed program: [action.ID]#

[action.Tip]
run = "file:MainExe"      # an .exe this package installs
do = "--register"         # run after the files are installed, and again on repair
undo = "--unregister"     # run on removal, before the files are removed

rubrapack turns the pair into deferred, elevated actions with their rollback twins, so removal and upgrade are all-or-nothing: if anything fails later (or do/undo itself exits non-zero), the files come back and the other command restores the registration. Both commands must be safe to run twice and must finish without asking anything - they run without a window, and nothing waits for a user. The arguments are passed as written (they are not formatted strings). check has no effect yet and only draws a warning (RP1318).

Registry values: [registry.ID]#

[registry.InstallDir]
root = "HKLM"
key = 'SOFTWARE\Example'          # literal strings keep the backslashes
name = "InstallDir"               # omit it for the key's default value
value = "[INSTALLDIR]"            # an MSI formatted string: [PROPERTY], [#FileID], [\[] for "["

type is string (default), expand, dword (an integer, 0 to 0xFFFFFFFF), qword (an integer, or "0x" and up to 16 hex digits), binary (hex digits) or multi (an array of strings). Windows Installer cannot write REG_QWORD itself; rubrapack adds its small helper DLL to the package for it, which also restores the previous value if the installation fails. Each value is its own component, removed at uninstall (keep = true leaves it); with = "file:ID" puts it in that file's component instead. In a 64-bit package values go to the 64-bit registry view; view = "32" writes to the 32-bit view. remove = true (without value) deletes the named value - or the whole key when name is omitted - during installation.

Shortcuts: [shortcut.ID]#

[dir.Menu]
path = "$(Programs)/Example"         # Start menu > Example

[shortcut.Settings]
dir = "Menu"
name = "Example settings"         # ".lnk" is added
target = "file:MainExe"
args = "--settings \"[INSTALLDIR]\""

A shortcut belongs to its target file (and its feature); folders created for it are removed at uninstall. In a per-machine package Programs and Desktop are the all-users Start menu and the Public Desktop.

In an MSIX a shortcut starts an application (its target must be an [msix-app.*] executable): one in Programs or StartMenu is that application's own Start menu entry (no args); one on the Desktop is written into the manifest and needs min-version = "10.0.19645.0" or later (RP1614). Other folders, working-dir and [...] in args are errors there (RP1613); for Startup use a startup task.

[assoc.Doc]
extension = ".exdoc"
prog-id = "Example.Document"      # tables sharing it describe one kind of document
description = "Example document"
target = "file:MainExe"
args = "--open \"%1\""            # the default is "%1"

[protocol.Link]
name = "example"                  # example:... opens the program
target = "file:MainExe"

In an MSI they are registry values under HKEY_CLASSES_ROOT, in the program's component: the extension's default value is the prog-id, the prog-id has the description, DefaultIcon (icon, or the program's first icon) and shell\open\command; a scheme gets URL Protocol. HKEY_CLASSES_ROOT follows the installation: per machine they land in HKLM\Software\Classes, per user in HKCU\Software\Classes, and removal takes them away. A type that only this program claims opens in it directly; where the user has chosen another program, Windows keeps that choice.

In an MSIX they go into the manifest of the application whose executable is the target (a file type association, a protocol), and args must be plain text. icon is not used there: the application's logo stands for the file type.

A file type may also say what it holds and whether the program takes it:

[assoc.Picture]
extension = ".png"
prog-id = "Example.Picture"
target = "file:MainExe"
content-type = "image/png"        # the type's media type
perceived-type = "image"          # image, text, audio, video, compressed, document, system, application
default = false                   # offer the program under "Open with", do not claim the type

Every [assoc.*] lists its prog-id under the extension's OpenWithProgids, so the program is offered under "Open with"; with default = false that is all it does (the extension's own value is left alone). In an MSIX content-type goes into the manifest; perceived-type and default are an MSI's.

Explorer's right-click menu: [menu.ID]#

[menu.Convert]
on = [".png", ".jpg", "folder"]   # where it shows
text = "Convert with Example"
text-ko = "Example 로 변환"        # a text per language
target = "file:MainExe"
args = "--convert \"%1\""         # "%1" is the selected path; the default is "%1" alone
icon = "file:MainExe"             # default: the program's own icon

[menu.Tools]                      # no target: a sub-menu
on = "*"
text = "Example tools"

[menu.Checksum]
parent = "Tools"                  # an item of that sub-menu; it shows where the sub-menu shows
text = "Checksum"
target = "file:MainExe"
args = "--sum \"%1\""
multi = "single"                  # only when one thing is selected

An item starts a program of the package with what was right-clicked. on is one place or a list: a file type (".png"), "*" (every file), "folder", "background" (the empty part of an open folder: %1 is then that folder), "drive", or "assoc:ID" (the file type of an [assoc.*], under its prog-id). multi says what several selected items mean: "each" (the default: one run per item), "one" (one run with all of them: write %* in args for all the paths) or "single" (the item shows only for one). extended = true shows the item only with Shift held. One level of sub-menus.

The same table serves both menus of Windows:

What to expect on Windows 11: an item appears some seconds after the installation ends; two or more items of one package for the same kind of thing are put by Windows under one entry named after the product; "drive" and windows11 = false items are in the classic menu only. To see what the menu part does, set the value MenuLog (a file's path) under HKCU\Software\rubrapack: each choice and the command it starts are appended there.

COM classes: [com.ID]#

[com.Widget]
file = "file:WidgetDll"           # an .exe or a .dll of this package serves the class
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D35}"
description = "Example widget"
prog-id = "Example.Widget"        # what scripts name it by
threading = "both"                # a DLL's apartment: sta (default), mta, both, neutral
app-id = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D36}"
surrogate = true                  # may run in dllhost, out of the caller's process
typelib = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D37}"
typelib-version = "1.2"           # major.minor in hex digits; typelib-file names another file

[com.Server]
file = "file:MainExe"             # a program: LocalServer32, started with its args
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D38}"
args = "-Embedding"

In an MSI these are registry values under HKEY_CLASSES_ROOT, in the server's component, as for file types: CLSID\{class} with its description; InprocServer32 (the DLL and ThreadingModel) or LocalServer32 (the program, quoted, then args); with a prog-id, CLSID\{class}\ProgID and <prog-id>\CLSID; with an app-id (or surrogate, which uses the class ID when there is no app-id), the class's AppID value and AppID\{app-id}, with DllSurrogate for a surrogate; with a typelib, CLSID\{class}\TypeLib and TypeLib\{typelib}\<version> (0\win64 or 0\win32 by the package's architecture, FLAGS, HELPDIR). Removal takes them away. The values are written as Registry rows - as Microsoft's tools write a class that is not advertised - rather than through the Class, ProgId, TypeLib and AppId tables, whose classes Windows Installer registers as advertised (installed on first use); a per-user installation puts them in HKCU\Software\Classes. Microsoft's validation (ICE33) warns about such rows - it would have the tables used - and the warning is expected for [com]. A class ID, a prog-id and an ID are each used once (RP1301); threading and surrogate are for a DLL, args for a program (RP1316).

In an MSIX a [com] becomes a class of the package's COM catalog (com:ComServer: an ExeServer for a program, a SurrogateServer for a DLL) and its prog-id a com:ProgId; app-id and typelib stay with the MSI, which a warning says (RP1612).

Explorer handlers: [handler.ID]#

[handler.Thumbs]
kind = "thumbnail"                # the picture Explorer shows for the file
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D41}"   # a [com.*] class of a DLL of the package
types = [".exdoc"]

[handler.Preview]
kind = "preview"                  # what the preview pane shows
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D43}"
types = [".exdoc"]
description = "Example document preview"
msi-only = true

[handler.Props]
kind = "property"                 # the file's properties (title, author, ...) for Explorer and search
class = "{8D1E2F30-4A5B-4C6D-8E7F-901A2B3C4D42}"
types = [".exdoc"]
msi-only = true

The handler is a COM class in a DLL of the package, written as a [com.*] (with threading), and [handler] tells Windows which file types it serves. In an MSI: a thumbnail or preview handler is the ShellEx value of each type under HKEY_CLASSES_ROOT ({E357FCCD-...} thumbnail, {8895B1C6-...} preview); a preview handler is also on the PreviewHandlers list, and its class gets the AppID of Windows' preview host (prevhost.exe, 64-bit or 32-bit by the package's architecture), so leave app-id and surrogate out of its [com]; a property handler is under PropertySystem\PropertyHandlers in HKLM, which Windows reads per machine only, so the package needs scope = "machine". A type has one handler of each kind (RP1301).

In an MSIX thumbnail and preview handlers go into a file type association of the package (desktop2:ThumbnailHandler, desktop2:DesktopPreviewHandler, with their classes in the package's COM catalog), one association per set of types (or the [assoc] that opens them); Explorer uses them while the package is installed. A package serves its classes from a surrogate (dllhost), so give a preview handler's class threading = "sta": with another model its window may get a thread without a message loop and stay blank (a warning, RP1612). A packaged property handler gave Explorer no values on Windows 11 where the same class installed by an MSI did, so an MSIX refuses that kind (RP1612) and msi-only = true keeps it for the MSI.

MSIX only: [msix-extension.ID]#

[msix-extension.Cli]
kind = "alias"
alias = "example.exe"             # typed in a console, it starts the application

[msix-extension.Boot]
kind = "startup-task"             # starts with Windows once the application has run once
display-name = "Example"          # the name in Task Manager; task-id defaults to the table ID
enabled = true
[msix-extension.Web]
kind = "firewall"                 # a Windows Firewall rule while the package is installed
direction = "in"
protocol = "tcp"
ports = "8080"
profile = "private"               # default "all"; file = "file:ID" for another program

An MSI build leaves these out. Fonts ([font.*]) in an MSIX are shared with other applications (uap4:SharedFonts) from the package's Fonts folder; title is not used there.

[msix-extension.Server]
kind = "com-server"               # a COM class the program (or a DLL) serves
file = "file:MainExe"
class = "{7A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C41}"
args = "-Embedding"

[msix-extension.Toast]
kind = "toast"                    # a click on the app's notification starts it as this class
class = "{7A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C43}"

[msix-extension.Menu]
kind = "context-menu"             # an Explorer context menu verb, handled by a DLL (IExplorerCommand)
file = "file:MenuDll"
class = "{7A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C44}"
types = [".txt", "*"]             # file types; "*" = every file

These become the application's com:ComServer (an ExeServer for a program, a SurrogateServer for a DLL), desktop:ToastNotificationActivation with the program registered for its class and args, and desktop4:FileExplorerContextMenus with the DLL as the class. Windows lists the classes in its packaged COM catalog while the package is installed. The context menu shows only if the DLL implements the verb (IExplorerCommand); rubrapack registers it, it does not write it.

A [service.*] goes into an MSIX as a packaged service (desktop6:Service, in the first application, with the capability packagedServices, and localSystemServices for account = "LocalSystem"): its name, start and account as in the MSI, args as plain text. display-name and description are not used there, and the service starts by start, not by start-on-install. It needs min-version = "10.0.19041.0" or later (RP1614). Removing the package stops and removes the service; firewall rules go with the package too.

Removing and copying: [remove.ID], [copy.ID]#

[remove.ID] deletes files matching name in dir - for example *.log an older version left, at on = "install", or files the program writes at run time, at on = "uninstall". Without name it removes the folder itself when it is empty. A failed installation puts removed files back.

An upgrade removes the old version too, and with it runs the old version's on = "uninstall" rows. upgrade = false keeps them out of that: the files go only when the product (or its feature) is really removed - for files the new version still wants, or that programs still running the old version may read again. It is done by rubrapack's helper DLL instead of the RemoveFile table: the files are moved to a backup folder (the volume's Config.Msi, or the temporary folder for a per-user package), put back if the removal fails, and deleted when it succeeds; a file a program holds is deleted at the next restart. It takes effect from the first version built with it, since an upgrade runs the old version's own rows. [copy.ID] installs a second copy of a file of this package into another folder; it goes and comes with its source. Folders that existed before the installation are never removed.

Environment variables: [env.ID]#

A system (machine-wide) variable. mode = "set" (default) replaces it, append/prepend add ;value to the end or value; to the front of what is there. Uninstall undoes exactly that: a set variable is deleted, an appended part is taken out and the rest kept. keep = true leaves it.

INI files: [ini.ID]#

mode = "set" (default) writes key=value in [section] of the file, add appends the value to a comma-separated list (a becomes a,b), remove deletes the key during installation. Uninstall takes out what set and add wrote. remove runs before files are installed, so it is for INI files an older version left behind.

Several packages in one setup: [chain]#

A source with [chain] builds a setup program instead of a package: rubrapack build suite.toml -o setup.exe. The setup program holds the [chain-package.*] packages, in the order the source lists them, each with its SHA-256; it checks them all, then installs them one after the other with Windows Installer. A package already installed (its ProductCode) is skipped. When a vital package (the default) fails, the chain stops and returns that package's error; the package rolls itself back, the ones before it stay. properties are passed to that package as on msiexec's command line.

Merge modules: [merge.ID]#

A merge module (.msm) is a piece of an installer that another vendor ships for its runtime or library. [merge.ID] copies its tables into the package: the module's files, components, registry values and its own actions. Its root folder becomes dir, its components join feature (default: the dir's feature, or Main), and its cabinet is embedded as a second cabinet of its own. A standard action the module's tables need and the package lacks (WriteRegistryValues, for example) is added at its usual place. rubrapack does not merge a module with non-ASCII text in a code page other than UTF-8 (RP1517). In an MSIX a module gives its files (below dir, or in the virtual file system when they go to a standard folder such as the system folder) and its registry values (in the package's hives); a module that needs more - custom actions, conditions on components, environment or INI entries, services, fonts, COM registration - or a value filled in at install time is refused there (RP1517; msi-only = true keeps it for the MSI). The merged module's ModuleSignature and ModuleComponents rows stay in the package, as Microsoft's merge tool leaves them; the tables the module lists in ModuleIgnoreTable do not go in.

A configurable module (one with a ModuleConfiguration table) asks for values - a text, a number, a set of bits, or a key such as one of its folders - which config gives, one "Name=value" string per item:

[merge.Runtime]
source = "runtime.msm"
dir = "INSTALLDIR"
config = ["ServerName=example.com", "Port=8080", "DataDir=DataFolder.6E0A1C52_8F3B_4B7D_9A21_3C4D5E6F7A99"]

An item not given takes the module's default, as when a merge tool declines to answer. The values go where the module's ModuleSubstitution table says, the way Microsoft's merge tool puts them there: a bitfield item sets only the bits of its mask, a key item may name one part of a key with [=Item;2], the null GUID becomes the feature's name, and a default row that every answered KeyNoOrphan item replaced is left out. A key value is written as the module's own key (with its GUID) and in the module's escaped form (\; for a semicolon). An unknown item, a number item without a number and an empty value where the module needs one are refused (RP1517), as is config for a module that takes none. The names, kinds and defaults of the items are in the module's ModuleConfiguration table: rubrapack inspect runtime.msm ModuleConfiguration.

Writing a merge module: [module]#

A source with [module] in place of [package] builds a merge module for others to merge: rubrapack build runtime.toml -o runtime.msm.

format = 1

[module]
name = "ExampleRuntime"
manufacturer = "Example"
version = "2.1.0"
arch = "x64"
id = "{6E0A1C52-8F3B-4B7D-9A21-3C4D5E6F7A99}"     # the module's own GUID, kept in every version

[dir.RuntimeDir]
path = "$(TARGETDIR)/Example Runtime"             # TARGETDIR: where the package puts the module

[files.Runtime]
dir = "RuntimeDir"
glob = "runtime/*"

[registry.Home]
root = "HKLM"
key = 'SOFTWARE\Example\Runtime'
name = "Home"
value = '$(RuntimeDir)'                           # the folder the package chose for it

Searching and requiring: [search.ID], [require.ID]#

A search runs before anything else and puts what it found into a public property (empty when nothing is found): a registry value's data, the full path of a file (path = a known folder and a relative path, like $(System) or $(ProgramFiles)/Example; min-version for program files), a folder, or the key file of another product's component. A requirement stops a first installation with its message when its condition is false (repair and removal are never blocked); conditions use Windows Installer's syntax (VersionNT >= 603, FOUND_TOOL, NOT OLDSETTING) and may test search results.

[search.Tool]
property = "FOUND_TOOL"
kind = "file"
path = "$(System)"
file = "tool.exe"

[require.Tool]
condition = "FOUND_TOOL"
message = "[ProductName] needs tool.exe."

A registry or dir search may name a dir instead of a property: the folder it finds becomes that dir's default. This is how a package remembers the folder the user chose: write the folder down, and look it up in the next version. A registry value is used only when that folder still exists; a folder given on the command line (msiexec /i app.msi INSTALLDIR=D:\Apps\Example\) still wins.

[registry.RememberDir]
root = "HKLM"
key = "Software\\Example"
name = "InstallDir"
value = "[INSTALLDIR]"

[search.PreviousDir]
property = "INSTALLDIR"           # a dir: its default, when the registry holds an existing folder
kind = "registry"
root = "HKLM"
key = "Software\\Example"
name = "InstallDir"

Services, fonts and permissions#

[service.ID] installs a service run by an .exe of the package: it is stopped before its files change and at uninstall, deleted at uninstall, and started after installation with start-on-install = true. [font.ID] registers a font file that the package installs into the Fonts folder; without title Windows reads the name from the TrueType/OpenType file. [permission.ID] sets an SDDL security descriptor on a folder the package creates, one of its files, or a registry value it writes (this raises the package to Windows Installer 5.0).

Per-user and dual packages: scope#

scope = "machine" (default) installs for everyone and needs administrator rights. scope = "user" installs for the current user without elevation: ProgramFiles becomes %LOCALAPPDATA%\Programs, Programs and Desktop are the user's, registry values go to HKCU (or HKMU), environment variables are the user's, and do/undo actions run as the user; asking for a per-machine installation is refused. scope = "dual" installs per user by default and per machine from an elevated prompt with msiexec /i x.msi ALLUSERS=1 MSIINSTALLPERUSER=""; its registry values use HKMU, which is HKLM or HKCU as installed. Services, fonts, permissions and the machine folders (SystemRoot, System, Fonts, ProgramData) need scope = "machine".

Cabinets#

Files are compressed into one cabinet embedded in the package, with MSZIP (deflate) or LZX (compress = "lzx": typically 5 to 20% smaller, about 4 MB a second on one processor where MSZIP uses them all). cab-max-size = N starts a new cabinet after N MiB of files; cab = "external" writes the cabinets next to the package as <name>.cab (or <name>-1.cab, <name>-2.cab, ...), which must travel with it; without cab-max-size external cabinets are split before 2 GiB each. Windows Installer cannot open a package of 2 GiB or more, so an embedded cabinet that large is an error (RP1516) that asks for cab = "external". rubrapack never overwrites an existing cabinet and writes the package last: it is built in <out>.rp-map (the package's bytes go straight into that file, not into memory) and renamed to its name only when everything succeeded, so a failed build leaves no package. Compression runs on every processor (--jobs N to use fewer); the bytes are the same either way. Every package also carries the administrative (msiexec /a, an uncompressed network image) and advertisement (msiexec /jm) sequences.

Only these fields are MSI formatted strings: registry value (and multi items), shortcut args, environment value, INI value, requirement message, service args. Everywhere else rubrapack writes the text exactly as given.

Installed apps entry and properties#

[arp] sets how the product appears in Settings > Installed apps (no-modify, no-repair, help, about). [property.NAME] adds a public property; secure = true lets it reach the elevated part of the installation, hidden = true keeps its value out of logs. Names the installer or rubrapack set themselves (ARP*, MSI*, RP_*, ALLUSERS, REBOOT, ...) are refused.

Uninstall in Installed apps runs msiexec /qb /x {ProductCode}: Windows Installer's own reduced window, not the package's dialogs, and when a program with a window holds one of the files, its own files-in-use box - in English, with Cancel as the default (Ignore goes on). no-remove = true turns Uninstall off for the product; Modify, still there, runs the package's dialogs (msiexec /i), whose Remove leads to the package's own files-in-use dialog with Continue as the default. Worth it for an input method or a shell extension, which nearly every program holds; it needs [package] ui = minimal, installdir or features (the sets with a Remove page) and no no-modify.

Installing from the command line#

Everything the dialogs choose can be given to msiexec instead:

PropertyWhat it does
INSTALLDIR=D:\Apps\Example\the install folder (any dir with an upper-case ID)
ADDLOCAL=Core,Extrainstall these features (ADDLOCAL=ALL: every feature)
REMOVE=Extraremove these features from an installed product (REMOVE=ALL: everything)
INSTALLLEVEL=3install every feature whose level is at most 3
RPLANGUAGE=kothe dialogs' language (with [ui] languages)
ALLUSERS=1 MSIINSTALLPERUSER=""a dual package for everyone (default: just the current user)
DESK=1, APP_MODE=serveryour own properties: when conditions, dialog values

Installing without a window#

Every package rubrapack builds installs, repairs, upgrades and uninstalls from the command line with no user interface:

msiexec /i example.msi /qn /l*v install.log
msiexec /x {ProductCode} /qn

Exit code 0 is success, 3010 success with a restart needed (rubrapack never restarts the machine itself), anything else a failure after which the machine is as before.

Dialogs: ui#

ui in [package] picks one of the built-in dialog sets. Without it (none) the package shows only Windows Installer's own progress bar.

uiInstallingAlready installed
basicprogress, then finished (or error)progress, finished
minimalwelcome, the license if there is one, progress, finishedrepair or remove
installdirwelcome, license, install folder (with a folder browser), ready, progress, finishedrepair or remove
featuresas installdir, plus a feature tree and the disk space it needsrepair or remove

Every set also has the cancel question, the error dialog, the files-in-use list and the out-of-disk-space warning. The files-in-use list comes when a program with a window holds a file being replaced or removed - for an input method or a shell extension, nearly every program. Its default button is Continue: the files are replaced at once, programs already open keep the old files until they are reopened, and with reboot = "suppress" (the default) no restart is asked for (FilesInUseText; with reboot = "allow", FilesInUseTextRestart, which says Windows may ask to restart). The dialogs only collect values that all have defaults, so /qn still installs without a window.

format = 1

[package]
ui = "installdir"
license = "LICENSE.txt"           # the Install/Next button stays disabled until it is accepted

[ui]
install-dir = "APPDIR"            # the dir the user may change (default INSTALLDIR)
banner = "banner.bmp"             # the strip at the top; about 493 x 58 pixels

[ui-text.WelcomeText]
text = "This will install [ProductName]. Close other programs first."

A .txt or .md license is shown as plain text (Korean, emoji and any other characters are kept); an .rtf license is used as it is. Without banner the strip is plain white. [ui-text.ID] replaces one text; the texts are MSI formatted strings, so [ProductName] is replaced and a literal [ is written [\[]. The IDs are: Back, Next, Cancel, Install, Finish, OK, Yes, No, Retry, Ignore, Abort, Exit, Browse, WelcomeTitle, WelcomeText, LicenseTitle, LicenseText, LicenseAccept, DirTitle, DirText, DirLabel, BrowseTitle, BrowseText, BrowseLookIn, BrowseFolder, BrowseUp, BrowseNew, CustomizeTitle, CustomizeText, Reset, DiskCost, DiskCostTitle, DiskCostText, ReadyTitle, ReadyText, ProgressTitle, ProgressText, ProgressStatus, ExitTitle, ExitText, UserExitTitle, UserExitText, FatalTitle, FatalText, CancelText, FilesInUseTitle, FilesInUseText, FilesInUseTextRestart, Continue, OutOfDiskTitle, OutOfDiskText, MaintTitle, MaintText, Repair, RepairText, Remove, RemoveText, LanguageTitle, LanguageText, DirGuardText (the guard's message below), PreflightAsk, PreflightSilent, PreflightFolder, PreflightCache (the preflight's messages), and for a removal - the maintenance page's Remove, or REMOVE=ALL - RemovalProgressTitle, RemovalExitTitle, RemovalExitText, RemovalUserExitTitle, RemovalUserExitText, RemovalFatalTitle, RemovalFatalText (another language that does not give these uses its install texts). In button texts & marks the access key (&Next is Alt+N).

Several languages#

The dialogs are in English. [ui] languages adds other languages to the same package; then the first page asks for the language, and every page after it - welcome, license, folder, your own pages, ready, progress, finished, and the cancel, error, files-in-use, disk-space and maintenance pages - speaks the one chosen. The choice made for the user in advance is the first added language whose LANGIDs hold the user's regional format (UserLanguageID), then the system locale (SystemLanguageID), else English. RPLANGUAGE=ko on the command line makes that choice instead (the language page still shows, with it selected); a silent installation (/qn) shows nothing and needs no choice. With English alone there is no language page.

[ui]
languages = ["ko"]                # English is always there, and the default
license-ko = "LICENSE-ko.txt"     # a license per language (default: [package] license)

[ui-text.WelcomeText]
text-ko = "[ProductName]을(를) 설치합니다."     # text = every language, text-xx = one

[dialog.Options]
after = "RpInstallDirDlg"
title = "Options"
title-ko = "선택 사항"

[dialog-control.Mode]
# ...
labels = ["&Typical", "&Portable"]
labels-ko = ["표준(&T)", "휴대용(&P)"]

Icon, the finished page, and the scope page#

Your own dialog pages: [dialog.ID], [dialog-control.ID]#

With minimal, installdir or features you can add pages to the built-in flow. A page gets the same banner and Back / Next / Cancel buttons as the others; you place the controls in its body.

[dialog.Options]
title = "Options"                 # the banner heading; [ProductName] when omitted
description = "Choose how to install."
after = "RpInstallDirDlg"         # shown right after this page

[dialog-control.ModeLabel]
dialog = "Options"
type = "text"
x = 20
y = 55
width = 330
height = 12
text = "Installation &mode:"

[dialog-control.Mode]
dialog = "Options"
type = "radio"
x = 20
y = 70
width = 200
height = 42                       # at least 12 per value
property = "APP_MODE"
values = ["typical", "portable", "server"]
labels = ["&Typical", "&Portable", "&Server"]

[property.APP_MODE]
value = "typical"                 # the default, also for a silent installation

Architectures and upgrade families#

arch is the source's own architecture and upgrade-code its upgrade family. To build the same source for another architecture (--arch), give that architecture its own family with upgrade-code-x86, upgrade-code-arm64 or upgrade-code-x64; without it the build is refused. Windows Installer's upgrade detection cannot tell architectures apart, so sharing one code would make installing one architecture remove the other. An MSIX has no upgrade code, so .msix and .msixbundle builds do not need these.

Versions that must be removed first#

refuse-upgrade-below = "1.0.0" refuses to upgrade an installed version below 1.0.0 and tells the user to remove it first; the message (refuse-upgrade-message, or a default one) always ends with the command that does it: msiexec /x {ProductCode} /qn MSIRESTARTMANAGERCONTROL=Disable. Use it when older versions were built by another tool without MSIRESTARTMANAGERCONTROL=Disable: removing such a version inside an upgrade would try to close every program that has its files loaded (see formats/msi-package.md, "Files in use").

Before it goes on: the preflight#

Before an installation, an upgrade or a removal changes anything, the package looks:

close-programs = "always" closes without asking, "never" never closes and never asks (also at /qn); "ask" is the default. For an input method or a shell extension, "never" is the usual choice: nearly every open program has its DLL loaded, so "ask" would put the question at every upgrade and removal and stop every silent one, while the files are replaced safely without closing anything (the old copies go through the cleanup task). RPCLOSE=yes or RPCLOSE=no on the command line still wins over the package's setting. preflight = false leaves the whole step out. The texts are PreflightAsk, PreflightSilent, PreflightFolder and PreflightCache ([ui-text.*]; English and Korean are built in, another language without them gets English).

Windows Installer looks after two things itself: while another installation is running, a second one ends at once with 1618; and after an installation that was cut off (a crash, a power cut), the next one first undoes the unfinished one. After a removal that was cut off, that undo puts the product back and the removal ends with 1605: run it once more.

Cleaning up later: the cleanup task#

A file a running program holds cannot always be removed at once: Windows Installer moves what it can aside and queues the rest for deletion at the next restart, and an input method or a shell extension may keep a computer from restarting for weeks. So a package that removes or replaces files (a removal, an upgrade, a repair) registers a scheduled task, rubrapack cleanup {ProductCode} (per user, followed by the user's SID). Two minutes later, then at every logon and every 15 minutes, it deletes what this installation left for the restart - files in the package's folders and the installer's backup copies of them in Config.Msi - as soon as nothing holds them, then the package's folders that were left only because of them, and finally itself. With nothing to do it goes at its first run; after 30 days it gives up and leaves the rest to the restart. It runs as SYSTEM for a per-machine package and as the user, never elevated, for a per-user one, from a folder only that account can write (%ProgramData%\rubrapack\cleanup\{ProductCode}, or the user's %LOCALAPPDATA%; per machine, a rubrapack folder there that someone else made first is not used and no task is registered), and it never deletes anything else: only what this installation queued, only while the same file is still at that path, and not a file an installed product now has there. [package] cleanup = false leaves it out.

A per-machine package also protects its own files from an earlier removal: when another product was removed while a program held one of its files, the file's deletion stays queued for the restart, and if this package then installs an identical file at that path, Windows Installer keeps the one already there - so the restart would delete it. The package takes such deletions back as it installs (and queues them again if the installation fails).

There is no separate uninstall program: Windows' Installed apps removes an MSI with Windows Installer, and what a removal leaves is the cleanup task's job.

Add-ons removed with their product: parent, remove-addons#

An add-on is a package of its own that extends another product - a language pack, a plug-in - and makes no sense without it. The add-on names the main product by its upgrade code, [package] parent = "{...}"; installing it writes its product code under SOFTWARE\rubrapack\Addons\<that upgrade code> (HKLM per machine, HKCU per user), and removing it takes the value away again. The main product sets remove-addons = true: when it is really removed (not when an upgrade replaces it), its cleanup task, started at once, waits for the removal to end and then removes every add-on still named there with msiexec /x {ProductCode} /qn, whichever way the main product was removed (Installed apps, its maintenance page, msiexec /x). An add-on that cannot be removed at that moment (another installation running) is tried again at the task's next run. Two packages cannot be removed in one Windows Installer run, so the add-ons go a few seconds after the main product, not before it; build them so that they can be removed without it. It takes effect from the first main version built with it, and needs the cleanup task (cleanup = false is an error); both keys are for MSI packages only.

Taking the place of other products: replaces#

replaces = ["{UpgradeCode}", ...] (at most 16) names products this package takes the place of - say, add-ons that were separate packages and are now features of this one. Installing this package removes every installed version of them in the same run, as it removes an older version of itself (RemoveExistingProducts; their own removal runs, with UPGRADINGPRODUCTCODE set), so no two products own the same files. The codes must not be this package's own upgrade code or its parent. MSI only.

Wildcards: [files.ID]#

glob is a source path with * (any characters within one folder), ? (one character) and ** (any number of folders). Matches are sorted by name, so the result never depends on the file system. Folders below the first wildcard are recreated under dir: glob = "dist/layouts/**/*.jmt" installs dist/layouts/de/x.jmt as <dir>/de/x.jmt. No match, a symbolic link, or the output file itself among the matches is an error.

Empty folders: [folder.ID]#

Creates the folder name inside dir even when no file goes there. With keep = true the folder stays after uninstall (for data the program writes).

Features#

Without any [feature.*] table everything goes into one hidden feature. As soon as one feature is declared, every file needs one: its own feature key, or the feature of its dir. A feature with a level above 1 is not installed by default. With ui = "features" the user picks features in a tree, and changes them later from Installed apps (Change on the maintenance page).

Conditions: when#

when takes a Windows Installer condition (VersionNT64, DESK = "1", NOT OLDVERSION) on a feature, a file or file group, a registry value (not one with a file: put it on the file), a shortcut, an environment variable or an INI value. What it guards is installed only when the condition holds; properties set in your own dialog pages, on the command line or by a search can be tested. The condition is evaluated when the item is first installed, a major upgrade included (a repair keeps what is there); an MSIX installs everything and refuses when.

[dialog-control.Desk]             # a check box in your own page (see below)
dialog = "Options"
type = "checkbox"
x = 20
y = 60
width = 300
height = 16
text = "Create a &desktop shortcut"
property = "DESK"

[shortcut.Desk]
dir = "Desktop"
name = "Example"
target = "file:App"
when = "DESK"

Variables#

$(NAME) is the one way to put a name into a string, and $$ is a literal $. Every name is replaced once; the result is not read again (a value containing $(X) stays literal). A name is

A build variable may not take a Windows name or a dir ID (RP1404).

Windows names#

The names of Windows folders and environment variables, written with Windows' spelling; letter case does not matter ($(appdata) is $(APPDATA)). In a value they become what Windows Installer fills in at install time, for the user who installs - or, in a type = "expand" registry value, the environment variable that the reading program expands at run time, for the user who runs it:

NamePath baseInstall time (64-bit / 32-bit package)Run time (type = "expand")
ProgramFilesyes[ProgramFiles64Folder] / [ProgramFilesFolder]%ProgramFiles%
ProgramFiles(x86)yes[ProgramFilesFolder]%ProgramFiles(x86)%
ProgramW6432-[ProgramFiles64Folder] / [%ProgramW6432]%ProgramW6432%
CommonProgramFilesyes[CommonFiles64Folder] / [CommonFilesFolder]%CommonProgramFiles%
CommonProgramFiles(x86)-[CommonFilesFolder]%CommonProgramFiles(x86)%
CommonProgramW6432-[CommonFiles64Folder] / [%CommonProgramW6432]%CommonProgramW6432%
ProgramData, ALLUSERSPROFILEyes[CommonAppDataFolder]%ProgramData% ...
APPDATAyes[AppDataFolder]%APPDATA%
LOCALAPPDATAyes[LocalAppDataFolder]%LOCALAPPDATA%
TEMP, TMPyes[TempFolder]%TEMP% ...
SystemRoot, windiryes[WindowsFolder]%SystemRoot% ...
Systemyes[System64Folder] / [SystemFolder]-
Fonts, Desktop, StartMenu, Programs, Startupyes[FontsFolder], [DesktopFolder], [StartMenuFolder], [ProgramMenuFolder], [StartupFolder]-
USERNAME-[LogonUser]%USERNAME%
COMPUTERNAME-[ComputerName]%COMPUTERNAME%
SystemDrive, USERPROFILE, PUBLIC, HOMEDRIVE, HOMEPATH, USERDOMAIN, LOGONSERVER, ComSpec, Path, PATHEXT, OS, PROCESSOR_ARCHITECTURE, NUMBER_OF_PROCESSORS-[%NAME]%NAME%

A dir ID in a value becomes [ID], its folder at install time. A folder's Windows Installer value ends with \, so a \ right after a folder name is dropped: '$(INSTALLDIR)\app.exe' becomes [INSTALLDIR]app.exe. A folder without an environment variable cannot stand in a type = "expand" value (RP1404). An MSIX has no install time: a value that needs one is refused there (RP1612, RP1613), while %NAME% in an expandable value works.

[NAME] and %NAME% written as they are pass through unchanged, for what has no $(...) name: [#FileID], [ProductVersion], [\[] for a literal [, %1 in file type arguments.

Program files#

Files that are Portable Executables (.exe, .dll, ...) are checked: their machine type must match arch (an x86 helper in an x64 package needs any-arch = true), and their version resource becomes the file's version in the package, which is how Windows Installer decides whether to replace an installed file.

Leaving a file at removal: keep#

keep = true on a file (or a file group) leaves it in place when the product is removed - for settings the user may have changed. A repair or a later version does not overwrite such a file once it has changed (Windows Installer's rule for files without a version). An MSIX removes all its files and refuses keep.

Guarding the install folder#

A program whose files are loaded by other programs - an input method, a shell extension, a service - must not be installed into a folder that someone else prepared: files already there stay, and a DLL planted beside the program would be loaded with its rights. guard = true on a dir makes a first installation stop before any file is placed when that folder

A folder that does not exist yet under a folder of a trusted owner - Program Files, say - passes (the installer creates it; [permission.*] can lock it). Repair, upgrade-in-place and removal are not checked; a major upgrade is a first installation of the new version and is checked like one (the folder the earlier version created passes). The installation ends with 1603 after the message DirGuardText ([1] is the folder; in the language chosen in the dialogs), also at /qn, and the log names the owner found. The check is done by rubrapack's helper DLL, which the package then carries (as for REG_QWORD values). What is checked is the owner: a folder owned by Administrators whose permissions let anyone write is not refused (lock it with [permission.*]).

[dir.INSTALLDIR]
path = "$(ProgramFiles)/Example"
guard = true

Paths#

A target path - [dir.*] and [search.*] path - starts with a folder: a dir ID or a Windows name that is a path base, as "$(ProgramFiles)/Example" or "$(INSTALLDIR)/docs", then / and the folders below. Build variables may follow ("$(INSTALLDIR)/v$(VERSION)").

Source paths are relative to the source file and use /. Absolute paths, \, symbolic links and missing files are errors. Target names may use any Unicode text except what Windows forbids (< > : " / \ | ? *, control characters, trailing dot or space, device names such as CON), and two names in one folder may not differ only by letter case. Windows Installer stores a name together with its 8.3 short name in 255 UTF-16 units, so a long name may use only what the short name leaves: 242 units for name.ext with a three-letter extension, 246 for a name without one (NTFS would accept 255). A longer name is refused with RP1514.

20.6 Command line#

rubrapack build <src.toml> -o <out.msi|out.msm|out.msix|out.msixbundle> [-D NAME=VALUE]...
                [--arch x64|arm64|x86 | --arch <list> (.msixbundle)]
                [--compress none|mszip|mszip:N|lzx|lzx:N] [--jobs N] [--nfc] [--reproducible]
                [<key> [--cert <chain.pem>]
                 [--timestamp <URL> [--tsa-trust <certificates>] [--tls-trust <certificates>] [--system-roots]
                  [--proxy <URL>]] [--allow-unsigned-cabs]]
                [--unsigned-test] [--msix-compress deflate|store]      (.msix, .msixbundle)
rubrapack inspect <file.msi> [table | --summary | --files | --streams]
rubrapack inspect <file.msi> [table | --summary] --json
rubrapack inspect <file.msix|file.msixbundle> [--files | --manifest]
rubrapack inspect <file.cab>
rubrapack new [msi] <name>
rubrapack new [<file>.toml] [-i]
rubrapack new <name> [--dist <folder>] [--name ...] [--ui ...] [--optional ...] ...
rubrapack edit <file>.toml [--set <table>.<key>=<value>]... [--unset <table>.<key>]... [--sync]
rubrapack guid [--from <text>]
rubrapack lint <src.toml> [-D NAME=VALUE]... [--arch x64|arm64|x86] [--target msi|msix] [--nfc] [--strict] [--json]
rubrapack lint <file.msi> [--previous <old.msi>] [--strict] [--json]
rubrapack explain [RPnnnn]
rubrapack schema
rubrapack lint <file.msix|file.msixbundle> [--strict]
rubrapack extract <file.msi|file.msix|file.msixbundle|file.cab> -d <new dir> [--limit-entries N] [--limit-bytes N]
rubrapack sign <file.exe|.dll|.msi|.msp|.msix|.msixbundle> <key> [--cert <chain.pem>]
               [--timestamp <URL> [--tsa-trust <certificates>] [--tls-trust <certificates>] [--system-roots]
                [--proxy <URL>]] [--allow-unsigned-cabs] [-o <out>]
rubrapack keys list [--pkcs11 <module> [--token-label <label>] [--pin-env VAR | --pin-file FILE]]
rubrapack verify <file.exe|.dll|.msi|.msp|.msix|.msixbundle> [--trust <certificates>]... [--system-roots] [--tsa-trust <certificates>]...
rubrapack version | help [command]

<key> is one of:
  --key <key.pfx|.pem> [--pass-env VAR | --pass-file FILE]                   a key file
  --pkcs11 <module> --key-label <label> [--token-label <label>]
           [--pin-env VAR | --pin-file FILE]                                  a key in a PKCS#11 token
  --key-store <SHA-1 thumbprint> [--machine-store]                            a key in the Windows store

20.7 Diagnostic codes#

Every problem rubrapack reports has a code, error[RPnnnn] or warning[RPnnnn], after the file, line and column it is about. The first two digits say what kind of problem it is:

CodesWhat went wrongWhere to look
RP00xxthe command line: an unknown command or option, a file that cannot be read or written, a difference a transform cannot carry (RP0013)rubrapack help <command>
RP10xxthe source file's encoding: not UTF-8 (or UTF-16 with a BOM), stray carriage returnssave the file as UTF-8
RP11xxTOML outside the subset rubrapack reads: multi-line strings, inline tables, a table defined twice; format missing or too new (RP1108)The TOML subset, Source format
RP12xxtables and keys: an unknown table or key (with a suggestion), a required key or table missing, something without a feature once features existTables
RP13xxvalues: IDs (unique across all tables, not reserved), GUIDs, versions, numbers out of range, references to things that do not existthe table's section
RP14xxvariables: $(NAME) without a value, $( not closed, a Windows name or dir ID where it cannot standVariables
RP15xxthe files to install: not found, a directory, a link, a glob without a match, a program for another architecture, names too long, a file that changed during the build, a package too largePaths, Program files
RP16xxMSIX: what an MSIX package needs, and what it cannot carryMSIX packages
RP19xxa feature this rubrapack does not provide-
RP20xxthe finished tables break a Windows Installer rule. A source that passes the RP1xxx checks should never meet these: please report it-
RP21xxlint of a package: dialogs, code page, text normalisationlint under Command line
RP22xxlint of an MSIX package or bundlelint under Command line
RP23xxlint --previous: this package would not upgrade the previous one cleanlyVersions and upgrades

The message says what to change; the codes above are the ones to search the manual for.