mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-08 21:03:46 +00:00
feat(installer): make the Windows desktop shortcut optional (#1517)
This commit is contained in:
1 parent
f4c9090b88
commit
b45d0f9ed2
4 files changed
+258
-4
No files matched your search
@@ -62,10 +62,13 @@
|
||||
"installerIcon": "public/icon.ico",
|
||||
"uninstallerIcon": "public/icon.ico",
|
||||
"shortcutName": "Termix",
|
||||
"uninstallDisplayName": "Termix"
|
||||
"uninstallDisplayName": "Termix",
|
||||
"include": "electron/installer.nsh"
|
||||
},
|
||||
"msi": {
|
||||
"artifactName": "termix_windows_${arch}_msi.${ext}"
|
||||
"artifactName": "termix_windows_${arch}_msi.${ext}",
|
||||
"oneClick": false,
|
||||
"additionalLightArgs": ["-sice:ICE57"]
|
||||
},
|
||||
"linux": {
|
||||
"artifactName": "termix_linux_${arch}_portable.${ext}",
|
||||
@@ -102,7 +105,6 @@
|
||||
"deb": {
|
||||
"artifactName": "termix_linux_${arch}_deb.${ext}"
|
||||
},
|
||||
|
||||
"mac": {
|
||||
"target": [
|
||||
{
|
||||
@@ -146,5 +148,6 @@
|
||||
"NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
|
||||
}
|
||||
},
|
||||
"generateUpdatesFilesForAllChannels": true
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"msiProjectCreated": "packaging/build/msi-shortcuts.cjs"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
!include MUI2.nsh
|
||||
!include nsDialogs.nsh
|
||||
!include LogicLib.nsh
|
||||
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
Var TermixDesktopCheckbox
|
||||
Var TermixDesktopChoice
|
||||
|
||||
; Keep electron-builder's --no-desktop-shortcut flag, adding the UI choice.
|
||||
!macro _TermixNoDesktopShortcut _a _b _t _f
|
||||
StrCmp $TermixDesktopChoice ${BST_UNCHECKED} `${_t}` 0
|
||||
${StdUtils.TestParameter} $R9 "no-desktop-shortcut"
|
||||
StrCmp $R9 "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!undef isNoDesktopShortcut
|
||||
!define isNoDesktopShortcut `"" TermixNoDesktopShortcut ""`
|
||||
|
||||
; Defined here, not at include time: electron-builder includes this file
|
||||
; before it adds the StdUtils plugin directory that isUpdated needs.
|
||||
!macro customPageAfterChangeDir
|
||||
Page custom TermixShortcutPage TermixShortcutPageLeave
|
||||
|
||||
Function TermixShortcutPage
|
||||
; Updates retain electron-builder's existing keep-shortcuts behavior.
|
||||
${If} ${isUpdated}
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro MUI_HEADER_TEXT "Shortcuts" "Choose whether to create a desktop shortcut."
|
||||
nsDialogs::Create 1018
|
||||
Pop $0
|
||||
${If} $0 == error
|
||||
Abort
|
||||
${EndIf}
|
||||
${NSD_CreateCheckbox} 0 0 100% 16u "Create a desktop shortcut"
|
||||
Pop $TermixDesktopCheckbox
|
||||
${If} $TermixDesktopChoice == ""
|
||||
StrCpy $TermixDesktopChoice ${BST_CHECKED}
|
||||
${EndIf}
|
||||
${StdUtils.TestParameter} $R9 "no-desktop-shortcut"
|
||||
${If} $R9 == "true"
|
||||
StrCpy $TermixDesktopChoice ${BST_UNCHECKED}
|
||||
EnableWindow $TermixDesktopCheckbox 0
|
||||
${EndIf}
|
||||
${NSD_SetState} $TermixDesktopCheckbox $TermixDesktopChoice
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function TermixShortcutPageLeave
|
||||
${NSD_GetState} $TermixDesktopCheckbox $TermixDesktopChoice
|
||||
FunctionEnd
|
||||
!macroend
|
||||
|
||||
!endif
|
||||
@@ -0,0 +1,74 @@
|
||||
const fs = require("node:fs/promises");
|
||||
|
||||
/** Give the generated MSI shortcut its own optional component. */
|
||||
module.exports = async function msiShortcuts(projectFile) {
|
||||
let xml = await fs.readFile(projectFile, "utf8");
|
||||
const shortcut = xml.match(/<Shortcut Id="desktopShortcut"[^>]*\/>/g);
|
||||
if (
|
||||
shortcut?.length !== 1 ||
|
||||
!xml.includes('<UIRef Id="WixUI_InstallDir"/>')
|
||||
) {
|
||||
throw new Error(
|
||||
"Unexpected MSI template: desktop shortcut or assisted UI missing",
|
||||
);
|
||||
}
|
||||
const link = shortcut[0].replace(
|
||||
' Advertise="yes"',
|
||||
' Target="[#mainExecutable]"',
|
||||
);
|
||||
xml = xml.replace(shortcut[0], "");
|
||||
xml = xml.replace(
|
||||
'<ComponentGroupRef Id="ProductComponents"/>',
|
||||
'<ComponentGroupRef Id="ProductComponents"/>\n <ComponentRef Id="TermixDesktopShortcut"/>',
|
||||
);
|
||||
xml = xml.replace(
|
||||
"</Product>",
|
||||
`
|
||||
<Property Id="TERMIX_DESKTOP_SHORTCUT" Value="1" Secure="yes"/>
|
||||
<DirectoryRef Id="DesktopFolder">
|
||||
<Component Id="TermixDesktopShortcut" Guid="*">
|
||||
<Condition>TERMIX_DESKTOP_SHORTCUT = "1"</Condition>
|
||||
${link}
|
||||
<RegistryValue Root="HKMU" Key="Software\\Termix" Name="DesktopShortcut" Type="integer" Value="1" KeyPath="yes"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
<UI>
|
||||
<Dialog Id="TermixShortcutDlg" Width="370" Height="270" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="15" Y="15" Width="340" Height="30" Text="Choose shortcuts"/>
|
||||
<Control Id="DesktopShortcut" Type="CheckBox" X="20" Y="65" Width="320" Height="20" Property="TERMIX_DESKTOP_SHORTCUT" CheckBoxValue="1" Text="Create a desktop shortcut"/>
|
||||
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="Back">
|
||||
<Publish Event="NewDialog" Value="InstallDirDlg">WixAppFolder = "WixPerMachineFolder"</Publish>
|
||||
<Publish Event="NewDialog" Value="InstallScopeDlg">WixAppFolder = "WixPerUserFolder"</Publish>
|
||||
</Control>
|
||||
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="Next">
|
||||
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
|
||||
<Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
|
||||
</Control>
|
||||
</Dialog>
|
||||
</UI>
|
||||
</Product>`,
|
||||
);
|
||||
xml = xml.replace(
|
||||
/(<Publish Dialog="InstallScopeDlg" Control="Next" Event="NewDialog" Value=")VerifyReadyDlg/g,
|
||||
"$1TermixShortcutDlg",
|
||||
);
|
||||
// WixUI_InstallDir also goes to VerifyReadyDlg at Order 4 once the folder
|
||||
// is valid, and the last true NewDialog wins, so route after it.
|
||||
xml = xml.replace(
|
||||
/<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="\d+">1<\/Publish>/g,
|
||||
'<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="TermixShortcutDlg" Order="5">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>',
|
||||
);
|
||||
xml = xml.replace(
|
||||
/(<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value=")InstallScopeDlg/g,
|
||||
"$1TermixShortcutDlg",
|
||||
);
|
||||
if (
|
||||
(xml.match(/Value="TermixShortcutDlg"/g) || []).length !== 3 ||
|
||||
!xml.includes('<ComponentRef Id="TermixDesktopShortcut"/>')
|
||||
) {
|
||||
throw new Error("Unexpected MSI template: shortcut dialog routing missing");
|
||||
}
|
||||
await fs.writeFile(projectFile, xml);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const customize = require("../../../../packaging/build/msi-shortcuts.cjs") as (
|
||||
path: string,
|
||||
) => Promise<void>;
|
||||
const { DOMParser } = require("@xmldom/xmldom");
|
||||
let dir: string;
|
||||
afterEach(async () => {
|
||||
if (dir) await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const project = `<Wix><Product>
|
||||
<UIRef Id="WixUI_InstallDir"/>
|
||||
<UI><Publish Dialog="InstallScopeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="2">1</Publish>
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallScopeDlg">1</Publish></UI>
|
||||
<Feature><ComponentGroupRef Id="ProductComponents"/></Feature>
|
||||
<ComponentGroup><Component><File Id="mainExecutable">
|
||||
<Shortcut Id="desktopShortcut" Directory="DesktopFolder" Name="Termix" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="TermixIcon.exe"/>
|
||||
<Shortcut Id="startMenuShortcut" Directory="ProgramMenuFolder" Advertise="yes"/>
|
||||
</File></Component></ComponentGroup>
|
||||
</Product></Wix>`;
|
||||
|
||||
describe("MSI desktop shortcut option", () => {
|
||||
it("makes only the desktop shortcut conditional and connects both installation scopes to its page", async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), "termix-msi-"));
|
||||
const file = join(dir, "project.wxs");
|
||||
await writeFile(file, project);
|
||||
await customize(file);
|
||||
const xml = await readFile(file, "utf8");
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
const elements = (name: string) =>
|
||||
Array.from(doc.getElementsByTagName(name)) as Element[];
|
||||
const desktop = elements("Shortcut").find(
|
||||
(node) => node.getAttribute("Id") === "desktopShortcut",
|
||||
)!;
|
||||
expect(desktop.getAttribute("Target")).toBe("[#mainExecutable]");
|
||||
expect(desktop.getAttribute("Advertise")).toBeFalsy();
|
||||
expect((desktop.parentNode as Element).getAttribute("Id")).toBe(
|
||||
"TermixDesktopShortcut",
|
||||
);
|
||||
expect(elements("Condition")[0].textContent).toBe(
|
||||
'TERMIX_DESKTOP_SHORTCUT = "1"',
|
||||
);
|
||||
expect(
|
||||
elements("Shortcut")
|
||||
.find((node) => node.getAttribute("Id") === "startMenuShortcut")!
|
||||
.getAttribute("Advertise"),
|
||||
).toBe("yes");
|
||||
expect(
|
||||
elements("Publish").filter(
|
||||
(node) => node.getAttribute("Value") === "TermixShortcutDlg",
|
||||
),
|
||||
).toHaveLength(3);
|
||||
const installDir = elements("Publish").find(
|
||||
(node) =>
|
||||
node.getAttribute("Dialog") === "InstallDirDlg" &&
|
||||
node.getAttribute("Value") === "TermixShortcutDlg",
|
||||
)!;
|
||||
// Must run after WixUI_InstallDir's own VerifyReadyDlg event (Order 4).
|
||||
expect(Number(installDir.getAttribute("Order"))).toBeGreaterThan(4);
|
||||
expect(installDir.textContent).toBe(
|
||||
'WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"',
|
||||
);
|
||||
expect(
|
||||
elements("Control")
|
||||
.find((node) => node.getAttribute("Type") === "CheckBox")!
|
||||
.getAttribute("Property"),
|
||||
).toBe("TERMIX_DESKTOP_SHORTCUT");
|
||||
});
|
||||
|
||||
it("accepts the installed electron-builder MSI template", async () => {
|
||||
const template = await readFile(
|
||||
require.resolve("app-builder-lib/templates/msi/template.xml"),
|
||||
"utf8",
|
||||
);
|
||||
const xml = require("ejs").render(
|
||||
template
|
||||
.replace(/{{/g, "<%")
|
||||
.replace(/}}/g, "%>")
|
||||
.replace(/\$\{([^}]+)}/g, "<%=$1%>"),
|
||||
{
|
||||
productName: "Termix",
|
||||
upgradeCode: "9A4E27D1-6F54-4B15-ABBD-54B194CB7200",
|
||||
version: "2.9.0",
|
||||
manufacturer: "Termix",
|
||||
compressionLevel: "high",
|
||||
installationDirectoryWixName: "Termix",
|
||||
iconPath: null,
|
||||
isRunAfterFinish: false,
|
||||
isAssisted: true,
|
||||
isPerMachine: false,
|
||||
programFilesId: "ProgramFiles64Folder",
|
||||
menuCategory: null,
|
||||
isCreateDesktopShortcut: true,
|
||||
isCreateStartMenuShortcut: true,
|
||||
dirs: "",
|
||||
files:
|
||||
'<Component Id="Main" Guid="*"><File Id="mainExecutable" Source="Termix.exe"><Shortcut Id="desktopShortcut" Directory="DesktopFolder" Name="Termix" Advertise="yes"/></File></Component>',
|
||||
},
|
||||
);
|
||||
dir = await mkdtemp(join(tmpdir(), "termix-msi-"));
|
||||
const file = join(dir, "project.wxs");
|
||||
await writeFile(file, xml);
|
||||
await customize(file);
|
||||
const result = await readFile(file, "utf8");
|
||||
expect(result.match(/Value="TermixShortcutDlg"/g)).toHaveLength(3);
|
||||
expect(result).toContain('RegistryValue Root="HKMU"');
|
||||
expect(result).toContain('<ComponentRef Id="TermixDesktopShortcut"/>');
|
||||
});
|
||||
|
||||
it("stops the build if the generated installer template changes", async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), "termix-msi-"));
|
||||
const file = join(dir, "project.wxs");
|
||||
await writeFile(file, "<Wix/>");
|
||||
await expect(customize(file)).rejects.toThrow("Unexpected MSI template");
|
||||
expect(await readFile(file, "utf8")).toBe("<Wix/>");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user