Files
YMhut-box-C-/installer/ymhut_box_winui.iss
T

1274 lines
42 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; 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;