fix: modernize windows installer with Inno Setup (#867)
Build Snapshot Release / build (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
Test / test-nix (push) Has been cancelled
Test / test-win (push) Has been cancelled
Build Snapshot Release / Windows installers (push) Has been cancelled
Update Restic / update-restic-version (push) Has been cancelled

This commit is contained in:
Andrew H.
2025-08-21 01:11:52 -05:00
committed by GitHub
parent a967832731
commit dc481e55c8
3 changed files with 384 additions and 312 deletions
+32 -9
View File
@@ -55,13 +55,36 @@ jobs:
dist/*.tar.gz
dist/*.zip
- name: Generate Installers
run: |
mkdir -p dist-installers
./scripts/generate-installers.sh ./dist-installers
installer:
name: Windows installers
needs: [ build ]
runs-on: windows-2022
- name: Upload Installers
uses: actions/upload-artifact@v4
with:
name: backrest-snapshot-installers
path: dist-installers/*.exe
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: backrest-snapshot-builds
- name: Unzip artifacts and compile Inno Setup installers
shell: powershell
run: |
mkdir windows_installers
foreach ($arch in "x86_64", "arm64") {
$src = "backrest_Windows_$arch"
Expand-Archive ".\${src}.zip"
cp build\windows\* $src
& "c:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DArch=$arch ${src}\installer.iss
cp "$src\Output\*" windows_installers
}
- name: Upload installers
uses: actions/upload-artifact@v4
with:
name: backrest-snapshot-installers
path: windows_installers\*.exe
-303
View File
@@ -1,303 +0,0 @@
!define BUILD_DIR "."
!define OUT_DIR "."
!define APP_NAME "Backrest"
!define COMP_NAME "garethgeorge"
!define WEB_SITE "https://github.com/garethgeorge/backrest"
!define COPYRIGHT "garethgeorge 2024"
!define DESCRIPTION "${APP_NAME} installer"
!define LICENSE_TXT "${BUILD_DIR}\LICENSE"
!define MAIN_APP_EXE "backrest-windows-tray.exe"
!define INSTALL_TYPE "SetShellVarContext current"
!define REG_ROOT "HKCU"
!define REG_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
# Extract version from the changelog.
!searchparse /file "${BUILD_DIR}\CHANGELOG.md" `## [` VERSION `]`
# User variables.
Var UIPort
Var WelcomeTitle
Var WelcomeText
Var WelcomePortNote
Var OldVersion
Var Cmd
Var InstallMode
Var InstallModeLower
######################################################################
# Installer file properties
# NSIS requires X.X.X.X format in VIProductVersion.
VIProductVersion "${VERSION}.0"
VIAddVersionKey "ProductName" "${APP_NAME}"
VIAddVersionKey "CompanyName" "${COMP_NAME}"
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"
######################################################################
# Installer settings
Unicode True
RequestExecutionLevel user
SetCompressor LZMA
Name "${APP_NAME}"
Caption "$(^Name) ${VERSION} Setup"
!ifdef ARCH
OutFile "${OUT_DIR}\Backrest-${ARCH}-setup.exe"
!else
OutFile "${OUT_DIR}\Backrest-setup.exe"
!endif
XPStyle on
# Default installation directory.
InstallDir "$LOCALAPPDATA\Programs\Backrest"
# If existing installation is detected, use that directory instead.
InstallDirRegKey "${REG_ROOT}" "${REG_UNINSTALL_PATH}" "UninstallString"
ManifestDPIAware true
ShowInstDetails show
ShowUninstDetails show
# Include NSIS headers used by this script.
!include "MUI2.nsh"
!include "LogicLib.nsh"
!include "Memento.nsh"
!include "WordFunc.nsh"
# Defines for the Memento macro.
!define MEMENTO_REGISTRY_ROOT "${REG_ROOT}"
!define MEMENTO_REGISTRY_KEY "${REG_UNINSTALL_PATH}"
######################################################################
# GUI pages
# Prompt to confirm exiting the installer.
!define MUI_ABORTWARNING
!define MUI_UNABORTWARNING
!define MUI_WELCOMEPAGE_TITLE "$WelcomeTitle"
!define MUI_TEXT_WELCOME_INFO_TEXT "$WelcomeText"
!define MUI_PAGE_CUSTOMFUNCTION_PRE onPreWelcome
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE onLeaveWelcome
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}"
!define MUI_COMPONENTSPAGE_NODESC
!define MUI_COMPONENTSPAGE_TEXT_COMPLIST "Select components to install:$\r$\n$\r$\nSelections will be remembered for future upgrades"
!define MUI_PAGE_CUSTOMFUNCTION_PRE onPreComponents
!insertmacro MUI_PAGE_COMPONENTS
!define MUI_PAGE_CUSTOMFUNCTION_PRE onPreDirectory
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAIN_APP_EXE}"
!define MUI_FINISHPAGE_RUN_TEXT "&Start ${APP_NAME} (runs in the system tray)"
# Use the built-in readme option to open the app URL.
!define MUI_FINISHPAGE_SHOWREADME http://localhost:$UIPort/
!define MUI_FINISHPAGE_SHOWREADME_TEXT "&Open Backrest user interface"
!define MUI_PAGE_CUSTOMFUNCTION_SHOW onShowFinish
!insertmacro MUI_PAGE_FINISH
# Uninstall pages.
!define MUI_UNFINISHPAGE_NOAUTOCLOSE
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
!insertmacro MUI_LANGUAGE "English"
######################################################################
# Functions
# Have to define the function this way to allow re-using it in the uninstall section.
!macro KillProcess UN
Function ${UN}KillProcess
ReadEnvStr $Cmd COMSPEC
DetailPrint "Stopping Backrest if it is running..."
# Gracefully attempt to stop Backrest processes for the current user.
# Do it 5 times, then kill forcefully.
nsExec::ExecToLog '$Cmd /C echo off & (for /L %i in (1,1,5) do tasklist /FI "USERNAME eq %USERNAME%" | findstr /I /V "setup" | findstr "backrest" && taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" || exit) & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" /F & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" /F '
FunctionEnd
!macroend
!insertmacro KillProcess ""
!insertmacro KillProcess "un."
Function .onInit
# $R0, $R1 etc are registers; used here as local variables.
# Read some environment variables.
ReadEnvStr $Cmd COMSPEC
ReadEnvStr $R1 BACKREST_PORT
${If} "$R1" == ""
# Use the default port and welcome text if the var is empty.
StrCpy $UIPort "9898"
StrCpy $WelcomePortNote ""
${Else}
# Extract port number.
${WordFind} "$R1" ":" "+2" $UIPort
StrCpy $WelcomePortNote "$\r$\n$\r$\nNOTE: detected BACKREST_PORT environment variable. Will use port $UIPort for shortcuts."
${EndIf}
# Read the previous Backrest version, if any.
ReadRegStr $OldVersion ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayVersion"
${If} "$OldVersion" == "00.00.00.00"
# Old pre-1.6.2 installer installed into C:\Program Files; override the default path when upgrading.
StrCpy $INSTDIR "$LOCALAPPDATA\Programs\Backrest"
${EndIf}
${If} "$OldVersion" != ""
# Detected existing installation.
${MementoSectionRestore}
${VersionCompare} "$OldVersion" "${VERSION}" $R3
${Select} $R3
${Case} "0"
StrCpy $InstallMode "Reinstall"
${Case} "1"
StrCpy $InstallMode "Downgrade"
${CaseElse}
StrCpy $InstallMode "Upgrade"
${EndSelect}
StrCpy $WelcomeTitle "Welcome to ${APP_NAME} $InstallMode"
# Convert to lowercase for Welcome text.
${StrFilter} "$InstallMode" "-" "" "" $InstallModeLower
StrCpy $WelcomeText "Setup will guide you through the $InstallModeLower of ${APP_NAME} from version $OldVersion to ${VERSION}.$\r$\n$\r$\nInstallation directory is $INSTDIR $WelcomePortNote$\r$\n$\r$\nClick Next to continue."
${Else}
# New installation.
# Check if port is already in use and go into the abort mode.
nsExec::ExecToStack '$Cmd /C netstat.exe -na | findstr LISTENING | findstr ":$UIPort " '
Pop $R4
${If} "$R4" == "0"
StrCpy "$InstallMode" "Abort"
StrCpy $WelcomeTitle "Error"
StrCpy $WelcomeText "*** WARNING ***$\r$\nBackrest binds to port $UIPort for web UI. This port is currently in use by another Backrest instance or another application.$\r$\n$\r$\nPerform the following:$\r$\nClick Start - type $\"environment$\", Enter to open System Properties.$\r$\nClick Environment Variables. Click New in the top section. Enter BACKREST_PORT as the name and 127.0.0.1:port as the value, where $\"port$\" is a number between 1024 and 65535 (avoid known ports; try 9900), then OK 3 times.$\r$\nExit and re-run this installer to have it pick up the new value.$\r$\nSee installation documentation for more details.$\r$\n$\r$\nClick Exit to exit."
${Else}
StrCpy $WelcomeTitle "Welcome to ${APP_NAME} Setup"
StrCpy $WelcomeText "Setup will guide you through the installation of ${APP_NAME}.$WelcomePortNote$\r$\n$\r$\nClick Next to continue."
${EndIf}
${EndIf}
FunctionEnd
Function onPreWelcome
${If} "$InstallMode" == "Abort"
# Change text on the button.
GetDlgItem $R5 $HWNDPARENT 1
${NSD_SetText} $R5 "&Exit"
${EndIf}
FunctionEnd
Function onLeaveWelcome
${If} "$InstallMode" == "Abort"
Quit
${EndIf}
FunctionEnd
Function onPreComponents
${If} "$InstallMode" != ""
GetDlgItem $R6 $HWNDPARENT 1
${NSD_SetText} $R6 "$(^InstallBtn)"
${EndIf}
FunctionEnd
Function onPreDirectory
# Skip directory page.
${If} "$InstallMode" != ""
Abort
${EndIf}
FunctionEnd
Function onShowFinish
# Run custom functions when the checkboxes are clicked.
${NSD_OnClick} $mui.FinishPage.Run onChkRun
FunctionEnd
Function onChkRun
Pop $R7
${NSD_GetState} $mui.FinishPage.Run $7
${If} $7 == ${BST_UNCHECKED}
${NSD_Uncheck} $mui.FinishPage.ShowReadme
EnableWindow $mui.FinishPage.ShowReadme 0
${Else}
EnableWindow $mui.FinishPage.ShowReadme 1
${EndIf}
FunctionEnd
Function .onInstSuccess
${MementoSectionSave}
FunctionEnd
######################################################################
# Sections
Section "Application files"
SectionIn RO
${INSTALL_TYPE}
Call KillProcess
# Clean up remnants from the old installer (except for items in "Program Files" which would require elevation).
${If} "$OldVersion" == "00.00.00.00"
Delete "$DESKTOP\${APP_NAME} Console.lnk"
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk"
Delete "$SMPROGRAMS\${APP_NAME}\Uninstall ${APP_NAME}.lnk"
DeleteRegKey ${REG_ROOT} "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}"
${EndIf}
# Allow reinstall and downgrade by overwriting the files.
SetOverwrite on
SetOutPath "$INSTDIR"
File "${BUILD_DIR}\backrest.exe"
File "${BUILD_DIR}\backrest-windows-tray.exe"
File "${BUILD_DIR}\LICENSE"
File "${BUILD_DIR}\icon.ico"
WriteUninstaller "$INSTDIR\uninstall.exe"
# Start Menu shortcuts.
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\icon.ico" 0
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} UI.lnk" "http://localhost:$UIPort/" "" "$INSTDIR\icon.ico" 0
WriteIniStr "$SMPROGRAMS\${APP_NAME}\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}"
# Registry entries.
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayName" "${APP_NAME}"
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\icon.ico"
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "DisplayVersion" "${VERSION}"
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}"
WriteRegStr ${REG_ROOT} "${REG_UNINSTALL_PATH}" "InstallLocation" "$INSTDIR"
SectionEnd
${MementoSection} "Run application at startup (recommended)" sect_startup
CreateDirectory $SMSTARTUP
CreateShortcut "$SMSTARTUP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\icon.ico" 0
${MementoSectionEnd}
${MementoSection} "Desktop shortcut" sect_desktop
CreateShortCut "$DESKTOP\${APP_NAME} UI.lnk" "http://localhost:$UIPort/" "" "$INSTDIR\icon.ico" 0
${MementoSectionEnd}
${MementoSectionDone}
# If a previous installation created the shortcuts, remove them when user deselects
# upon upgrade/reinstall to honour the new choice.
Section "-Remove deselected shortcuts"
${IfNot} ${SectionIsSelected} ${sect_startup}
Delete "$SMSTARTUP\${APP_NAME}.lnk"
${EndIf}
${IfNot} ${SectionIsSelected} ${sect_desktop}
Delete "$DESKTOP\${APP_NAME} UI.lnk"
${EndIf}
SectionEnd
Section "Uninstall"
${INSTALL_TYPE}
Call un.KillProcess
Delete "$INSTDIR\LICENSE"
Delete "$INSTDIR\icon.ico"
Delete "$INSTDIR\install.lock"
Delete "$INSTDIR\restic*.exe"
Delete "$INSTDIR\backrest.exe"
Delete "$INSTDIR\backrest-windows-tray.exe"
Delete "$INSTDIR\uninstall.exe"
RmDir "$INSTDIR"
# Delete Start Menu shortcuts.
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} UI.lnk"
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} website.url"
RmDir "$SMPROGRAMS\${APP_NAME}"
# Startup and desktop shortcuts.
Delete "$SMSTARTUP\${APP_NAME}.lnk"
Delete "$DESKTOP\${APP_NAME} UI.lnk"
# Registry key.
DeleteRegKey ${REG_ROOT} "${REG_UNINSTALL_PATH}"
SectionEnd
+352
View File
@@ -0,0 +1,352 @@
#define B "Backrest"
#define TrayExe "backrest-windows-tray.exe"
#define Website "https://github.com/garethgeorge/backrest/"
; The following is needed to extract the version from the change log.
; If the application executable had the version info, then could use built-in GetVersion* functions.
#define fHandle FileOpen("CHANGELOG.md")
#expr FileRead(fHandle)
#expr FileRead(fHandle)
#define Line FileRead(fHandle)
#expr FileClose(fHandle)
#define VStart Pos("[", Line) + 1
#define VEnd Pos("]", Line)
#define VLen (VEnd - VStart)
#define BackrestVersion Copy(Line, VStart, VLen)
[Setup]
AppName={#B}
AppVersion={#BackrestVersion}
AppVerName={#B} {#BackrestVersion}
AppPublisher=garethgeorge
AppPublisherURL={#Website}
VersionInfoVersion={#BackrestVersion}
DefaultDirName={autopf}\{#B}
DefaultGroupName={#B}
DisableProgramGroupPage=yes
AlwaysShowDirOnReadyPage=yes
UninstallDisplayIcon={app}\icon.ico
#ifndef Arch
#define Arch "x86_64"
#endif
OutputBaseFilename={#B}Setup-{#Arch}
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
UsePreviousPrivileges=no
SetupMutex={#B}Setup
; Disable built-in RestartManager functionality, see comments under [Files].
CloseApplications=no
RestartApplications=no
#if SameText(Arch, "arm64")
#define ArchAllowed "arm64"
#else
#define ArchAllowed "x64os"
#endif
ArchitecturesAllowed={#ArchAllowed}
ArchitecturesInstallIn64BitMode={#ArchAllowed}
SetupLogging=yes
UninstallLogging=yes
[Tasks]
Name: "adminstartcurrent"; Description: "Run {#B} as the current user. Automatically start when the current user logs in. Inherits user access to network resources. Configuration is stored in the current user profile."; GroupDescription: "Execution context"; Check: IsAdminInstallMode; Flags: exclusive
Name: "adminstartsystem"; Description: "Run {#B} as the system user. Automatically start before any user logs in. Configuration is stored in the system user profile."; GroupDescription: "Execution context"; Check: IsAdminInstallMode; Flags: exclusive unchecked
Name: "autostart"; Description: "Automatically start {#B} at log on"; Check: IsUserInstallMode
Name: "desktopicon"; Description: "Create a desktop icon"; Flags: unchecked
Name: "addtopath"; Description: "Add {#B} directory to PATH ({code:GetEnvTarget})"; Flags: unchecked
#define PortDesc "Select a network port for the web interface"
Name: "port9898"; Description: "Default (9898)"; GroupDescription: "{#PortDesc}"; Flags: exclusive; Check: IsUserInstallMode
Name: "port9899"; Description: "9899"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
Name: "port9900"; Description: "9900"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
Name: "port9901"; Description: "9901"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
Name: "port9902"; Description: "9902"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
[Files]
; Need to stop Backrest not only when uninstalling but also before upgrades or reinstalls
; This is only an issue when Backrest runs as SYSTEM or non-current user. Inno can natively close applications in other cases,
; but doing it the same way in all cases for consistency.
Source: "LICENSE"; DestDir: "{app}"; Flags: ignoreversion; BeforeInstall: StopBackrest
Source: "icon.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#TrayExe}"; DestDir: "{app}"; Flags: ignoreversion
Source: "backrest.exe"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; For user install mode only.
Name: "{autostartup}\{#B} systray"; Filename: "{app}\{#TrayExe}"; Parameters: "{code:GetPortParam}"; IconFilename: "{app}\icon.ico"; Check: IsUserInstallMode
Name: "{group}\{#B} systray"; Filename: "{app}\{#TrayExe}"; Parameters: "{code:GetPortParam}"; IconFilename: "{app}\icon.ico"; Check: IsUserInstallMode
; For both modes.
Name: "{group}\{#B}{code:GetIconSuffix}"; Filename: "http://localhost:{code:GetPort}/"; IconFilename: "{app}\icon.ico"
Name: "{group}\{#B} website"; Filename: "{#Website}"
Name: "{autodesktop}\{#B}{code:GetIconSuffix}"; Filename: "http://localhost:{code:GetPort}/"; IconFilename: "{app}\icon.ico"; Tasks: desktopicon
[Messages]
PrivilegesRequiredOverrideText2=%1 can be installed to run with standard or administrative privileges.%n%nIf you need to use Windows VSS feature with "--use-fs-snapshot" restic option, select the administrative option.%nProtecting Backrest web UI with a password is highly recommended, especially with the administrative install.
PrivilegesRequiredOverrideAllUsers=Install &system-wide with administrative privileges
[Run]
; Use Task Scheduler to run Backrest elevated. The 30s delay is needed to avoid an issue with tray icon being broken.
; The double-quotes escape double-quotes inside the parameter. The backslash escapes double-quotes inside the -Command block.
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""$t = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME ; $t.Delay = 'PT30S'; Register-ScheduledTask -Force -TaskName '{#B}' -RunLevel Highest -Trigger $t -Action $(New-ScheduledTaskAction -Execute \""{app}\{#TrayExe}\"" -Argument '--bind-address 127.0.0.1:9897' -WorkingDirectory '{app}') -Settings $(New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -ExecutionTimeLimit 0); Start-ScheduledTask -TaskName '{#B}'"" "; Flags: runascurrentuser logoutput runhidden; Tasks: adminstartcurrent; Check: IsAdminInstallMode
; System user task. No need for systray here, and running it without returning control is the only way to stop it gracefully later.
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Register-ScheduledTask -Force -TaskName '{#B}' -RunLevel Highest -User System -Trigger $(New-ScheduledTaskTrigger -AtStartup) -Action $(New-ScheduledTaskAction -Execute \""{app}\backrest.exe\"" -Argument '--bind-address 127.0.0.1:9897' -WorkingDirectory '{app}') -Settings $(New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -ExecutionTimeLimit 0); Start-ScheduledTask -TaskName '{#B}'"" "; Flags: runascurrentuser logoutput runhidden; Tasks: adminstartsystem; Check: IsAdminInstallMode
; PATH
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""$newp = '{app}'; $a = [Environment]::GetEnvironmentVariable('PATH', '{code:GetEnvTarget}') -split ';' ; if ($a -notcontains $newp) {{ echo 'Adding to PATH'; $a += $newp; $path = $a -join ';' ; [Environment]::SetEnvironmentVariable('PATH', $path, '{code:GetEnvTarget}') }"" "; Flags: logoutput runhidden; Tasks: addtopath
; Remove from PATH for existing installation when unchecked.
; Reuse the same command in multiple places. Two quotes to escape in preprocessor, two more for the Parameters directive.
#define PathDelCmd "-ExecutionPolicy Bypass -Command """"$newp = '{app}'; $a = [Environment]::GetEnvironmentVariable('PATH', '{code:GetEnvTarget}') -split ';' ; if ($a -contains $newp) {{ echo 'Removing from PATH'; $path = ($a | Where-Object {{ $_ -ne $newp }) -join ';' ; [Environment]::SetEnvironmentVariable('PATH', $path, '{code:GetEnvTarget}') }"""" "
Filename: "powershell.exe"; Parameters: "{#PathDelCmd}"; Flags: logoutput runhidden; Tasks: not addtopath; Check: IsExistingInstallation
Filename: "{app}\{#TrayExe}"; Parameters: "{code:GetPortParam}"; Description: "Start {#B} (runs in the system tray)"; Flags: postinstall waituntilidle; Check: IsUserInstallMode
Filename: "http://localhost:{code:GetPort}/"; Description: "Open {#B} user interface"; Flags: postinstall shellexec
[UninstallRun]
Filename: "powershell.exe"; Parameters: "{#PathDelCmd}"; Flags: logoutput runhidden; RunOnceId: "RemoveFromPath"
[UninstallDelete]
Type: files; Name: "{app}\restic*.exe"
Type: files; Name: "{app}\install.lock"
; Built-in deletion runs before this section and fails to remove the directory due to the files above.
Type: dirifempty; Name: "{app}"
[Code]
var
UserInstallationExists, AdminInstallationExists: Boolean;
PreviousAdminUser, PreviousAdminTasks, PreviousVersionUser, PreviousVersionAdmin: String;
AppName, AppDirAdmin, AppDirUser, RegKey, Cmd: String;
procedure AssignGlobals();
begin
AppName := ExpandConstant('{#SetupSetting("AppName")}');
Cmd := ExpandConstant('{cmd}');
RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + AppName + '_is1';
if RegQueryStringValue(HKEY_CURRENT_USER, RegKey, 'Inno Setup: App Path', AppDirUser)
then UserInstallationExists := True else UserInstallationExists := False;
if RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'Inno Setup: App Path', AppDirAdmin)
then AdminInstallationExists := True else AdminInstallationExists := False;
RegQueryStringValue(HKEY_CURRENT_USER, RegKey, 'DisplayVersion', PreviousVersionUser)
RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'DisplayVersion', PreviousVersionAdmin)
RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'Inno Setup: Selected Tasks', PreviousAdminTasks);
RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'Inno Setup: User', PreviousAdminUser);
end;
function IsUserInstallMode(): Boolean;
begin
if IsAdminInstallMode then Result := False else Result := True;
end;
function GetPort(Param: String): String;
var
S: String;
A: array of String;
i: Integer;
begin
if IsAdminInstallMode then Result := '9897'
else
begin
A := StringSplit(WizardSelectedTasks(False), [','], stAll);
for i := 0 to GetArrayLength(A) - 1 do
begin
S := A[i];
if Pos('port', S) > 0 then StringChangeEx(S, 'port', '', True);
end;
Result := S;
end;
end;
function GetPortParam(Param: String): String;
var
S: String;
begin
S := GetPort('');
// Don't add any shortcut parameters for default port selection.
if S = '9898' then Result := '' else Result := '--bind-address 127.0.0.1:' + S;
end;
function GetIconSuffix(Param: String): String;
begin
if IsAdminInstallMode and UserInstallationExists then Result := ' (system-wide)'
else if IsUserInstallMode and AdminInstallationExists then Result := ' (current user)'
else Result := '';
end;
function GetEnvTarget(Param: String): String;
begin
if IsAdminInstallMode then Result := 'Machine' else Result := 'User';
end;
function IsExistingInstallation(): Boolean;
begin
if UserInstallationExists or AdminInstallationExists then Result := True else Result := False;
end;
procedure StopBackrest();
var
ResultCode: Integer;
begin
// Attempt to terminate Backrest gracefully for the current user, wait for a second (since taskkill returns immediately),
// then check and kill forcefully if it's still running.
if IsUserInstallMode then
ExecAndLogOutput(Cmd, '/C "taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" & ping -n 2 127.0.0.1 >nul & tasklist /FI "USERNAME eq %USERNAME%" | findstr /I /V "setup" | findstr "backrest" && (echo Forcing & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" /F & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" /F)" ',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode, nil)
// For admin installs stop through the task scheduler. For the SYSTEM or non-current user this is the only way to stop gracefully.
// Ending the scheduled task makes a gracefull attempt, then force kills.
else if IsAdminInstallMode then
begin
ExecAndLogOutput(Cmd, '/C schtasks /End /TN ' + AppName + ' || (taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest-windows-tray.exe" /F & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" /F)',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode, nil);
// Remove the task when uninstalling.
if IsUninstaller then
ExecAndLogOutput(Cmd, '/C schtasks /Delete /TN ' + AppName + ' /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, nil);
end;
end;
procedure ShowUpgradeMsg(AppDir: String; InstalledVersion: String);
var
AppVersion, Msg: String;
InstalledVersionFull, NewVersionFull: Int64;
CompResult: Integer;
begin
AppVersion := ExpandConstant('{#SetupSetting("AppVersion")}');
// Convert both old and new versions to the correct type. It also automatically adds '.0' if '0.0.0' format is used.
if StrToVersion(InstalledVersion, InstalledVersionFull) and StrToVersion(AppVersion, NewVersionFull) then
begin
CompResult := ComparePackedVersion(InstalledVersionFull, NewVersionFull);
if CompResult < 0 then Msg := 'upgrade'
else if CompResult = 0 then Msg := 'reinstall'
else if CompResult > 0 then Msg := 'downgrade'
else Msg := 'upgrade/reinstall/downgrade';
end;
MsgBox('Detected existing installation of Backrest ' + InstalledVersion +
' in ' + Chr(13) + Chr(10) + AppDir + Chr(13) + Chr(10) + Chr(13) + Chr(10) +
'Setup will ' + Msg + ' to version ' + AppVersion + '.', mbInformation, MB_OK);
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
ResultPortCheck: Integer;
begin
Result := True
if CurPageID = wpSelectTasks then
begin
// Prevent installing an admin instance when a user instance exists under the same user.
// It wouldn't work anyway due to the same configuration path.
if IsAdminInstallMode and WizardIsTaskSelected('adminstartcurrent') and UserInstallationExists then
begin
MsgBox('Detected an existing non-administrative installation under the same user ' + GetUserNameString +
'. Cannot proceed with this selection.', mbError, MB_OK)
Result := False;
end
else if IsUserInstallMode and not UserInstallationExists then
begin
if ExecAndLogOutput(Cmd, '/C netstat -na | findstr LISTENING | findstr /C:":' + GetPort('') + ' "', '', SW_HIDE,
ewWaitUntilTerminated, ResultPortCheck, nil) and (ResultPortCheck = 0) then
begin
MsgBox('Selected port is in use, choose another one and try again.', mbError, MB_OK)
Result := False;
end;
end;
end;
end;
procedure RunListClickCheck(Sender: TObject);
begin
if not WizardForm.RunList.Checked[0] then
begin
WizardForm.RunList.Checked[1] := False;
WizardForm.RunList.ItemEnabled[1] := False;
end
else WizardForm.RunList.ItemEnabled[1] := True;
end;
procedure CurPageChanged(CurPageID: Integer);
var
i: Integer;
begin
if CurPageID = wpSelectTasks then
// Prevent the user from switching installation type back and forth in admin mode.
// It would cause issues with terminating processes and also with Backrest data being in different user profiles.
if IsAdminInstallMode and AdminInstallationExists then
for i := 1 to 2 do WizardForm.TasksList.ItemEnabled[i] := False
end;
procedure InitializeWizard();
begin
WizardForm.ReadyMemo.ScrollBars := ssVertical;
WizardForm.ReadyMemo.WordWrap := True;
// Uncheck and disable the "open" checkbox when the "start" is unchecked on the Finish page.
if IsUserInstallMode then WizardForm.RunList.OnClickCheck := @RunListClickCheck;
end;
function InitializeSetup(): Boolean;
var
OldUninstaller: String;
I: Integer;
begin
Result := True;
AssignGlobals;
// Check for presence of the old Nullsoft installation.
if RegQueryStringValue(HKEY_CURRENT_USER, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\Backrest',
'UninstallString', OldUninstaller) then
begin
MsgBox('Detected an existing installation done by the old installer that needs to be uninstalled before proceeding. Your configuration will not be impacted.' + Chr(13) + Chr(10) + Chr(13) + Chr(10) +
'Re-run this setup after uninstallation is complete.', mbInformation, MB_OK);
ExecAsOriginalUser(OldUninstaller, '', '', SW_SHOWNORMAL, ewNoWait, I);
Abort;
end;
// Upgrade/reinstall scenarios.
if IsUserInstallMode and UserInstallationExists then ShowUpgradeMsg(AppDirUser, PreviousVersionUser)
else if IsUserInstallMode and AdminInstallationExists then
begin
if GetUserNameString = PreviousAdminUser then
begin
// Prevent installing a user instance when an admin instance already exists under the same user.
MsgBox('Detected an existing administrative installation under the same user ' + PreviousAdminUser +
'. Cannot proceed. Uninstall it and try again. Setup will exit now.', mbError, MB_OK);
Result := False;
end
else begin
// But allow and notify about an existing admin instance under a different user, if present.
MsgBox('Detected an existing administrative installation of Backrest ' + PreviousVersionAdmin + ' under user ' +
PreviousAdminUser + '.' + Chr(13) + Chr(10) + Chr(13) + Chr(10) +
'Setup will install a non-administrative instance for the current user.', mbInformation, MB_OK);
end;
end
// Notify the user about the existing installation of the same type, if present.
else if IsAdminInstallMode and AdminInstallationExists then
begin
ShowUpgradeMsg(AppDirAdmin, PreviousVersionAdmin);
// Warn if attempting to upgrade/reinstall under a different user.
if (Pos('adminstartcurrent', PreviousAdminTasks) > 0) and (GetUserNameString <> PreviousAdminUser) then
MsgBox('Warning! Previous installation of this type was done by user ' + PreviousAdminUser +
'. If you proceed, Backrest will be reinstalled to run under the current user. Configuration will be lost.', mbError, MB_OK);
end;
end;
function InitializeUninstall(): Boolean;
begin
Result := True;
AssignGlobals;
if AdminInstallationExists and UserInstallationExists then
MsgBox('Detected two existing installations of Backrest. Ensure you are running the correct uninstaller. Windows Settings - Apps panel shows only one entry at a time.' + Chr(13) + Chr(10) +
'Use Control Panel - Programs and Features instead to identify the correct uninstaller. Or run unins000.exe directly from the appropriate Backrest directory.', mbInformation, MB_OK);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
BackrestConfig: String;
begin
// Under the system user installation a special profile is used.
if IsAdminInstallMode and AdminInstallationExists and (Pos('adminstartsystem', PreviousAdminTasks) > 0) then
BackrestConfig := 'C:\Windows\system32\config\systemprofile\AppData\Roaming\backrest'
else
BackrestConfig := GetEnv('APPDATA') + '\backrest';
case CurUninstallStep of
usUninstall:
StopBackrest;
usDone:
begin
if MsgBox('Do you want to delete Backrest configuration in this location?' + Chr(13) + Chr(10) + BackrestConfig,
mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
if DelTree(BackrestConfig, True, True, True) then MsgBox('Done!', mbInformation, MB_OK)
else MsgBox('Failed to remove the path', mbInformation, MB_OK);
end;
end;
end;