c92f358774
完成天气胶囊在高 DPI 和窄窗口下的逐级降级布局,减少窗口拖动期间的主题与材质重建。新增三个停用的 manifest v3 插件示例、Tauri 伴随程序和构建哈希校验;旧版插件自动回收到可恢复目录。安装器升级成功后递归清理旧静态资源空目录,并补充相关文档和测试。
1351 lines
45 KiB
Plaintext
1351 lines
45 KiB
Plaintext
; 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;
|
||
|
||
function IsPreservedDirectoryTree(const RelativePath: string): Boolean;
|
||
var
|
||
Normalized: string;
|
||
begin
|
||
Normalized := LowerCase(Trim(RelativePath));
|
||
Result :=
|
||
(Normalized = 'data') or (Pos('data\', Normalized) = 1) or
|
||
(Normalized = 'feedbackpackages') or (Pos('feedbackpackages\', Normalized) = 1) or
|
||
(Normalized = 'config') or (Pos('config\', Normalized) = 1) or
|
||
(Normalized = 'runtime\uninstall-engine') or
|
||
(Pos('runtime\uninstall-engine\', Normalized) = 1);
|
||
end;
|
||
|
||
function ManifestRequiresDirectory(const RelativePath: string): Boolean;
|
||
var
|
||
I: Integer;
|
||
DirectoryPrefix: string;
|
||
Item: string;
|
||
begin
|
||
Result := False;
|
||
DirectoryPrefix := LowerCase(RemoveBackslashUnlessRoot(RelativePath)) + '\';
|
||
for I := 0 to NewPayloadFiles.Count - 1 do
|
||
begin
|
||
Item := LowerCase(NewPayloadFiles[I]);
|
||
if Pos(DirectoryPrefix, Item) = 1 then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function IsInstallDirectoryPath(const AppDir, Candidate: string): Boolean;
|
||
var
|
||
RootPath: string;
|
||
CandidatePath: string;
|
||
begin
|
||
RootPath := LowerCase(AddBackslash(RemoveBackslashUnlessRoot(AppDir)));
|
||
CandidatePath := LowerCase(AddBackslash(RemoveBackslashUnlessRoot(Candidate)));
|
||
Result := Pos(RootPath, CandidatePath) = 1;
|
||
end;
|
||
|
||
procedure RemoveEmptyInstallDirectories(const AppDir, CurrentDir: string);
|
||
var
|
||
FindRec: TFindRec;
|
||
FullPath: string;
|
||
RelativePath: string;
|
||
begin
|
||
if (not DirExists(CurrentDir)) or (not IsInstallDirectoryPath(AppDir, CurrentDir)) then
|
||
Exit;
|
||
|
||
if FindFirst(AddBackslash(CurrentDir) + '*', FindRec) then
|
||
begin
|
||
try
|
||
repeat
|
||
if (FindRec.Name <> '.') and (FindRec.Name <> '..') and
|
||
((FindRec.Attributes and DirectoryAttribute) <> 0) then
|
||
begin
|
||
FullPath := AddBackslash(CurrentDir) + FindRec.Name;
|
||
RelativePath := RelativeInstallPath(AppDir, FullPath);
|
||
if (RelativePath <> '') and
|
||
((FindRec.Attributes and $400) = 0) and
|
||
(not IsPreservedDirectoryTree(RelativePath)) then
|
||
begin
|
||
RemoveEmptyInstallDirectories(AppDir, FullPath);
|
||
if (not ManifestRequiresDirectory(RelativePath)) and RemoveDir(FullPath) then
|
||
Log('Removed empty obsolete install directory: ' + RelativePath);
|
||
end;
|
||
end;
|
||
until not FindNext(FindRec);
|
||
finally
|
||
FindClose(FindRec);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure FinalizeLegacyInstallLayout();
|
||
var
|
||
AppDir: string;
|
||
begin
|
||
AppDir := ExpandConstant('{app}');
|
||
VerifyNewApplicationPayload(AppDir);
|
||
if OldPayloadFiles.Count > 0 then
|
||
DeleteKnownObsoletePayload(AppDir)
|
||
else
|
||
QuarantineUnknownLegacyFiles(AppDir, AppDir);
|
||
RemoveEmptyInstallDirectories(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;
|