Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/docs/platforms.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,11 @@ <h2 id="start-at-login">Start at login</h2>
Frost yourself.
</p>
<p>
On Windows the login item deliberately points at Squirrel's
<code>Update.exe</code> rather than the executable directly, because the
executable lives in a versioned directory that moves on every update.
On Windows the login item points at Squirrel's stable
<code>Frost.exe</code> launcher rather than the versioned application
executable, which moves on every update. This keeps the entry valid
while Windows identifies it as Frost instead of Squirrel's generic
<code>Update.exe</code>.
</p>

<h2 id="single-instance">One instance at a time</h2>
Expand Down
49 changes: 48 additions & 1 deletion forge.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,43 @@ const BUNDLE_ID = "frost";
// --no-git-tag-version`) before invoking Forge, so this is the version being
// built. Used to name the Windows installer, which Squirrel does not version
// for us.
const { version } = JSON.parse(
const packageMetadata = JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
);
const { version, repository } = packageMetadata;

function githubOwner(repositoryMetadata) {
const repositoryUrl =
typeof repositoryMetadata === "string"
? repositoryMetadata
: repositoryMetadata?.url;
if (typeof repositoryUrl !== "string") {
throw new Error(
"Windows publisher metadata requires a GitHub repository URL in package.json"
);
}

const scpMatch = /^git@github\.com:([^/\s]+)\//i.exec(repositoryUrl);
if (scpMatch) {
return scpMatch[1];
}

try {
const url = new URL(repositoryUrl.replace(/^git\+/, ""));
const owner = url.pathname.split("/").filter(Boolean)[0];
if (url.hostname.toLowerCase() === "github.com" && owner) {
return owner;
}
} catch {
// The error below identifies the invalid repository metadata.
}

throw new Error(
"Windows publisher metadata requires a GitHub repository URL in package.json"
);
}

const GITHUB_OWNER = githubOwner(repository);

// Signing is opt-in through the environment, the same way windowsSign is below.
// Pull request validation builds deliberately run without the Developer ID in
Expand Down Expand Up @@ -107,6 +141,13 @@ export default {
appBundleId: BUNDLE_ID,
extraResource,
out: "./out",
// Squirrel's stable launcher copies these resource values from
// Frost.exe. Task Manager uses them for the startup item's identity.
win32metadata: {
CompanyName: GITHUB_OWNER,
FileDescription: "Frost",
ProductName: "Frost",
},
...macSigning,
extendInfo: {
LSUIElement: true,
Expand All @@ -133,6 +174,12 @@ export default {
// step.
name: squirrelPackageName(arch),
exe: "Frost.exe",
// Squirrel uses this for the stable Frost.exe launcher it
// creates next to Update.exe. That is the launcher Windows
// Startup Apps should invoke, so keep its publisher aligned
// with the GitHub owner rather than the upstream updater.
authors: GITHUB_OWNER,
owners: GITHUB_OWNER,
setupExe: `Frost-win32-${arch}-${version}.exe`,
setupIcon: "./src/icons/AppIcon.ico",
// Shown next to the entry in Add/Remove Programs. Squirrel
Expand Down
38 changes: 34 additions & 4 deletions src/squirrel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const APP_USER_MODEL_ID =
*/
const UPDATE_EXE = resolve(dirname(process.execPath), "..", "Update.exe");
const EXE_NAME = basename(process.execPath);
// Squirrel also creates a stable launcher next to Update.exe. Unlike the
// generic updater, it carries Frost's own version metadata, so Windows shows
// a recognizable name and publisher in its Startup Apps list.
const SQUIRREL_LAUNCHER = resolve(dirname(process.execPath), "..", EXE_NAME);
const LOGIN_ITEM_NAME = "Frost-Update";
const LEGACY_LOGIN_ITEM_NAME = APP_USER_MODEL_ID;
const LEGACY_LOGIN_ITEM_OPTIONS = {
path: UPDATE_EXE,
args: ["--processStart", `"${EXE_NAME}"`],
};

const isWindows = process.platform === "win32";

Expand Down Expand Up @@ -104,15 +114,35 @@ export function setAppUserModelId() {
*
* On Windows a Squirrel install lives in a versioned directory, so pointing
* the registry entry at the current `Frost.exe` would break on the next
* update. Launching through `Update.exe --processStart` keeps the entry valid
* because the updater always resolves the newest version for us.
* update. Squirrel's stable root launcher keeps the entry valid and carries
* Frost's metadata; using its generic `Update.exe` directly instead makes
* Task Manager display the updater as "Update", published by GitHub.
*/
export function setOpenAtLogin(openAtLogin: boolean) {
if (isWindows && app.isPackaged) {
const legacyLaunchItem = app
.getLoginItemSettings(LEGACY_LOGIN_ITEM_OPTIONS)
.launchItems?.find(
(item) => item.name === LEGACY_LOGIN_ITEM_NAME
);

// Versions before Frost-Update used Electron's default registry value
// name. Remove that entry once so an upgrade does not leave two startup
// items behind. Preserve its enabled state: Task Manager records a
// user's choice to disable startup separately from the Run key.
if (legacyLaunchItem) {
app.setLoginItemSettings({
openAtLogin: false,
name: LEGACY_LOGIN_ITEM_NAME,
});
}
app.setLoginItemSettings({
openAtLogin,
path: UPDATE_EXE,
args: ["--processStart", `"${EXE_NAME}"`],
name: LOGIN_ITEM_NAME,
path: SQUIRREL_LAUNCHER,
...(legacyLaunchItem
? { enabled: legacyLaunchItem.enabled }
: {}),
});
return;
}
Expand Down
Loading