Files
LanMountainDesktop/LanMountainDesktop/installer/LanMountainDesktop.iss
2026-04-16 01:59:21 +08:00

692 lines
21 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.
#define MyAppName "LanMountainDesktop"
#define MyAppPublisher "LanMountainDesktop Team"
#define MyAppExeName "LanMountainDesktop.Launcher.exe"
#define MyAppId "{{5A058B0D-F95D-4A18-B9A0-93F843655DDB}"
#define MyAppRegistryId "{5A058B0D-F95D-4A18-B9A0-93F843655DDB}"
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#ifndef PublishDir
#define PublishDir "..\artifacts\publish\win-x64"
#endif
#ifndef MyOutputDir
#define MyOutputDir "..\artifacts\installer"
#endif
#ifndef MyAppArch
#define MyAppArch "x64"
#endif
#ifndef MyAppSuffix
#define MyAppSuffix ""
#endif
#ifndef IsSelfContained
#define IsSelfContained "true"
#endif
[Setup]
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DisableDirPage=no
UsePreviousAppDir=no
ShowLanguageDialog=yes
UsePreviousLanguage=no
LanguageDetectionMethod=uilanguage
DefaultGroupName={cm:AppShortcutName}
UninstallDisplayIcon={app}\{#MyAppExeName}
OutputDir={#MyOutputDir}
OutputBaseFilename={#MyAppName}-Setup-{#MyAppVersion}-{#MyAppArch}{#MyAppSuffix}
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
; Leave PrivilegesRequiredOverridesAllowed unset so users cannot downgrade
; installation mode via dialog or /ALLUSERS /CURRENTUSER command-line switches.
PrivilegesRequired=admin
CloseApplications=yes
CloseApplicationsFilter={#MyAppExeName}
RestartApplications=no
DisableProgramGroupPage=yes
#if MyAppArch == "x64"
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
#else
#if MyAppArch == "x86"
ArchitecturesAllowed=x86compatible
#endif
#endif
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "chinesesimplified"; MessagesFile: "{#SourcePath}\ChineseSimplified.isl"
[CustomMessages]
english.StartupTaskDescription=Launch LanMountainDesktop when you sign in to Windows
chinesesimplified.StartupTaskDescription=登录 Windows 时启动 LanMountainDesktop
english.AppShortcutName=LanMountainDesktop
chinesesimplified.AppShortcutName=阑山桌面
english.WebView2MissingMessage=Microsoft Edge WebView2 Runtime is required for the browser component.
chinesesimplified.WebView2MissingMessage=浏览器组件需要 Microsoft Edge WebView2 Runtime。
english.WebView2MissingAction=Click "Yes" to open the official download page. Install it first, then run this installer again.
chinesesimplified.WebView2MissingAction=单击“是”打开官方下载页面。请先完成安装,然后重新运行此安装程序。
english.WebView2OpenFailedMessage=Unable to open the download page automatically.
chinesesimplified.WebView2OpenFailedMessage=无法自动打开下载页面。
english.WebView2OpenFailedAction=Please open this URL manually:
chinesesimplified.WebView2OpenFailedAction=请手动打开以下链接:
english.UpgradePageCaption=Upgrade Existing Installation
chinesesimplified.UpgradePageCaption=升级现有安装
english.UpgradePageDescription=LanMountainDesktop is already installed on this computer. Choose how to upgrade it.
chinesesimplified.UpgradePageDescription=此计算机上已安装 LanMountainDesktop。请选择升级方式。
english.UpgradeDetectedVersionLabel=Detected version:
chinesesimplified.UpgradeDetectedVersionLabel=检测到的版本:
english.UpgradeCurrentLocationLabel=Current location:
chinesesimplified.UpgradeCurrentLocationLabel=当前安装位置:
english.UpgradePageSubCaption=Choose "Upgrade existing installation" to reuse the current location, or choose "Change installation location and migrate installation" to move the app without leaving a duplicate copy behind.
chinesesimplified.UpgradePageSubCaption=选择“升级现有安装”可复用当前安装位置;选择“更改安装位置并迁移安装”可移动应用,同时避免留下重复安装。
english.UpgradeOptionInPlace=Upgrade existing installation
chinesesimplified.UpgradeOptionInPlace=升级现有安装
english.UpgradeOptionRelocate=Change installation location and migrate installation
chinesesimplified.UpgradeOptionRelocate=更改安装位置并迁移安装
english.UpgradeUnknownVersion=Unknown
chinesesimplified.UpgradeUnknownVersion=未知
english.UpgradeCleanupMissingUninstaller=Setup found an existing installation, but its uninstaller is unavailable. Please uninstall the current version manually and run this installer again.
chinesesimplified.UpgradeCleanupMissingUninstaller=安装程序发现了现有安装,但无法找到它的卸载程序。请先手动卸载当前版本,再重新运行此安装程序。
english.UpgradeCleanupFailedPrefix=Setup could not remove the existing installation automatically. Error code:
chinesesimplified.UpgradeCleanupFailedPrefix=安装程序无法自动移除现有安装。错误代码:
english.UpgradeCleanupFailedSuffix=Please close LanMountainDesktop, uninstall the current version manually, and then run this installer again.
chinesesimplified.UpgradeCleanupFailedSuffix=请关闭 LanMountain Desktop手动卸载当前版本然后重新运行此安装程序。
english.DotNetRuntimeMissingTitle=.NET Desktop Runtime Required
chinesesimplified.DotNetRuntimeMissingTitle=需要 .NET Desktop Runtime
english.DotNetRuntimeMissingMessage=This application requires .NET 10.0 Desktop Runtime to run.
chinesesimplified.DotNetRuntimeMissingMessage=此应用程序需要 .NET 10.0 Desktop Runtime 才能运行。
english.DotNetRuntimeMissingAction=Click "Yes" to open the official download page. Install it first, then run this installer again.
chinesesimplified.DotNetRuntimeMissingAction=单击"是"打开官方下载页面。请先完成安装,然后重新运行此安装程序。
english.DotNetRuntimeOpenFailedMessage=Unable to open the download page automatically.
chinesesimplified.DotNetRuntimeOpenFailedMessage=无法自动打开下载页面。
english.DotNetRuntimeOpenFailedAction=Please open this URL manually:
chinesesimplified.DotNetRuntimeOpenFailedAction=请手动打开以下链接:
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
Name: "startup"; Description: "{cm:StartupTaskDescription}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Dirs]
Name: "{app}\log"; Permissions: users-modify
[InstallDelete]
Type: files; Name: "{app}\LanMontainDesktop.exe"
Type: files; Name: "{app}\LanMontainDesktop.dll"
Type: files; Name: "{app}\LanMontainDesktop.deps.json"
Type: files; Name: "{app}\LanMontainDesktop.runtimeconfig.json"
Type: files; Name: "{app}\LanMontainDesktop.pdb"
[Files]
Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{autoprograms}\{cm:AppShortcutName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{cm:AppShortcutName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Registry]
Root: HKA; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Tasks: startup; Flags: uninsdeletevalue
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Code]
const
UninstallRegSubkey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppRegistryId}_is1';
WebView2RuntimeKeyPath = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
WebView2RuntimeDownloadUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703';
DotNetRuntimeDownloadUrl = 'https://dotnet.microsoft.com/download/dotnet/10.0';
UpgradeChoiceInPlace = 0;
UpgradeChoiceRelocate = 1;
var
UpgradeModePage: TInputOptionWizardPage;
ExistingInstallFound: Boolean;
ExistingInstallPath: String;
ExistingInstallVersion: String;
ExistingUninstallCommand: String;
ExistingInstallWas64Bit: Boolean;
ExistingInstallIsPerUser: Boolean;
ExistingInstallRemoved: Boolean;
function NormalizePathValue(const Value: String): String;
begin
Result := RemoveBackslashUnlessRoot(Trim(Value));
end;
function CombinePath(const BasePath: String; const ChildName: String): String;
begin
if BasePath = '' then
begin
Result := ChildName;
end
else
begin
Result := NormalizePathValue(BasePath) + '\' + ChildName;
end;
end;
function SamePath(const LeftPath: String; const RightPath: String): Boolean;
begin
Result := CompareText(NormalizePathValue(LeftPath), NormalizePathValue(RightPath)) = 0;
end;
function ExtractExecutableFromCommand(const CommandLine: String): String;
var
CommandText: String;
ClosingQuotePos: Integer;
ExePos: Integer;
begin
Result := '';
CommandText := Trim(CommandLine);
if CommandText = '' then
begin
exit;
end;
if CommandText[1] = '"' then
begin
Delete(CommandText, 1, 1);
ClosingQuotePos := Pos('"', CommandText);
if ClosingQuotePos > 0 then
begin
Result := Copy(CommandText, 1, ClosingQuotePos - 1);
end
else
begin
Result := CommandText;
end;
end
else
begin
ExePos := Pos('.exe', LowerCase(CommandText));
if ExePos > 0 then
begin
Result := Copy(CommandText, 1, ExePos + 3);
end
else
begin
Result := CommandText;
end;
end;
Result := NormalizePathValue(RemoveQuotes(Result));
end;
function GetExistingExecutablePath(): String;
begin
if ExistingInstallPath = '' then
begin
Result := '';
end
else
begin
Result := CombinePath(ExistingInstallPath, '{#MyAppExeName}');
end;
end;
function GetDefaultInstallPath(): String;
begin
Result := NormalizePathValue(ExpandConstant('{autopf}\{#MyAppName}'));
end;
function GetExistingInstallVersionText(): String;
begin
Result := Trim(ExistingInstallVersion);
if Result = '' then
begin
Result := CustomMessage('UpgradeUnknownVersion');
end;
end;
procedure ShowUpgradeCleanupError(const MessageText: String);
begin
Log(MessageText);
if not WizardSilent then
begin
MsgBox(MessageText, mbError, MB_OK);
end;
end;
function TryLoadExistingInstallation(
const RootKey: Integer;
const Is64BitView: Boolean;
const IsPerUser: Boolean): Boolean;
var
InstallLocation: String;
AppPath: String;
UninstallString: String;
DisplayVersion: String;
ResolvedPath: String;
begin
Result := False;
InstallLocation := '';
AppPath := '';
UninstallString := '';
DisplayVersion := '';
if not RegKeyExists(RootKey, UninstallRegSubkey) then
begin
exit;
end;
RegQueryStringValue(RootKey, UninstallRegSubkey, 'InstallLocation', InstallLocation);
RegQueryStringValue(RootKey, UninstallRegSubkey, 'Inno Setup: App Path', AppPath);
RegQueryStringValue(RootKey, UninstallRegSubkey, 'UninstallString', UninstallString);
RegQueryStringValue(RootKey, UninstallRegSubkey, 'DisplayVersion', DisplayVersion);
ResolvedPath := NormalizePathValue(InstallLocation);
if ResolvedPath = '' then
begin
ResolvedPath := NormalizePathValue(AppPath);
end;
if (ResolvedPath = '') and (UninstallString <> '') then
begin
ResolvedPath := NormalizePathValue(ExtractFileDir(ExtractExecutableFromCommand(UninstallString)));
end;
if (ResolvedPath = '') or
(not DirExists(ResolvedPath)) or
(not FileExists(CombinePath(ResolvedPath, '{#MyAppExeName}'))) then
begin
exit;
end;
ExistingInstallFound := True;
ExistingInstallPath := ResolvedPath;
ExistingInstallVersion := Trim(DisplayVersion);
ExistingUninstallCommand := Trim(UninstallString);
ExistingInstallWas64Bit := Is64BitView;
ExistingInstallIsPerUser := IsPerUser;
Result := True;
end;
procedure DetectExistingInstallation;
begin
ExistingInstallFound := False;
ExistingInstallPath := '';
ExistingInstallVersion := '';
ExistingUninstallCommand := '';
ExistingInstallWas64Bit := False;
ExistingInstallIsPerUser := False;
ExistingInstallRemoved := False;
if IsWin64 then
begin
if TryLoadExistingInstallation(HKLM64, True, False) then
begin
exit;
end;
if TryLoadExistingInstallation(HKCU64, True, True) then
begin
exit;
end;
end;
if TryLoadExistingInstallation(HKLM32, False, False) then
begin
exit;
end;
TryLoadExistingInstallation(HKCU32, False, True);
end;
function SelectedUpgradeChoice(): Integer;
begin
if UpgradeModePage <> nil then
begin
Result := UpgradeModePage.SelectedValueIndex;
end
else
begin
Result := UpgradeChoiceInPlace;
end;
end;
procedure ApplySelectedInstallDirectory;
var
CurrentDir: String;
begin
if not ExistingInstallFound then
begin
exit;
end;
if SelectedUpgradeChoice() = UpgradeChoiceInPlace then
begin
WizardForm.DirEdit.Text := ExistingInstallPath;
exit;
end;
CurrentDir := NormalizePathValue(WizardDirValue);
if (CurrentDir = '') or SamePath(CurrentDir, GetDefaultInstallPath()) then
begin
WizardForm.DirEdit.Text := ExistingInstallPath;
end;
end;
function GetSelectedInstallPath(): String;
begin
Result := NormalizePathValue(ExpandConstant('{app}'));
if Result = '' then
begin
Result := NormalizePathValue(WizardDirValue);
end;
if Result = '' then
begin
Result := ExistingInstallPath;
end;
end;
function ExistingInstallRequiresCleanup(): Boolean;
var
TargetPath: String;
begin
Result := False;
if not ExistingInstallFound or ExistingInstallRemoved then
begin
exit;
end;
TargetPath := GetSelectedInstallPath();
Result := ExistingInstallIsPerUser or
(not SamePath(TargetPath, ExistingInstallPath)) or
(ExistingInstallWas64Bit <> Is64BitInstallMode);
end;
function RemoveExistingInstallation(): Boolean;
var
UninstallerPath: String;
ResultCode: Integer;
begin
Result := True;
if not ExistingInstallRequiresCleanup() then
begin
exit;
end;
UninstallerPath := ExtractExecutableFromCommand(ExistingUninstallCommand);
if (UninstallerPath = '') or (not FileExists(UninstallerPath)) then
begin
ShowUpgradeCleanupError(CustomMessage('UpgradeCleanupMissingUninstaller'));
Result := False;
exit;
end;
ResultCode := -1;
if not Exec(
UninstallerPath,
'/VERYSILENT /SUPPRESSMSGBOXES /NORESTART',
ExtractFileDir(UninstallerPath),
SW_SHOWNORMAL,
ewWaitUntilTerminated,
ResultCode) or (ResultCode <> 0) then
begin
ShowUpgradeCleanupError(
CustomMessage('UpgradeCleanupFailedPrefix') + ' ' + IntToStr(ResultCode) + '. ' +
CustomMessage('UpgradeCleanupFailedSuffix'));
Result := False;
exit;
end;
ExistingInstallRemoved := True;
end;
function IsWebView2RuntimeInstalled(): Boolean;
var
VersionValue: String;
begin
Result :=
RegQueryStringValue(HKLM64, WebView2RuntimeKeyPath, 'pv', VersionValue) or
RegQueryStringValue(HKLM32, WebView2RuntimeKeyPath, 'pv', VersionValue) or
RegQueryStringValue(HKCU64, WebView2RuntimeKeyPath, 'pv', VersionValue) or
RegQueryStringValue(HKCU32, WebView2RuntimeKeyPath, 'pv', VersionValue);
end;
// Checks whether a .NET 10.x shared framework is installed under the given
// base path by enumerating version sub-directories and looking for one that
// starts with '10.'.
function IsDotNet10RuntimePresent(const BasePath: String): Boolean;
var
FindRec: TFindRec;
begin
Result := False;
if not DirExists(BasePath) then
begin
exit;
end;
if FindFirst(BasePath + '\*', FindRec) then
begin
try
repeat
if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY <> 0) and
(Length(FindRec.Name) >= 3) and
(Copy(FindRec.Name, 1, 3) = '10.') then
begin
Result := True;
exit;
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
// Returns True when the .NET 10 Desktop Runtime (or the .NET 10 Core Runtime
// which is sufficient for Avalonia apps) is found on the system.
// We check both Microsoft.WindowsDesktop.App and Microsoft.NETCore.App because
// the runtimeconfig.json may reference either framework depending on the
// publish mode and the app only needs the one it actually references.
function IsDotNetDesktopRuntimeInstalled(): Boolean;
var
BasePath: String;
begin
Result := False;
// Check 64-bit Program Files
BasePath := ExpandConstant('{commonpf64}\dotnet\shared\Microsoft.WindowsDesktop.App');
if IsDotNet10RuntimePresent(BasePath) then
begin
Result := True;
exit;
end;
BasePath := ExpandConstant('{commonpf64}\dotnet\shared\Microsoft.NETCore.App');
if IsDotNet10RuntimePresent(BasePath) then
begin
Result := True;
exit;
end;
// Check 32-bit Program Files
BasePath := ExpandConstant('{commonpf}\dotnet\shared\Microsoft.WindowsDesktop.App');
if IsDotNet10RuntimePresent(BasePath) then
begin
Result := True;
exit;
end;
BasePath := ExpandConstant('{commonpf}\dotnet\shared\Microsoft.NETCore.App');
if IsDotNet10RuntimePresent(BasePath) then
begin
Result := True;
exit;
end;
end;
function InitializeSetup(): Boolean;
var
ErrorCode: Integer;
IsSelfContainedBuild: Boolean;
begin
IsSelfContainedBuild := ('{#IsSelfContained}' = 'true');
if not IsSelfContainedBuild then
begin
if not IsDotNetDesktopRuntimeInstalled() then
begin
if MsgBox(
CustomMessage('DotNetRuntimeMissingMessage') + #13#10#13#10 +
CustomMessage('DotNetRuntimeMissingAction'),
mbConfirmation,
MB_YESNO) = IDYES then
begin
if not ShellExec('open', DotNetRuntimeDownloadUrl, '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode) then
begin
MsgBox(
CustomMessage('DotNetRuntimeOpenFailedMessage') + #13#10 +
CustomMessage('DotNetRuntimeOpenFailedAction') + #13#10 + DotNetRuntimeDownloadUrl,
mbError,
MB_OK);
end;
end;
Result := False;
exit;
end;
end;
if IsWebView2RuntimeInstalled() then
begin
Result := True;
exit;
end;
if MsgBox(
CustomMessage('WebView2MissingMessage') + #13#10#13#10 +
CustomMessage('WebView2MissingAction'),
mbConfirmation,
MB_YESNO) = IDYES then
begin
if not ShellExec('open', WebView2RuntimeDownloadUrl, '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode) then
begin
MsgBox(
CustomMessage('WebView2OpenFailedMessage') + #13#10 +
CustomMessage('WebView2OpenFailedAction') + #13#10 + WebView2RuntimeDownloadUrl,
mbError,
MB_OK);
end;
end;
Result := False;
end;
procedure InitializeWizard;
var
DetailsText: String;
begin
DetectExistingInstallation;
if not ExistingInstallFound then
begin
exit;
end;
DetailsText :=
CustomMessage('UpgradeDetectedVersionLabel') + ' ' + GetExistingInstallVersionText() + #13#10 +
CustomMessage('UpgradeCurrentLocationLabel') + ' ' + ExistingInstallPath + #13#10#13#10 +
CustomMessage('UpgradePageSubCaption');
UpgradeModePage := CreateInputOptionPage(
wpWelcome,
CustomMessage('UpgradePageCaption'),
CustomMessage('UpgradePageDescription'),
DetailsText,
True,
False);
UpgradeModePage.Add(CustomMessage('UpgradeOptionInPlace'));
UpgradeModePage.Add(CustomMessage('UpgradeOptionRelocate'));
UpgradeModePage.SelectedValueIndex := UpgradeChoiceInPlace;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
Result := True;
if (UpgradeModePage <> nil) and (CurPageID = UpgradeModePage.ID) then
begin
ApplySelectedInstallDirectory;
end;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (UpgradeModePage <> nil) and (PageID = UpgradeModePage.ID) then
begin
Result := not ExistingInstallFound;
exit;
end;
if PageID = wpSelectDir then
begin
Result := ExistingInstallFound and (SelectedUpgradeChoice() = UpgradeChoiceInPlace);
end;
end;
procedure RegisterExtraCloseApplicationsResources;
var
ExistingExecutablePath: String;
begin
if not ExistingInstallFound then
begin
exit;
end;
ExistingExecutablePath := GetExistingExecutablePath();
if (ExistingExecutablePath <> '') and FileExists(ExistingExecutablePath) then
begin
RegisterExtraCloseApplicationsResource(False, ExistingExecutablePath);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
LauncherPath: String;
AppDirPath: String;
begin
if CurStep = ssInstall then
begin
if not RemoveExistingInstallation() then
begin
Abort;
end;
end;
if CurStep = ssPostInstall then
begin
// 验证 Launcher 是否存在
LauncherPath := ExpandConstant('{app}\{#MyAppExeName}');
if not FileExists(LauncherPath) then
begin
MsgBox('安装验证失败: Launcher 可执行文件不存在。' + #13#10 +
'预期路径: ' + LauncherPath + #13#10 + #13#10 +
'请联系开发者报告此问题。', mbError, MB_OK);
Abort;
end;
// 验证至少存在一个 app-* 目录
AppDirPath := ExpandConstant('{app}\app-{#MyAppVersion}');
if not DirExists(AppDirPath) then
begin
MsgBox('安装验证失败: 应用版本目录不存在。' + #13#10 +
'预期路径: ' + AppDirPath + #13#10 + #13#10 +
'请联系开发者报告此问题。', mbError, MB_OK);
Abort;
end;
end;
end;