; YMhut Box - WinUI 3 Windows installer (Stage B packaging) ; This installer ships the self-contained WinUI 3 publish output. ; Build the payload with: ; dotnet publish src\box-winUI\YMhut.Box.WinUI.csproj -c Release -r win-x64 --self-contained -o publish\winui ; then run ISCC against this .iss. #define MyAppName "YMhut Box" #define MyAppPublisher "YMhut" #define MyAppURL "https://ymhut.cn" #define MyAppExeName "YMhutBox.exe" #define MyAppId "cn.ymhut.box.winui" #ifndef MyAppVersion #define MyAppVersion "2.0.2" #endif #ifndef MyAppBuild #define MyAppBuild "1" #endif #ifndef MyAppChannel #define MyAppChannel "stable" #endif #ifndef PayloadDir #define PayloadDir "..\latest" #endif #ifndef BundleVCRedist #define BundleVCRedist 0 #endif #ifndef BundleWebView2 #define BundleWebView2 0 #endif #ifndef ChineseMessagesFile #define ChineseMessagesFile "compiler:Default.isl" #endif #ifndef WebView2BootstrapperUrl #define WebView2BootstrapperUrl "https://go.microsoft.com/fwlink/?LinkId=2124703" #endif #ifndef VCRedistUrl #define VCRedistUrl "https://aka.ms/vs/17/release/vc_redist.x64.exe" #endif [Setup] AppId={#MyAppId} AppName={#MyAppName} AppVersion={#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} DefaultDirName={code:GetDefaultInstallDir} DefaultGroupName={#MyAppName} OutputDir=..\installer_output OutputBaseFilename=YMhut_Box_WinUI_Setup_{#MyAppVersion} Compression=lzma2 CompressionThreads=auto LZMADictionarySize=1048576 LZMAUseSeparateProcess=yes SolidCompression=yes WizardStyle=modern ShowLanguageDialog=yes LanguageDetectionMethod=uilanguage PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=commandline UsePreviousPrivileges=no ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible UsePreviousAppDir=no AllowNoIcons=yes SetupLogging=yes CloseApplications=yes CloseApplicationsFilter={#MyAppExeName} RestartApplications=no UninstallDisplayIcon={app}\{#MyAppExeName} [Languages] Name: "chinesesimp"; MessagesFile: "{#ChineseMessagesFile}" Name: "english"; MessagesFile: "compiler:Default.isl" [CustomMessages] english.DesktopIcon=Create a desktop shortcut english.StartMenuIcon=Create a Start menu shortcut english.ShortcutGroup=Shortcuts english.AutoStart=Start automatically after sign-in english.LaunchApp=Launch {#MyAppName} after setup finishes english.StartupGroup=Startup english.MaintenanceGroup=Maintenance english.InstallSummaryTitle=Install output summary english.InstallSummaryDescription=Review the install directories and selected output before copying files. english.InstallSummaryVersion=Version english.InstallSummaryInstallDir=Install directory english.InstallSummaryPayload=Payload source english.InstallSummaryUserData=User data english.InstallSummaryLogs=Logs english.InstallSummaryTasks=Selected tasks english.InstallOutputInitial=Ready to extract files. english.InstallOutputClean=Cleaning old application files. english.InstallOutputExtract=Extracting new application payload. english.InstallOutputFile=Extracting: english.InstallOutputPostInstall=Finalizing installation. english.InstallOutputDone=Installation files are ready. english.ProtectedInstallNeedsAdmin=The existing installation is in a protected system directory. Restart setup as administrator or choose a user-writable folder. english.NonSystemInstallRequired=Please choose a writable folder on a non-system drive. YMhut Box no longer installs to the Windows system drive by default. chinesesimp.NonSystemInstallRequired=请选择一个非系统盘上的可写目录。YMhut Box 不再默认安装到 Windows 系统盘。 english.RepairRuntimes=Check and install system prerequisites (VC++ / WebView2) english.MigrateLegacyData=Import legacy data english.CleanCache=Clean legacy cache folders english.WebView2RuntimeName=WebView2 Runtime english.VCRuntimeName=VC++ Runtime english.DependencyDownloadCancelled=%1 download was canceled. english.DependencyDownloadFailed=%1 download failed: %2 english.DependencyStartFailed=%1 installer could not be started. english.DependencyInstallFailed=%1 installation failed with exit code %2. english.WebView2StillMissing=WebView2 Runtime is still not detected after installation. Check the network connection or system policy, then retry. english.VCRuntimeStillMissing=VC++ Runtime is still not detected after installation. Check the network connection or system policy, then retry. chinesesimp.DesktopIcon=创建桌面快捷方式 chinesesimp.StartMenuIcon=创建开始菜单快捷方式 chinesesimp.ShortcutGroup=快捷方式 chinesesimp.AutoStart=开机后自动启动 chinesesimp.LaunchApp=安装完成后立即启动 {#MyAppName} chinesesimp.StartupGroup=启动 chinesesimp.MaintenanceGroup=维护 chinesesimp.InstallSummaryTitle=安装输出摘要 chinesesimp.InstallSummaryDescription=复制文件前确认安装目录、输出位置和已选任务。 chinesesimp.InstallSummaryVersion=版本 chinesesimp.InstallSummaryInstallDir=安装目录 chinesesimp.InstallSummaryPayload=载荷来源 chinesesimp.InstallSummaryUserData=用户数据 chinesesimp.InstallSummaryLogs=日志 chinesesimp.InstallSummaryTasks=已选任务 chinesesimp.InstallOutputInitial=准备提取文件。 chinesesimp.InstallOutputClean=正在清理旧版本程序文件。 chinesesimp.InstallOutputExtract=正在提取新版本程序文件。 chinesesimp.InstallOutputFile=正在提取: chinesesimp.InstallOutputPostInstall=正在完成安装收尾。 chinesesimp.InstallOutputDone=安装文件已准备完成。 chinesesimp.ProtectedInstallNeedsAdmin=检测到旧版本安装在受保护的系统目录。请以管理员身份重新运行安装器修复,或选择当前用户可写的安装目录。 chinesesimp.RepairRuntimes=检查并安装系统依赖(VC++ / WebView2) chinesesimp.MigrateLegacyData=导入旧版数据 chinesesimp.CleanCache=清理旧版缓存目录 chinesesimp.WebView2RuntimeName=WebView2 Runtime chinesesimp.VCRuntimeName=VC++ Runtime chinesesimp.DependencyDownloadCancelled=%1 下载已取消。 chinesesimp.DependencyDownloadFailed=%1 下载失败:%2 chinesesimp.DependencyStartFailed=%1 安装器启动失败。 chinesesimp.DependencyInstallFailed=%1 安装失败,退出码:%2。 chinesesimp.WebView2StillMissing=WebView2 Runtime 安装后仍未检测到,请检查网络连接或系统策略后重试。 chinesesimp.VCRuntimeStillMissing=VC++ Runtime 安装后仍未检测到,请检查网络连接或系统策略后重试。 [Tasks] Name: "desktopicon"; Description: "{cm:DesktopIcon}"; GroupDescription: "{cm:ShortcutGroup}"; Flags: checkedonce Name: "startmenuicon"; Description: "{cm:StartMenuIcon}"; GroupDescription: "{cm:ShortcutGroup}"; Flags: unchecked Name: "autostart"; Description: "{cm:AutoStart}"; GroupDescription: "{cm:StartupGroup}"; Flags: unchecked Name: "launchapp"; Description: "{cm:LaunchApp}"; GroupDescription: "{cm:StartupGroup}"; Flags: checkedonce [Files] Source: "{#PayloadDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; BeforeInstall: LogCurrentExtractFile #if Int(BundleVCRedist) == 1 Source: "{#PayloadDir}\prereqs\vc_redist.x64.exe"; Flags: dontcopy #endif #if Int(BundleWebView2) == 1 Source: "{#PayloadDir}\prereqs\MicrosoftEdgeWebView2RuntimeInstallerX64.exe"; Flags: dontcopy #endif [Dirs] Name: "{app}\data" Name: "{app}\FeedbackPackages"; Permissions: users-modify Name: "{app}\data\legacy-imports" [InstallDelete] Type: filesandordirs; Name: "{app}\resources\lang" Type: filesandordirs; Name: "{app}\lang" Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Runtime" Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\runtime" Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Runtimes" Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\runtimes" Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Tools" Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Metadata" Type: filesandordirs; Name: "{localappdata}\YMhut Box\Runtime" Type: filesandordirs; Name: "{localappdata}\YMhut Box\runtime" Type: filesandordirs; Name: "{localappdata}\YMhut Box\Tools" Type: filesandordirs; Name: "{localappdata}\YMhut Box\Metadata" Type: filesandordirs; Name: "{localappdata}\ymhut_box\Runtime" Type: filesandordirs; Name: "{localappdata}\ymhut_box\runtime" Type: filesandordirs; Name: "{localappdata}\ymhut_box\Tools" Type: filesandordirs; Name: "{localappdata}\ymhut_box\Metadata" Type: filesandordirs; Name: "{localappdata}\YMhut Box\cache"; Check: ShouldCleanCache Type: filesandordirs; Name: "{userappdata}\YMhut Box\cache"; Check: ShouldCleanCache Type: filesandordirs; Name: "{localappdata}\ymhut_box\cache"; Check: ShouldCleanCache Type: filesandordirs; Name: "{userappdata}\ymhut_box\cache"; Check: ShouldCleanCache Type: files; Name: "{app}\*.winmd" Type: files; Name: "{app}\*.pdb" Type: files; Name: "{app}\workloads*.json" Type: files; Name: "{app}\installer-layout.txt" Type: files; Name: "{app}\config\installer-layout.dat" Type: files; Name: "{app}\config\installer-required-files.dat" Type: files; Name: "{app}\Assets\data\reference-data.dat" Type: files; Name: "{app}\YMhut.Box.Worker.exe" Type: files; Name: "{app}\YMhut.Box.Worker.dll" Type: files; Name: "{app}\YMhut.Box.Worker.deps.json" Type: files; Name: "{app}\YMhut.Box.Worker.runtimeconfig.json" Type: files; Name: "{app}\YMhut.Box.PluginHost.exe" Type: files; Name: "{app}\YMhut.Box.PluginHost.dll" Type: files; Name: "{app}\YMhut.Box.PluginHost.deps.json" Type: files; Name: "{app}\YMhut.Box.PluginHost.runtimeconfig.json" Type: filesandordirs; Name: "{app}\updater" Type: files; Name: "{app}\YMhut.Box.Updater.exe" Type: files; Name: "{app}\YMhut.Box.Updater.dll" Type: files; Name: "{app}\YMhut.Box.Updater.deps.json" Type: files; Name: "{app}\YMhut.Box.Updater.runtimeconfig.json" [Icons] Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startmenuicon Name: "{userdesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Registry] Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchApp}"; Flags: nowait postinstall skipifsilent; Tasks: launchapp [UninstallDelete] Type: files; Name: "{userdesktop}\{#MyAppName}.lnk" Type: files; Name: "{userprograms}\{#MyAppName}.lnk" [Code] const DirectoryAttribute = $10; InstallOutputFlushLines = 16; InstallOutputTrimCharacters = 65000; InstallOutputKeepCharacters = 48000; BundledVCRedistIncluded = {#BundleVCRedist}; BundledWebView2Included = {#BundleWebView2}; WebView2ClientKey = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'; WebView2ClientKeyWow64 = 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'; WebView2ClientStateKey = 'SOFTWARE\Microsoft\EdgeUpdate\ClientState\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'; WebView2ClientStateKeyWow64 = 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\ClientState\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'; VCRedistKey = 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64'; var DownloadPage: TDownloadWizardPage; SummaryPage: TWizardPage; SummaryMemo: TNewMemo; InstallOutputMemo: TNewMemo; ExistingInstallDir: string; LastInstallProgressPercent: Integer; InstallOutputLineCount: Integer; InstallOutputPendingLineCount: Integer; InstallOutputBuffer: string; function NormalizePathPrefix(const Value: string): string; begin Result := Lowercase(AddBackslash(ExpandConstant(Value))); end; function PathStartsWith(const Path, Prefix: string): Boolean; begin Result := Pos(Lowercase(Prefix), Lowercase(AddBackslash(Path))) = 1; end; function IsProtectedInstallPath(const DirName: string): Boolean; begin Result := PathStartsWith(DirName, NormalizePathPrefix('{autopf}')) or PathStartsWith(DirName, NormalizePathPrefix('{pf}')) or PathStartsWith(DirName, NormalizePathPrefix('{pf32}')) or PathStartsWith(DirName, NormalizePathPrefix('{commonpf}')) or PathStartsWith(DirName, NormalizePathPrefix('{commonpf32}')); end; function GetSystemDrivePrefix(): string; begin Result := Uppercase(Copy(ExpandConstant('{win}'), 1, 2)); end; function IsSystemDrivePath(const DirName: string): Boolean; begin Result := Uppercase(Copy(ExpandConstant(DirName), 1, 2)) = GetSystemDrivePrefix(); end; function IsWritableDirectory(const DirName: string): Boolean; var ProbePath: string; begin Result := False; try if not ForceDirectories(DirName) then Exit; ProbePath := AddBackslash(DirName) + '.ymhut-write-test.tmp'; if SaveStringToFile(ProbePath, 'ok', False) then begin DeleteFile(ProbePath); Result := True; end; except Result := False; end; end; function IsNonSystemWritableInstallDir(const DirName: string): Boolean; begin Result := (DirName <> '') and (not IsSystemDrivePath(DirName)) and (not IsProtectedInstallPath(DirName)) and IsWritableDirectory(DirName); end; function FindDefaultNonSystemInstallDir(): string; var Index: Integer; DriveLetter: string; Candidate: string; begin Result := ''; for Index := 1 to Length('DEFGHIJKLMNOPQRSTUVWXYZ') do begin DriveLetter := Copy('DEFGHIJKLMNOPQRSTUVWXYZ', Index, 1); if DriveLetter + ':' = GetSystemDrivePrefix() then Continue; Candidate := DriveLetter + ':\Programs\YMhut Box'; if DirExists(DriveLetter + ':\') and IsWritableDirectory(Candidate) then begin Result := Candidate; Exit; end; end; end; function GetDefaultInstallDir(Param: string): string; var DefaultDir: string; begin if (ExistingInstallDir <> '') and ((not IsProtectedInstallPath(ExistingInstallDir)) or IsAdminInstallMode) then Result := ExistingInstallDir else begin DefaultDir := FindDefaultNonSystemInstallDir(); if DefaultDir <> '' then Result := DefaultDir else Result := 'D:\Programs\YMhut Box'; end; end; function ShouldCleanCache(): Boolean; begin Result := True; end; function DetectExistingInstallDir(): string; var UninstallKey: string; InstallLocation: string; begin Result := ''; UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + '{#MyAppId}' + '_is1'; if RegQueryStringValue(HKLM64, UninstallKey, 'InstallLocation', InstallLocation) and FileExists(AddBackslash(InstallLocation) + '{#MyAppExeName}') then begin Result := InstallLocation; Exit; end; if RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', InstallLocation) and FileExists(AddBackslash(InstallLocation) + '{#MyAppExeName}') then begin Result := InstallLocation; Exit; end; if RegQueryStringValue(HKCU, UninstallKey, 'InstallLocation', InstallLocation) and FileExists(AddBackslash(InstallLocation) + '{#MyAppExeName}') then begin Result := InstallLocation; Exit; end; end; function InitializeSetup(): Boolean; begin ExistingInstallDir := DetectExistingInstallDir(); if (ExistingInstallDir <> '') and IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode) then begin MsgBox(CustomMessage('ProtectedInstallNeedsAdmin'), mbError, MB_OK); Result := False; Exit; end; Result := True; end; procedure LayoutInstallOutputMemo(); forward; procedure ScrollInstallOutputToBottom(); forward; procedure InitializeWizard; begin DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); DownloadPage.ShowBaseNameInsteadOfUrl := True; ExistingInstallDir := DetectExistingInstallDir(); SummaryPage := CreateCustomPage(wpSelectTasks, CustomMessage('InstallSummaryTitle'), CustomMessage('InstallSummaryDescription')); SummaryMemo := TNewMemo.Create(SummaryPage); SummaryMemo.Parent := SummaryPage.Surface; SummaryMemo.Left := 0; SummaryMemo.Top := 0; SummaryMemo.Width := SummaryPage.SurfaceWidth; SummaryMemo.Height := SummaryPage.SurfaceHeight; SummaryMemo.ReadOnly := True; SummaryMemo.WordWrap := False; SummaryMemo.ScrollBars := ssVertical; InstallOutputMemo := TNewMemo.Create(WizardForm.InstallingPage); InstallOutputMemo.Parent := WizardForm.InstallingPage; InstallOutputMemo.ReadOnly := True; InstallOutputMemo.WordWrap := False; InstallOutputMemo.ScrollBars := ssVertical; InstallOutputMemo.Text := CustomMessage('InstallOutputInitial') + #13#10; InstallOutputLineCount := 1; InstallOutputPendingLineCount := 0; InstallOutputBuffer := ''; LayoutInstallOutputMemo(); ScrollInstallOutputToBottom(); end; procedure LayoutInstallOutputMemo(); var AvailableWidth: Integer; AvailableHeight: Integer; begin if InstallOutputMemo = nil then Exit; InstallOutputMemo.Left := WizardForm.ProgressGauge.Left; InstallOutputMemo.Top := WizardForm.ProgressGauge.Top + WizardForm.ProgressGauge.Height + ScaleY(10); AvailableWidth := WizardForm.InstallingPage.Width - (InstallOutputMemo.Left * 2); if AvailableWidth < WizardForm.ProgressGauge.Width then AvailableWidth := WizardForm.ProgressGauge.Width; InstallOutputMemo.Width := AvailableWidth; AvailableHeight := WizardForm.InstallingPage.Height - InstallOutputMemo.Top - ScaleY(6); if AvailableHeight < ScaleY(96) then AvailableHeight := ScaleY(96); InstallOutputMemo.Height := AvailableHeight; end; procedure ScrollInstallOutputToBottom(); begin if InstallOutputMemo = nil then Exit; InstallOutputMemo.SelStart := Length(InstallOutputMemo.Text); end; procedure FlushInstallOutput(); var TextValue: string; begin if InstallOutputMemo = nil then begin InstallOutputBuffer := ''; InstallOutputPendingLineCount := 0; Exit; end; if InstallOutputBuffer = '' then begin ScrollInstallOutputToBottom(); Exit; end; TextValue := InstallOutputMemo.Text + InstallOutputBuffer; InstallOutputLineCount := InstallOutputLineCount + InstallOutputPendingLineCount; if Length(TextValue) > InstallOutputTrimCharacters then begin TextValue := Copy(TextValue, Length(TextValue) - InstallOutputKeepCharacters + 1, InstallOutputKeepCharacters); InstallOutputLineCount := 500; end; InstallOutputMemo.Text := TextValue; InstallOutputBuffer := ''; InstallOutputPendingLineCount := 0; ScrollInstallOutputToBottom(); end; procedure QueueInstallOutput(const Text: string; const ForceFlush, WriteSetupLog: Boolean); begin if WriteSetupLog then Log(Text); if InstallOutputMemo = nil then Exit; InstallOutputBuffer := InstallOutputBuffer + Text + #13#10; InstallOutputPendingLineCount := InstallOutputPendingLineCount + 1; if ForceFlush or (InstallOutputPendingLineCount >= InstallOutputFlushLines) then FlushInstallOutput(); end; procedure AppendInstallOutput(const Text: string); begin QueueInstallOutput(Text, True, True); end; procedure LogCurrentExtractFile(); var FileName: string; begin FileName := ExpandConstant(CurrentFileName); if FileName <> '' then QueueInstallOutput(CustomMessage('InstallOutputFile') + ' ' + FileName, False, False); end; function SelectedTaskText(const TaskName, LabelText: string): string; begin if WizardIsTaskSelected(TaskName) then Result := ' - ' + LabelText + #13#10 else Result := ''; end; function BuildInstallSummary(): string; var Tasks: string; begin Tasks := SelectedTaskText('desktopicon', CustomMessage('DesktopIcon')) + SelectedTaskText('startmenuicon', CustomMessage('StartMenuIcon')) + SelectedTaskText('autostart', CustomMessage('AutoStart')) + SelectedTaskText('launchapp', CustomMessage('LaunchApp')); if Tasks = '' then Tasks := ' - (none)' + #13#10; Result := CustomMessage('InstallSummaryVersion') + ': {#MyAppVersion}' + #13#10 + CustomMessage('InstallSummaryInstallDir') + ': ' + WizardDirValue + #13#10 + CustomMessage('InstallSummaryPayload') + ': {#PayloadDir}' + #13#10 + '' + #13#10 + CustomMessage('InstallSummaryTasks') + ':' + #13#10 + Tasks; end; procedure UpdateInstallSummary(); begin if SummaryMemo <> nil then SummaryMemo.Text := BuildInstallSummary(); end; procedure CurPageChanged(CurPageID: Integer); begin if (SummaryPage <> nil) and (CurPageID = SummaryPage.ID) then UpdateInstallSummary(); if CurPageID = wpInstalling then begin LayoutInstallOutputMemo(); FlushInstallOutput(); end; end; function NextButtonClick(CurPageID: Integer): Boolean; begin Result := True; if (CurPageID = wpSelectDir) and ((ExistingInstallDir = '') or (IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode))) then begin if not IsNonSystemWritableInstallDir(WizardDirValue) then begin MsgBox(CustomMessage('NonSystemInstallRequired'), mbError, MB_OK); Result := False; end; end; end; function DirectoryHasEntries(const DirName: string): Boolean; var FindRec: TFindRec; begin Result := False; if not DirExists(DirName) then Exit; if FindFirst(AddBackslash(DirName) + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') and (FindRec.Name <> '..') then begin Result := True; Exit; end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; function PathLooksLikeUpgradeInstall(const DirName: string): Boolean; begin Result := FileExists(AddBackslash(DirName) + '{#MyAppExeName}') or FileExists(AddBackslash(DirName) + 'YMhutBoxDesktop.exe') or FileExists(AddBackslash(DirName) + 'installer-layout.txt') or FileExists(AddBackslash(DirName) + 'config\installer-layout.dat') or FileExists(AddBackslash(DirName) + 'config\installer-required-files.dat') or FileExists(AddBackslash(DirName) + 'config\install-manifest.ini'); end; function IsVCRedistInstalled(): Boolean; var Version: string; Installed: Cardinal; begin Result := (RegQueryDWordValue(HKLM64, VCRedistKey, 'Installed', Installed) and (Installed = 1) and RegQueryStringValue(HKLM64, VCRedistKey, 'Version', Version) and (Version <> '') and (Version <> '0.0.0.0')) or (RegQueryDWordValue(HKLM, VCRedistKey, 'Installed', Installed) and (Installed = 1) and RegQueryStringValue(HKLM, VCRedistKey, 'Version', Version) and (Version <> '') and (Version <> '0.0.0.0')) or (RegQueryDWordValue(HKLM32, VCRedistKey, 'Installed', Installed) and (Installed = 1) and RegQueryStringValue(HKLM32, VCRedistKey, 'Version', Version) and (Version <> '') and (Version <> '0.0.0.0')); end; function HasWebView2RuntimeExecutable(const Root: string): Boolean; var FindRec: TFindRec; begin Result := False; if not DirExists(Root) then Exit; if FileExists(AddBackslash(Root) + 'msedgewebview2.exe') then begin Result := True; Exit; end; if FindFirst(AddBackslash(Root) + '*', FindRec) then begin try repeat if ((FindRec.Attributes and DirectoryAttribute) <> 0) and FileExists(AddBackslash(Root) + FindRec.Name + '\msedgewebview2.exe') then begin Result := True; Exit; end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; function IsWebView2RuntimeVersionInstalled(const RootKey: Integer; const SubKey: string): Boolean; var Version: string; begin Result := (RegQueryStringValue(RootKey, SubKey, 'pv', Version) or RegQueryStringValue(RootKey, SubKey, 'Version', Version)) and (Version <> '') and (Version <> '0.0.0.0'); end; function IsWebView2Installed(): Boolean; begin Result := IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientKey) or IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientKey) or IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientKey) or IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientKey) or IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientKeyWow64) or IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientKeyWow64) or IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientKeyWow64) or IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientKeyWow64) or IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientStateKey) or IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientStateKey) or IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientStateKey) or IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientStateKey) or IsWebView2RuntimeVersionInstalled(HKLM64, WebView2ClientStateKeyWow64) or IsWebView2RuntimeVersionInstalled(HKLM, WebView2ClientStateKeyWow64) or IsWebView2RuntimeVersionInstalled(HKLM32, WebView2ClientStateKeyWow64) or IsWebView2RuntimeVersionInstalled(HKCU, WebView2ClientStateKeyWow64) or HasWebView2RuntimeExecutable(ExpandConstant('{localappdata}\Microsoft\EdgeWebView\Application')) or HasWebView2RuntimeExecutable(ExpandConstant('{pf}\Microsoft\EdgeWebView\Application')) or HasWebView2RuntimeExecutable(ExpandConstant('{pf32}\Microsoft\EdgeWebView\Application')) or HasWebView2RuntimeExecutable(ExpandConstant('{localappdata}\Microsoft\EdgeCore')) or HasWebView2RuntimeExecutable(ExpandConstant('{pf}\Microsoft\EdgeCore')) or HasWebView2RuntimeExecutable(ExpandConstant('{pf32}\Microsoft\EdgeCore')) or HasWebView2RuntimeExecutable(ExpandConstant('{localappdata}\Microsoft\Edge\Application')) or HasWebView2RuntimeExecutable(ExpandConstant('{pf}\Microsoft\Edge\Application')) or HasWebView2RuntimeExecutable(ExpandConstant('{pf32}\Microsoft\Edge\Application')); end; function WaitForWebView2Installed(const Seconds: Integer): Boolean; var Index: Integer; begin for Index := 1 to Seconds do begin if IsWebView2Installed() then begin Result := True; Exit; end; Sleep(1000); end; Result := IsWebView2Installed(); end; function WaitForVCRedistInstalled(const Seconds: Integer): Boolean; var Index: Integer; begin for Index := 1 to Seconds do begin if IsVCRedistInstalled() then begin Result := True; Exit; end; Sleep(1000); end; Result := IsVCRedistInstalled(); end; function GetCustomMessageValue(const Key: string): string; begin Result := CustomMessage(Key); end; function FormatCustomMessage(const Key, Value: string): string; begin Result := FmtMessage(CustomMessage(Key), [Value]); end; function FormatCustomMessage2(const Key, Value1, Value2: string): string; begin Result := FmtMessage(CustomMessage(Key), [Value1, Value2]); end; function DownloadAndInstallPrerequisite(const Url, FileName, Arguments, FriendlyName: string; var NeedsRestart: Boolean): Boolean; var ResultCode: Integer; ExecutablePath: string; begin Result := False; ExecutablePath := ExpandConstant('{tmp}\' + FileName); DownloadPage.Clear; DownloadPage.Add(Url, FileName, ''); if not WizardSilent then DownloadPage.Show; try DownloadPage.Download; except if DownloadPage.AbortedByUser then RaiseException(FormatCustomMessage('DependencyDownloadCancelled', FriendlyName)) else RaiseException(FormatCustomMessage2('DependencyDownloadFailed', FriendlyName, GetExceptionMessage)); end; if not WizardSilent then DownloadPage.Hide; if not Exec(ExecutablePath, Arguments, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then RaiseException(FormatCustomMessage('DependencyStartFailed', FriendlyName)); if (ResultCode <> 0) and (ResultCode <> 1638) and (ResultCode <> 3010) and (ResultCode <> 1641) then RaiseException(FormatCustomMessage2('DependencyInstallFailed', FriendlyName, IntToStr(ResultCode))); if (ResultCode = 3010) or (ResultCode = 1641) then NeedsRestart := True; Result := True; end; procedure CopyIfExists(const Source, TargetDir: string); begin if FileExists(Source) then begin ForceDirectories(TargetDir); FileCopy(Source, AddBackslash(TargetDir) + ExtractFileName(Source), False); end; end; procedure MigrateLegacyData(); var TargetDir: string; begin TargetDir := ExpandConstant('{app}\data\legacy-imports'); CopyIfExists(ExpandConstant('{localappdata}\YMhut Box\settings.json'), TargetDir); CopyIfExists(ExpandConstant('{localappdata}\YMhut Box\shared_preferences.json'), TargetDir); CopyIfExists(ExpandConstant('{localappdata}\YMhut Box\app_state.db'), TargetDir); CopyIfExists(ExpandConstant('{userappdata}\ymhut_box\settings.json'), TargetDir); CopyIfExists(ExpandConstant('{userappdata}\ymhut_box\shared_preferences.json'), TargetDir); CopyIfExists(ExpandConstant('{userappdata}\ymhut_box\app_state.db'), TargetDir); end; procedure DeleteFilesByPattern(const DirName, Pattern: string; const Recursive: Boolean); var FindRec: TFindRec; Child: string; begin if not DirExists(DirName) then Exit; if FindFirst(AddBackslash(DirName) + Pattern, FindRec) then begin try repeat if ((FindRec.Attributes and DirectoryAttribute) = 0) then DeleteFile(AddBackslash(DirName) + FindRec.Name); until not FindNext(FindRec); finally FindClose(FindRec); end; end; if not Recursive then Exit; if FindFirst(AddBackslash(DirName) + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') and (FindRec.Name <> '..') and ((FindRec.Attributes and DirectoryAttribute) <> 0) then begin Child := AddBackslash(DirName) + FindRec.Name; DeleteFilesByPattern(Child, Pattern, True); end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; procedure CleanCultureFolders(const DirName: string); var FindRec: TFindRec; CultureDir: string; begin if not DirExists(DirName) then Exit; if FindFirst(AddBackslash(DirName) + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') and (FindRec.Name <> '..') and ((FindRec.Attributes and DirectoryAttribute) <> 0) then begin CultureDir := AddBackslash(DirName) + FindRec.Name; if FileExists(AddBackslash(CultureDir) + 'Microsoft.ui.xaml.dll.mui') or FileExists(AddBackslash(CultureDir) + 'Microsoft.UI.Xaml.Phone.dll.mui') then DelTree(CultureDir, True, True, True); end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; function ShouldPreserveAppEntry(const Name: string): Boolean; var LowerName: string; begin LowerName := Lowercase(Name); Result := (LowerName = 'data') or (LowerName = 'feedbackpackages') or (Pos('unins', LowerName) = 1); end; procedure CleanOldApplicationPayload(const DirName: string); var FindRec: TFindRec; EntryPath: string; begin if not DirExists(DirName) then Exit; if FindFirst(AddBackslash(DirName) + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') and (FindRec.Name <> '..') and (not ShouldPreserveAppEntry(FindRec.Name)) then begin EntryPath := AddBackslash(DirName) + FindRec.Name; if ((FindRec.Attributes and DirectoryAttribute) <> 0) then DelTree(EntryPath, True, True, True) else DeleteFile(EntryPath); end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; procedure CleanLegacyInstallLayout(); var AppDir: string; begin AppDir := ExpandConstant('{app}'); CleanOldApplicationPayload(AppDir); DeleteFilesByPattern(AppDir, '*.winmd', True); DeleteFilesByPattern(AppDir, '*.pdb', True); DeleteFilesByPattern(AppDir, 'workloads*.json', True); DeleteFilesByPattern(AddBackslash(AppDir) + 'Assets\data', '*.json', True); DeleteFile(AddBackslash(AppDir) + 'installer-layout.txt'); DeleteFile(AddBackslash(AppDir) + 'config\installer-layout.dat'); DeleteFile(AddBackslash(AppDir) + 'config\installer-required-files.dat'); DeleteFile(AddBackslash(AppDir) + 'Assets\data\reference-data.dat'); CleanCultureFolders(AppDir); end; function PrepareToInstall(var NeedsRestart: Boolean): string; var ResultCode: Integer; begin AppendInstallOutput('Prepare system prerequisites.'); Result := ''; if (ExistingInstallDir <> '') and IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode) then begin Result := CustomMessage('ProtectedInstallNeedsAdmin'); Exit; end; if IsProtectedInstallPath(WizardDirValue) and (not IsAdminInstallMode) then begin Result := CustomMessage('ProtectedInstallNeedsAdmin'); Exit; end; try if not IsWebView2Installed() then begin DownloadAndInstallPrerequisite('{#WebView2BootstrapperUrl}', 'MicrosoftEdgeWebView2Setup.exe', '/silent /install', GetCustomMessageValue('WebView2RuntimeName'), NeedsRestart); if not WaitForWebView2Installed(60) then begin Result := GetCustomMessageValue('WebView2StillMissing'); Exit; end; end; if (not IsVCRedistInstalled()) then begin DownloadAndInstallPrerequisite('{#VCRedistUrl}', 'vc_redist.x64.exe', '/install /quiet /norestart', GetCustomMessageValue('VCRuntimeName'), NeedsRestart); if not WaitForVCRedistInstalled(10) then begin Result := GetCustomMessageValue('VCRuntimeStillMissing'); Exit; end; end; except Result := GetExceptionMessage; end; end; function ShouldSkipPage(PageID: Integer): Boolean; begin Result := (PageID = wpSelectDir) and (ExistingInstallDir <> '') and ((not IsProtectedInstallPath(ExistingInstallDir)) or IsAdminInstallMode); end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssInstall then begin LastInstallProgressPercent := -1; AppendInstallOutput(CustomMessage('InstallOutputClean')); CleanLegacyInstallLayout(); AppendInstallOutput(CustomMessage('InstallOutputExtract')); end else if CurStep = ssPostInstall then begin FlushInstallOutput(); AppendInstallOutput(CustomMessage('InstallOutputPostInstall')); MigrateLegacyData(); AppendInstallOutput(CustomMessage('InstallOutputDone')); end; end; procedure CurInstallProgressChanged(CurProgress, MaxProgress: Integer); var ProgressPercent: Integer; begin if (MaxProgress > 0) then begin if (CurProgress >= MaxProgress) then begin if LastInstallProgressPercent <> 100 then begin LastInstallProgressPercent := 100; FlushInstallOutput(); end; Exit; end; if (CurProgress > 0) then begin ProgressPercent := CurProgress * 100 div MaxProgress; if ProgressPercent <> LastInstallProgressPercent then begin LastInstallProgressPercent := ProgressPercent; FlushInstallOutput(); end; end; end; end;