; 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" #ifndef MyAppIdValue #define MyAppIdValue "cn.ymhut.box.winui" #endif #define MyAppId MyAppIdValue #ifndef QAIsolated #define QAIsolated 0 #endif #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 ; Preserve a high compression ratio without returning to the 1 GiB dictionary, ; which requires more than 11 GiB of working memory for this solid payload. CompressionThreads=1 LZMADictionarySize=262144 LZMANumFastBytes=273 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 #if Int(QAIsolated) == 1 CloseApplications=no #else CloseApplications=yes CloseApplicationsFilter={#MyAppExeName} #endif RestartApplications=no UninstallDisplayIcon={app}\{#MyAppExeName} UninstallFilesDir={app}\Runtime\uninstall-engine [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.UserWritableInstallRequired=Choose a writable current-user folder, or restart setup as administrator for a protected directory. chinesesimp.UserWritableInstallRequired=请选择当前用户可写目录;如需安装到受保护目录,请以管理员身份重新启动安装程序。 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 Name: "cleancache"; Description: "{cm:CleanCache}"; GroupDescription: "{cm:MaintenanceGroup}"; Flags: unchecked [Files] Source: "{#PayloadDir}\*"; DestDir: "{app}"; Excludes: "*.pri"; Flags: ignoreversion recursesubdirs createallsubdirs; BeforeInstall: LogCurrentExtractFile Source: "{#PayloadDir}\*.pri"; DestDir: "{app}"; Attribs: hidden; 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" [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" Type: dirifempty; Name: "{app}" [Code] const DirectoryAttribute = $10; InstallOutputFlushLines = 16; InstallOutputTrimCharacters = 65000; InstallOutputKeepCharacters = 48000; EM_SCROLLCARET = $00B7; WM_VSCROLL = $0115; SB_BOTTOM = 7; 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; LastExtractFileName: string; OldPayloadFiles: TStringList; NewPayloadFiles: TStringList; LegacyBackupDir: string; function SendMessage(hWnd: Longint; Msg: Longint; wParam: Longint; lParam: Longint): Longint; external 'SendMessageW@user32.dll stdcall'; 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 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 IsUserWritableInstallDir(const DirName: string): Boolean; begin Result := (DirName <> '') and (not IsProtectedInstallPath(DirName)) and IsWritableDirectory(DirName); end; function GetDefaultInstallDir(Param: string): string; begin if (ExistingInstallDir <> '') and ((not IsProtectedInstallPath(ExistingInstallDir)) or IsAdminInstallMode) then Result := ExistingInstallDir else Result := ExpandConstant('{localappdata}\Programs\YMhut Box'); end; function ShouldCleanCache(): Boolean; begin Result := WizardIsTaskSelected('cleancache'); 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 CaptureOldPayloadManifest(); forward; procedure FinalizeLegacyInstallLayout(); forward; procedure InitializeWizard; begin OldPayloadFiles := TStringList.Create; NewPayloadFiles := TStringList.Create; 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); InstallOutputMemo.SelLength := 0; SendMessage(InstallOutputMemo.Handle, EM_SCROLLCARET, 0, 0); SendMessage(InstallOutputMemo.Handle, WM_VSCROLL, SB_BOTTOM, 0); 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 LogBootstrapEvent(const EventName, Value: string); begin Log('YMHUT_EVENT:' + EventName + '|' + Value); end; procedure LogCurrentExtractFile(); var FileName: string; begin FileName := ExpandConstant(CurrentFileName); if FileName <> '' then begin LastExtractFileName := FileName; QueueInstallOutput(CustomMessage('InstallOutputFile') + ' ' + FileName, False, True); end; 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 IsUserWritableInstallDir(WizardDirValue) then begin MsgBox(CustomMessage('UserWritableInstallRequired'), 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 AcquireAndInstallPrerequisite( const Url, DownloadFileName, BundledFileName, Arguments, FriendlyName: string; const Bundled: Boolean; var NeedsRestart: Boolean): Boolean; var Attempt: Integer; Downloaded: Boolean; LastDownloadError: string; ResultCode: Integer; ExecutablePath: string; begin Result := False; if Bundled then begin LogBootstrapEvent('DEPENDENCY', 'bundled|' + FriendlyName); ExtractTemporaryFile(BundledFileName); ExecutablePath := ExpandConstant('{tmp}\' + BundledFileName); end else begin ExecutablePath := ExpandConstant('{tmp}\' + DownloadFileName); Downloaded := False; LastDownloadError := ''; for Attempt := 1 to 3 do begin LogBootstrapEvent('DEPENDENCY', 'download|' + FriendlyName); DownloadPage.Clear; DownloadPage.Add(Url, DownloadFileName, ''); if not WizardSilent then DownloadPage.Show; try try DownloadPage.Download; Downloaded := True; except if DownloadPage.AbortedByUser then RaiseException(FormatCustomMessage('DependencyDownloadCancelled', FriendlyName)) else LastDownloadError := GetExceptionMessage; end; finally if not WizardSilent then DownloadPage.Hide; end; if Downloaded then Break; if Attempt < 3 then begin LogBootstrapEvent('DEPENDENCY', 'retry|' + FriendlyName); Sleep(Attempt * 1500); end; end; if not Downloaded then RaiseException(FormatCustomMessage2('DependencyDownloadFailed', FriendlyName, LastDownloadError)); end; if not FileExists(ExecutablePath) then RaiseException(FormatCustomMessage2('DependencyDownloadFailed', FriendlyName, 'downloaded file is missing')); LogBootstrapEvent('DEPENDENCY', 'install|' + FriendlyName); LogBootstrapEvent('DEPENDENCY', 'waiting|' + FriendlyName); if not Exec(ExecutablePath, Arguments, '', SW_SHOW, 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; LogBootstrapEvent('DEPENDENCY', 'complete|' + FriendlyName); Result := True; end; procedure SetSetupFailure(var ResultText: string; const MessageText: string); begin ResultText := MessageText; LogBootstrapEvent('FAILURE', MessageText); end; procedure CopyIfExists(const Source, TargetDir: string); begin if FileExists(Source) then begin ForceDirectories(TargetDir); CopyFile(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 NormalizeManifestEntry(const Value: string): string; begin Result := Trim(Value); StringChangeEx(Result, '/', '\', True); while (Length(Result) > 0) and (Result[1] = '\') do Delete(Result, 1, 1); Result := Lowercase(Result); if (Result = '') or (Result = '..') or (Pos('..\', Result) > 0) or (Pos('\..', Result) > 0) or (Pos(':', Result) > 0) then Result := ''; end; procedure LoadInstallManifestFiles(const FileName: string; Files: TStringList); var Lines: TStringList; I: Integer; Entry: string; InFiles: Boolean; begin Files.Clear; if not FileExists(FileName) then Exit; Lines := TStringList.Create; try Lines.LoadFromFile(FileName); InFiles := False; for I := 0 to Lines.Count - 1 do begin Entry := Trim(Lines[I]); if (Length(Entry) > 1) and (Entry[1] = '[') then begin InFiles := CompareText(Entry, '[Files]') = 0; Continue; end; if InFiles then begin Entry := NormalizeManifestEntry(Entry); if (Entry <> '') and (Files.IndexOf(Entry) < 0) then Files.Add(Entry); end; end; finally Lines.Free; end; end; function ManifestContains(Files: TStringList; const RelativePath: string): Boolean; var Entry: string; begin Entry := NormalizeManifestEntry(RelativePath); Result := (Entry <> '') and (Files.IndexOf(Entry) >= 0); end; function IsPreservedInstallPath(const RelativePath: string): Boolean; var Entry: string; begin Entry := NormalizeManifestEntry(RelativePath); Result := (Entry = 'data') or (Pos('data\', Entry) = 1) or (Entry = 'feedbackpackages') or (Pos('feedbackpackages\', Entry) = 1) or (Entry = 'config\install-manifest.ini') or (Entry = 'unins000.exe') or (Entry = 'runtime\uninstall-engine') or (Pos('runtime\uninstall-engine\', Entry) = 1); end; function RelativeInstallPath(const AppDir, FullPath: string): string; begin Result := ''; if Pos(Lowercase(AddBackslash(AppDir)), Lowercase(FullPath)) <> 1 then Exit; Result := NormalizeManifestEntry(Copy(FullPath, Length(AddBackslash(AppDir)) + 1, Length(FullPath))); end; procedure CaptureOldPayloadManifest(); var ManifestPath: string; begin if OldPayloadFiles = nil then Exit; ManifestPath := AddBackslash(WizardDirValue) + 'config\install-manifest.ini'; LoadInstallManifestFiles(ManifestPath, OldPayloadFiles); Log(Format('Captured %d files from the previous install manifest.', [OldPayloadFiles.Count])); end; function HasToolUiPayload(const AppDir: string): Boolean; var FindRec: TFindRec; begin Result := False; if FindFirst(AddBackslash(AppDir) + 'Assets\data\tool-ui\*.dat', FindRec) then begin try repeat if (FindRec.Attributes and DirectoryAttribute) = 0 then begin Result := True; Exit; end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; procedure VerifyNewApplicationPayload(const AppDir: string); var ManifestPath: string; begin ManifestPath := AddBackslash(AppDir) + 'config\install-manifest.ini'; LoadInstallManifestFiles(ManifestPath, NewPayloadFiles); if NewPayloadFiles.Count = 0 then RaiseException('The new install manifest is missing or empty. Legacy files were not changed.'); if not FileExists(AddBackslash(AppDir) + '{#MyAppExeName}') or not FileExists(AddBackslash(AppDir) + 'YMhutBox.dll') or not FileExists(AddBackslash(AppDir) + 'Assets\runtime-assets.dat') or not FileExists(AddBackslash(AppDir) + 'Assets\data\ymhut-data.ybin') or not FileExists(AddBackslash(AppDir) + 'music-api\node.exe') or not FileExists(AddBackslash(AppDir) + 'music-api\api\ymhut-sidecar.js') or not FileExists(AddBackslash(AppDir) + 'music-api\api\pnpm-lock.yaml') or not FileExists(AddBackslash(AppDir) + 'music-api\api\LICENSE') or not FileExists(AddBackslash(AppDir) + 'unins000.exe') or not FileExists(AddBackslash(AppDir) + 'Runtime\uninstall-engine\unins000.exe') or not FileExists(AddBackslash(AppDir) + 'Runtime\uninstall-engine\unins000.dat') or not HasToolUiPayload(AppDir) then RaiseException('The new application payload is incomplete. Legacy files were not changed.'); end; procedure RegisterCustomUninstallEntry(const AppDir: string); var UninstallKey: string; UninstallCommand: string; QuietUninstallCommand: string; begin UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + '{#MyAppId}' + '_is1'; UninstallCommand := '"' + AddBackslash(AppDir) + 'unins000.exe" /UNINSTALL /INSTALLDIR="' + AppDir + '"'; QuietUninstallCommand := UninstallCommand + ' /VERYSILENT /NORESTART'; if IsAdminInstallMode then begin RegWriteStringValue(HKLM64, UninstallKey, 'UninstallString', UninstallCommand); RegWriteStringValue(HKLM64, UninstallKey, 'QuietUninstallString', QuietUninstallCommand); end else begin RegWriteStringValue(HKCU, UninstallKey, 'UninstallString', UninstallCommand); RegWriteStringValue(HKCU, UninstallKey, 'QuietUninstallString', QuietUninstallCommand); end; Log('Registered the YMhut Box uninstall bootstrap.'); end; procedure DeleteKnownObsoletePayload(const AppDir: string); var I: Integer; RelativePath: string; FullPath: string; begin for I := 0 to OldPayloadFiles.Count - 1 do begin RelativePath := OldPayloadFiles[I]; if (not ManifestContains(NewPayloadFiles, RelativePath)) and (not IsPreservedInstallPath(RelativePath)) then begin FullPath := AddBackslash(AppDir) + RelativePath; if FileExists(FullPath) then begin if DeleteFile(FullPath) then Log('Removed obsolete payload file: ' + RelativePath) else Log('Unable to remove obsolete payload file: ' + RelativePath); end; end; end; end; procedure QuarantineLegacyFile(const AppDir, FullPath, RelativePath: string); var TargetPath: string; begin if LegacyBackupDir = '' then begin LegacyBackupDir := AddBackslash(AppDir) + 'data\legacy-imports\program-backup-' + GetDateTimeString('yyyymmdd-hhnnss', '', ''); end; TargetPath := AddBackslash(LegacyBackupDir) + RelativePath; ForceDirectories(ExtractFileDir(TargetPath)); if FileExists(TargetPath) then TargetPath := TargetPath + '.old'; if RenameFile(FullPath, TargetPath) or (CopyFile(FullPath, TargetPath, False) and DeleteFile(FullPath)) then Log('Preserved unknown legacy file: ' + RelativePath) else Log('Unable to preserve unknown legacy file: ' + RelativePath); end; procedure QuarantineUnknownLegacyFiles(const AppDir, CurrentDir: string); var FindRec: TFindRec; FullPath: string; RelativePath: string; begin if not DirExists(CurrentDir) then Exit; if FindFirst(AddBackslash(CurrentDir) + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') and (FindRec.Name <> '..') then begin FullPath := AddBackslash(CurrentDir) + FindRec.Name; RelativePath := RelativeInstallPath(AppDir, FullPath); if (RelativePath <> '') and (not IsPreservedInstallPath(RelativePath)) then begin if (FindRec.Attributes and DirectoryAttribute) <> 0 then begin QuarantineUnknownLegacyFiles(AppDir, FullPath); RemoveDir(FullPath); end else if (not ManifestContains(NewPayloadFiles, RelativePath)) and (not ManifestContains(OldPayloadFiles, RelativePath)) then QuarantineLegacyFile(AppDir, FullPath, RelativePath); end; end; until not FindNext(FindRec); finally FindClose(FindRec); end; end; end; procedure CleanLegacyRuntimeAndCache(); begin #if Int(QAIsolated) == 0 DelTree(ExpandConstant('{localappdata}\YMhut Box\WinUI\Runtime'), True, True, True); DelTree(ExpandConstant('{localappdata}\YMhut Box\WinUI\Runtimes'), True, True, True); DelTree(ExpandConstant('{localappdata}\YMhut Box\WinUI\Tools'), True, True, True); DelTree(ExpandConstant('{localappdata}\YMhut Box\WinUI\Metadata'), True, True, True); DelTree(ExpandConstant('{localappdata}\YMhut Box\Runtime'), True, True, True); DelTree(ExpandConstant('{localappdata}\YMhut Box\Tools'), True, True, True); DelTree(ExpandConstant('{localappdata}\YMhut Box\Metadata'), True, True, True); DelTree(ExpandConstant('{localappdata}\ymhut_box\Runtime'), True, True, True); DelTree(ExpandConstant('{localappdata}\ymhut_box\Tools'), True, True, True); DelTree(ExpandConstant('{localappdata}\ymhut_box\Metadata'), True, True, True); if ShouldCleanCache() then begin DelTree(ExpandConstant('{localappdata}\YMhut Box\cache'), True, True, True); DelTree(ExpandConstant('{userappdata}\YMhut Box\cache'), True, True, True); DelTree(ExpandConstant('{localappdata}\ymhut_box\cache'), True, True, True); DelTree(ExpandConstant('{userappdata}\ymhut_box\cache'), True, True, True); end; #endif end; procedure FinalizeLegacyInstallLayout(); var AppDir: string; begin AppDir := ExpandConstant('{app}'); VerifyNewApplicationPayload(AppDir); if OldPayloadFiles.Count > 0 then DeleteKnownObsoletePayload(AppDir) else QuarantineUnknownLegacyFiles(AppDir, AppDir); CleanLegacyRuntimeAndCache(); end; function PrepareToInstall(var NeedsRestart: Boolean): string; begin AppendInstallOutput('Prepare system prerequisites.'); LogBootstrapEvent('STAGE', 'prerequisites'); Result := ''; #if Int(QAIsolated) == 1 Exit; #endif if (ExistingInstallDir <> '') and IsProtectedInstallPath(ExistingInstallDir) and (not IsAdminInstallMode) then begin SetSetupFailure(Result, CustomMessage('ProtectedInstallNeedsAdmin')); Exit; end; if IsProtectedInstallPath(WizardDirValue) and (not IsAdminInstallMode) then begin SetSetupFailure(Result, CustomMessage('ProtectedInstallNeedsAdmin')); Exit; end; CaptureOldPayloadManifest(); try if IsWebView2Installed() then LogBootstrapEvent('DEPENDENCY', 'ready|' + GetCustomMessageValue('WebView2RuntimeName')) else begin AcquireAndInstallPrerequisite( '{#WebView2BootstrapperUrl}', 'MicrosoftEdgeWebView2Setup.exe', 'MicrosoftEdgeWebView2RuntimeInstallerX64.exe', '/install', GetCustomMessageValue('WebView2RuntimeName'), BundledWebView2Included = 1, NeedsRestart); if not WaitForWebView2Installed(60) then begin SetSetupFailure(Result, GetCustomMessageValue('WebView2StillMissing')); Exit; end; end; if IsVCRedistInstalled() then LogBootstrapEvent('DEPENDENCY', 'ready|' + GetCustomMessageValue('VCRuntimeName')) else begin AcquireAndInstallPrerequisite( '{#VCRedistUrl}', 'vc_redist.x64.exe', 'vc_redist.x64.exe', '/install /norestart', GetCustomMessageValue('VCRuntimeName'), BundledVCRedistIncluded = 1, NeedsRestart); if not WaitForVCRedistInstalled(10) then begin SetSetupFailure(Result, GetCustomMessageValue('VCRuntimeStillMissing')); Exit; end; end; except SetSetupFailure(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; LastExtractFileName := ''; LogBootstrapEvent('STAGE', 'files'); AppendInstallOutput(CustomMessage('InstallOutputExtract')); end else if CurStep = ssPostInstall then begin FlushInstallOutput(); LogBootstrapEvent('STAGE', 'finalize'); AppendInstallOutput(CustomMessage('InstallOutputPostInstall')); FinalizeLegacyInstallLayout(); RegisterCustomUninstallEntry(ExpandConstant('{app}')); #if Int(QAIsolated) == 0 MigrateLegacyData(); #endif AppendInstallOutput(CustomMessage('InstallOutputDone')); end; end; procedure DeinitializeSetup(); begin if OldPayloadFiles <> nil then OldPayloadFiles.Free; if NewPayloadFiles <> nil then NewPayloadFiles.Free; 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; LogBootstrapEvent('PROGRESS', '100|' + LastExtractFileName); FlushInstallOutput(); end; Exit; end; if (CurProgress > 0) then begin { CurProgress can represent tens or hundreds of millions of bytes. Cast before multiplying so large payloads do not raise an integer-overflow runtime error from the progress callback. } ProgressPercent := (Int64(CurProgress) * 100) div MaxProgress; if ProgressPercent <> LastInstallProgressPercent then begin LastInstallProgressPercent := ProgressPercent; LogBootstrapEvent('PROGRESS', IntToStr(ProgressPercent) + '|' + LastExtractFileName); FlushInstallOutput(); end; end; end; end;