完善在线工具、插件运行时与安装卸载流程

This commit is contained in:
2026-08-19 16:15:52 +08:00
parent 80093f285e
commit 7a2af2646c
366 changed files with 6683 additions and 490 deletions
+273 -78
View File
@@ -82,6 +82,7 @@ CloseApplicationsFilter={#MyAppExeName}
#endif
RestartApplications=no
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallFilesDir={app}\Runtime\uninstall-engine
[Languages]
Name: "chinesesimp"; MessagesFile: "{#ChineseMessagesFile}"
@@ -162,6 +163,7 @@ Name: "desktopicon"; Description: "{cm:DesktopIcon}"; GroupDescription: "{cm:Sho
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
@@ -178,50 +180,6 @@ Name: "{app}\data"
Name: "{app}\FeedbackPackages"; Permissions: users-modify
Name: "{app}\data\legacy-imports"
[InstallDelete]
Type: filesandordirs; Name: "{app}\resources\lang"
Type: filesandordirs; Name: "{app}\lang"
#if Int(QAIsolated) == 0
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Runtimes"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\runtimes"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Tools"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\WinUI\Metadata"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\Runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\runtime"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\Tools"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\Metadata"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\Runtime"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\runtime"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\Tools"
Type: filesandordirs; Name: "{localappdata}\ymhut_box\Metadata"
Type: filesandordirs; Name: "{localappdata}\YMhut Box\cache"; Check: ShouldCleanCache
Type: filesandordirs; Name: "{userappdata}\YMhut Box\cache"; Check: ShouldCleanCache
Type: filesandordirs; Name: "{localappdata}\ymhut_box\cache"; Check: ShouldCleanCache
Type: filesandordirs; Name: "{userappdata}\ymhut_box\cache"; Check: ShouldCleanCache
#endif
Type: files; Name: "{app}\*.winmd"
Type: files; Name: "{app}\*.pdb"
Type: files; Name: "{app}\workloads*.json"
Type: files; Name: "{app}\installer-layout.txt"
Type: files; Name: "{app}\config\installer-layout.dat"
Type: files; Name: "{app}\config\installer-required-files.dat"
Type: files; Name: "{app}\Assets\data\reference-data.dat"
Type: files; Name: "{app}\YMhut.Box.Worker.exe"
Type: files; Name: "{app}\YMhut.Box.Worker.dll"
Type: files; Name: "{app}\YMhut.Box.Worker.deps.json"
Type: files; Name: "{app}\YMhut.Box.Worker.runtimeconfig.json"
Type: files; Name: "{app}\YMhut.Box.PluginHost.exe"
Type: files; Name: "{app}\YMhut.Box.PluginHost.dll"
Type: files; Name: "{app}\YMhut.Box.PluginHost.deps.json"
Type: files; Name: "{app}\YMhut.Box.PluginHost.runtimeconfig.json"
Type: filesandordirs; Name: "{app}\updater"
Type: files; Name: "{app}\YMhut.Box.Updater.exe"
Type: files; Name: "{app}\YMhut.Box.Updater.dll"
Type: files; Name: "{app}\YMhut.Box.Updater.deps.json"
Type: files; Name: "{app}\YMhut.Box.Updater.runtimeconfig.json"
[Icons]
Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startmenuicon
Name: "{userdesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
@@ -265,6 +223,9 @@ var
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';
@@ -326,7 +287,7 @@ end;
function ShouldCleanCache(): Boolean;
begin
Result := True;
Result := WizardIsTaskSelected('cleancache');
end;
function DetectExistingInstallDir(): string;
@@ -373,9 +334,13 @@ 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();
@@ -902,37 +867,109 @@ begin
end;
end;
function ShouldPreserveAppEntry(const Name: string): Boolean;
var
LowerName: string;
function NormalizeManifestEntry(const Value: string): string;
begin
LowerName := Lowercase(Name);
Result :=
(LowerName = 'data') or
(LowerName = 'feedbackpackages') or
(Pos('unins', LowerName) = 1);
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 CleanOldApplicationPayload(const DirName: string);
procedure LoadInstallManifestFiles(const FileName: string; Files: TStringList);
var
FindRec: TFindRec;
EntryPath: string;
Lines: TStringList;
I: Integer;
Entry: string;
InFiles: Boolean;
begin
if not DirExists(DirName) then
Files.Clear;
if not FileExists(FileName) then
Exit;
if FindFirst(AddBackslash(DirName) + '*', FindRec) then
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.Name <> '.') and (FindRec.Name <> '..') and
(not ShouldPreserveAppEntry(FindRec.Name)) then
if (FindRec.Attributes and DirectoryAttribute) = 0 then
begin
EntryPath := AddBackslash(DirName) + FindRec.Name;
if ((FindRec.Attributes and DirectoryAttribute) <> 0) then
DelTree(EntryPath, True, True, True)
else
DeleteFile(EntryPath);
Result := True;
Exit;
end;
until not FindNext(FindRec);
finally
@@ -941,21 +978,169 @@ begin
end;
end;
procedure CleanLegacyInstallLayout();
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}');
CleanOldApplicationPayload(AppDir);
DeleteFilesByPattern(AppDir, '*.winmd', True);
DeleteFilesByPattern(AppDir, '*.pdb', True);
DeleteFilesByPattern(AppDir, 'workloads*.json', True);
DeleteFilesByPattern(AddBackslash(AppDir) + 'Assets\data', '*.json', True);
DeleteFile(AddBackslash(AppDir) + 'installer-layout.txt');
DeleteFile(AddBackslash(AppDir) + 'config\installer-layout.dat');
DeleteFile(AddBackslash(AppDir) + 'config\installer-required-files.dat');
DeleteFile(AddBackslash(AppDir) + 'Assets\data\reference-data.dat');
CleanCultureFolders(AppDir);
VerifyNewApplicationPayload(AppDir);
if OldPayloadFiles.Count > 0 then
DeleteKnownObsoletePayload(AppDir)
else
QuarantineUnknownLegacyFiles(AppDir, AppDir);
CleanLegacyRuntimeAndCache();
end;
function PrepareToInstall(var NeedsRestart: Boolean): string;
@@ -978,6 +1163,8 @@ begin
Exit;
end;
CaptureOldPayloadManifest();
try
if IsWebView2Installed() then
LogBootstrapEvent('DEPENDENCY', 'ready|' + GetCustomMessageValue('WebView2RuntimeName'))
@@ -1028,8 +1215,6 @@ begin
LastInstallProgressPercent := -1;
LastExtractFileName := '';
LogBootstrapEvent('STAGE', 'files');
AppendInstallOutput(CustomMessage('InstallOutputClean'));
CleanLegacyInstallLayout();
AppendInstallOutput(CustomMessage('InstallOutputExtract'));
end
else if CurStep = ssPostInstall then
@@ -1037,6 +1222,8 @@ begin
FlushInstallOutput();
LogBootstrapEvent('STAGE', 'finalize');
AppendInstallOutput(CustomMessage('InstallOutputPostInstall'));
FinalizeLegacyInstallLayout();
RegisterCustomUninstallEntry(ExpandConstant('{app}'));
#if Int(QAIsolated) == 0
MigrateLegacyData();
#endif
@@ -1044,6 +1231,14 @@ begin
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;