Upgrades & downgrades¶
When you ship a new exe package for an app that is already installed, Sigil
compares the version being installed against the version already recorded in
Add/Remove Programs and picks one of four paths automatically — the equivalent
of WiX's MajorUpgrade plus a downgrade block, or the Inno/NSIS
"detect the old version and uninstall it first" idiom. There is nothing to
configure: the version comes from app.version, and the app identity from
app.id.
The four paths¶
At startup the installer reads the scope-correct ARP entry for app.id
(DisplayVersion, InstallLocation, UninstallString) and classifies the run:
| Installed vs. this build | What happens |
|---|---|
| Nothing installed | Fresh install. |
| Same version | Repair / reinstall — the recorded install is replayed in reverse, then re-applied (idempotent: no duplicate PATH entries, shortcuts, or ARP rows). |
| Older installed | Upgrade. The previous version's uninstall.exe /S /Uninstall is run first (and must exit 0), then the new version installs into the previous install directory so user data outside the install journal is preserved. |
| Newer installed | Downgrade — blocked. The wizard shows a notice screen; a silent install exits with code 3. Pass /force-downgrade to override. |
An upgrade over a running app is the other common non-zero outcome: files held
open by a running process abort the run with exit 4 unless /closeapps was
passed. See the setup.exe reference.
The upgrade removes the old version by running its own uninstall.exe
(not the new build's uninstall logic), because that binary owns the previous
version's rollback journal and knows exactly how to reverse it.
Two things about that spawn are worth knowing before you debug one:
- The prior uninstaller is admitted through the single-instance lock explicitly.
It derives the same app+scope mutex name as the run that spawned it, so it would
otherwise refuse to start as "already running" and the whole upgrade would exit 5.
It is let through by a
SIGIL_SETUP_LOCK_HANDOFFenvironment token, validated against the real parent process id and that process's creation time — so only a genuine child of the running setup is admitted, not anything else that happens to know the token. - An unverifiable prior uninstaller is refused. When privilege is at stake — a
machine-scope install, or a process that is already elevated — the previous version's
uninstall.exeis only launched if it is Authenticode-valid or sits in a directory only administrators can write. Otherwise the run aborts before anything is installed withcannot upgrade: '…' is not verified. This is reachable in practice for an unsigned machine-scope install on a secondary volume such asD:\Apps\MyApp, whose default permissions grantUserswrite. See Uninstaller.
Scope¶
If the app was previously installed per-user, the upgrade stays per-user; if it
was per-machine, it stays per-machine. The existing install's scope wins over an
auto-resolved scope, so an upgrade always re-targets exactly what was installed.
An explicit /allusers / /currentuser, or a manifest that fixes scope:, is
still authoritative. If you force a different scope than the existing install
(e.g. /allusers over a per-user install), Sigil removes the old version in its
own scope and installs fresh into the new scope's default directory — the prior
directory is not carried across a scope change.
Install directory is preserved¶
An upgrade installs into the prior location even when the new build's default
destination differs (a changed installer.install_dir, a renamed app, a
different scope root). Precedence for the destination is:
- the wizard-collected path, then
/D=<path>, then- the prior install directory (upgrade), then
installer.install_dir, then- the default
<scope root>\<App.Name>.
/force-downgrade¶
Setup.exe /S /force-downgrade
Installs an older version over an installed newer one. Without it, a silent
downgrade refuses to run and returns exit code 3 (distinct from 1 step
failure, 2 cancelled, and 64 usage error) so automation can detect the
block. With it, the newer version is removed first (exactly like an upgrade)
and the older version is installed.
Version comparison & pre-release tags¶
Version ordering uses .NET's numeric dotted-version comparison — the same
semantics as the version_gte(a, b) expression function. Only numeric dotted
forms with at least two components parse (1.2, 1.2.3, 1.2.3.4).
SemVer pre-release / build tags are not understood. A value like
1.2.0-rc1 or 1.2.0+build.7 is not parsed as "before 1.2.0"; it falls back
to a plain lexicographic comparison. Sigil does not implement SemVer
pre-release precedence.
For the upgrade decision this is deliberately conservative: whenever a numeric
ordering can't be proven, the run is treated as an upgrade, never a block.
An installed version that does not parse as a numeric dotted version is treated
as older than the incoming build (an upgrade, with a warning); an incoming
app.version that is a SemVer tag (or absent) likewise never blocks. Keep
app.version a plain numeric dotted version to get precise upgrade detection.
Interaction with pre_install hooks¶
Lifecycle pre_install hooks run as part of the new install, which happens
after the previous version has already been removed. If you need logic that
observes the old files (data migration, config capture), it cannot run in
pre_install — by then the old version is gone. The upgrade path intentionally
performs no migration beyond removing the old version and preserving the install
directory; capture anything you need from the prior install before packaging the
upgrade, or read it at install time with the data-retrieval expression functions
(registry_read, file_version, …).
Not covered¶
- Side-by-side installs of multiple versions — Sigil keeps a single ARP row
per
app.id. - Delta / patch updates — upgrades install the full new package.