diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee517634..5dfd8287 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/build/windows/install.nsi b/build/windows/install.nsi deleted file mode 100644 index 9bea917a..00000000 --- a/build/windows/install.nsi +++ /dev/null @@ -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 diff --git a/build/windows/installer.iss b/build/windows/installer.iss new file mode 100644 index 00000000..bf11718d --- /dev/null +++ b/build/windows/installer.iss @@ -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;