fix(desktop): download the version a linux update prompt names (#2673)

This commit is contained in:
Hampus
2026-09-11 13:19:25 +02:00
committed by GitHub
parent 84aa8880f5
commit 2830221949
3 changed files with 335 additions and 115 deletions
+11 -115
View File
@@ -1,23 +1,26 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createRequire} from 'node:module';
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
import {isPortableMode} from '@electron/common/UserDataPath';
import {destroyDesktopTray} from '@electron/main/DesktopTray';
import {isFlatpakRuntime} from '@electron/main/LinuxSandbox';
import {
DOWNLOAD_PAGE_URL,
getManualDownloadOptions,
getManualDownloadUrl,
MANUAL_DESKTOP_FORMATS,
type ManualDesktopFormat,
type ManualLatestFile,
type ManualLatestInfo,
UPDATE_BASE_URL,
type UpdaterDownloadOption,
} from '@electron/main/UpdaterDownloads';
import {setQuitting} from '@electron/main/Window';
import {app, autoUpdater, type BrowserWindow, ipcMain} from 'electron';
import log from 'electron-log';
import type {UpdateInfo} from 'velopack';
type UpdaterContext = 'user' | 'background' | 'focus';
type UpdaterDownloadOption = {
format: ManualDesktopFormat;
label: string;
url: string;
suggestedName?: string;
sha256?: string | null;
};
type UpdaterEvent =
| {
type: 'checking';
@@ -63,17 +66,6 @@ type UpdaterEvent =
};
const requireModule = createRequire(import.meta.url);
type DesktopDownloadArch = 'x64' | 'arm64';
function getDesktopDownloadArch(arch: NodeJS.Architecture): DesktopDownloadArch {
return arch === 'arm64' ? 'arm64' : 'x64';
}
const DESKTOP_DOWNLOAD_ARCH = getDesktopDownloadArch(process.arch);
const UPDATE_API_ENDPOINT = BUILD_CHANNEL === 'canary' ? 'https://api.canary.fluxer.app' : 'https://api.fluxer.app';
const UPDATE_BASE_URL = `${UPDATE_API_ENDPOINT}/dl/desktop/${BUILD_CHANNEL}/${process.platform}/${DESKTOP_DOWNLOAD_ARCH}`;
const DOWNLOAD_PAGE_URL =
BUILD_CHANNEL === 'canary' ? 'https://canary.fluxer.app/download' : 'https://fluxer.app/download';
let lastContext: UpdaterContext = 'background';
let pendingVelopackUpdate: UpdateInfo | null = null;
@@ -86,12 +78,6 @@ const UPDATE_DOWNLOAD_RETRY_BASE_DELAY_MS = 3000;
const UPDATE_DOWNLOAD_RETRY_MAX_DELAY_MS = 60000;
const ELECTRON_DOWNLOAD_MAX_RETRIES = 4;
const MANUAL_DESKTOP_FORMATS = ['setup', 'dmg', 'zip', 'appimage', 'deb', 'rpm', 'tar_gz'] as const;
type ManualDesktopFormat = (typeof MANUAL_DESKTOP_FORMATS)[number];
type ManualLatestFile = {url: string; sha256: string | null};
type LinuxManualDesktopFormat = Extract<ManualDesktopFormat, 'appimage' | 'deb' | 'rpm' | 'tar_gz'>;
function send(win: BrowserWindow | null, event: UpdaterEvent) {
win?.webContents.send('updater-event', event);
}
@@ -399,12 +385,6 @@ function registerElectronUpdater(getMainWindow: () => BrowserWindow | null): voi
});
}
type ManualLatestInfo = {
version: string;
pubDate: string | null;
files: Partial<Record<ManualDesktopFormat, ManualLatestFile>>;
};
let manualLatestCache: {at: number; info: ManualLatestInfo} | null = null;
const MANUAL_CACHE_TTL_MS = 5 * 60 * 1000;
@@ -453,90 +433,6 @@ function parseManualLatestFiles(value: unknown): Partial<Record<ManualDesktopFor
return files;
}
function getManualDownloadFormatPreference(): Array<ManualDesktopFormat> {
if (process.platform === 'linux') {
return ['appimage', 'deb', 'rpm', 'tar_gz'];
}
if (process.platform === 'darwin') {
return ['dmg', 'zip'];
}
if (process.platform === 'win32') {
return ['setup'];
}
return [];
}
const LINUX_MANUAL_FORMAT_LABELS: Record<LinuxManualDesktopFormat, string> = {
appimage: 'AppImage',
deb: 'DEB package',
rpm: 'RPM package',
tar_gz: 'tar.gz archive',
};
const LINUX_MANUAL_FORMAT_EXTENSIONS: Record<LinuxManualDesktopFormat, string> = {
appimage: '.AppImage',
deb: '.deb',
rpm: '.rpm',
tar_gz: '.tar.gz',
};
const LINUX_MANUAL_ARCH_TOKENS: Record<LinuxManualDesktopFormat, Record<DesktopDownloadArch, string>> = {
appimage: {x64: 'x86_64', arm64: 'arm64'},
deb: {x64: 'amd64', arm64: 'arm64'},
rpm: {x64: 'x86_64', arm64: 'aarch64'},
tar_gz: {x64: 'x64', arm64: 'arm64'},
};
function isLinuxManualDesktopFormat(format: ManualDesktopFormat): format is LinuxManualDesktopFormat {
return format === 'appimage' || format === 'deb' || format === 'rpm' || format === 'tar_gz';
}
function buildManualLatestDownloadUrl(format: ManualDesktopFormat): string {
return `${UPDATE_BASE_URL}/latest/${format}`;
}
function getArtifactProductName(): string {
return BUILD_CHANNEL === 'canary' ? 'Fluxer-Canary' : 'Fluxer';
}
function getManualUpdateSuggestedName(format: LinuxManualDesktopFormat, version: string): string {
const archToken = LINUX_MANUAL_ARCH_TOKENS[format][DESKTOP_DOWNLOAD_ARCH];
const extension = LINUX_MANUAL_FORMAT_EXTENSIONS[format];
return `${getArtifactProductName()}-${version}-linux-${archToken}${extension}`;
}
function getManualDownloadOptions(info: ManualLatestInfo): Array<UpdaterDownloadOption> {
if (process.platform !== 'linux') {
return [];
}
return getManualDownloadFormatPreference()
.filter(isLinuxManualDesktopFormat)
.map((format) => {
const file = info.files[format];
return {
format,
label: LINUX_MANUAL_FORMAT_LABELS[format],
url: buildManualLatestDownloadUrl(format),
suggestedName: getManualUpdateSuggestedName(format, info.version),
sha256: file?.sha256 ?? null,
};
});
}
function getManualDownloadUrl(info: ManualLatestInfo): string {
const [preferredOption] = getManualDownloadOptions(info);
if (preferredOption) {
return preferredOption.url;
}
for (const format of getManualDownloadFormatPreference()) {
const url = info.files[format]?.url;
if (url) {
return url;
}
}
return DOWNLOAD_PAGE_URL;
}
async function fetchManualLatest(options: {forceRefresh?: boolean} = {}): Promise<ManualLatestInfo> {
const now = Date.now();
if (!options.forceRefresh && manualLatestCache && now - manualLatestCache.at < MANUAL_CACHE_TTL_MS) {
@@ -0,0 +1,205 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
import {describe, test} from 'node:test';
import {fileURLToPath} from 'node:url';
import vm from 'node:vm';
const require = createRequire(import.meta.url);
const esbuild = require('esbuild');
const sourcePath = fileURLToPath(new URL('./UpdaterDownloads.ts', import.meta.url));
const source = readFileSync(sourcePath, 'utf8');
const transformedSource = esbuild.transformSync(source, {
loader: 'ts',
format: 'cjs',
platform: 'node',
target: 'node20',
}).code;
const APPIMAGE_SHA256 = 'a'.repeat(64);
const DEB_SHA256 = 'b'.repeat(64);
const TAR_GZ_SHA256 = 'c'.repeat(64);
function loadUpdaterDownloads({channel = 'stable', platform = 'linux', arch = 'x64'} = {}) {
function requireStub(specifier) {
if (specifier === '@electron/common/BuildChannel') return {BUILD_CHANNEL: channel};
throw new Error(`Unexpected import: ${specifier}`);
}
const module = {exports: {}};
const context = vm.createContext({
require: requireStub,
module,
exports: module.exports,
process: {platform, arch},
});
vm.runInContext(transformedSource, context, {filename: sourcePath});
return module.exports;
}
function latestInfo(version, files = {}) {
return {version, pubDate: null, files};
}
describe('UpdaterDownloads Linux manual update options', () => {
test('pins every format to the version its name and checksum describe', () => {
const {getManualDownloadOptions} = loadUpdaterDownloads();
const info = latestInfo('2026.910.101500', {
appimage: {
url: 'https://web.fluxer.app/api/dl/desktop/stable/linux/x64/2026.910.101500/appimage',
sha256: APPIMAGE_SHA256,
},
deb: {url: 'https://web.fluxer.app/api/dl/desktop/stable/linux/x64/2026.910.101500/deb', sha256: DEB_SHA256},
tar_gz: {
url: 'https://web.fluxer.app/api/dl/desktop/stable/linux/x64/2026.910.101500/tar_gz',
sha256: TAR_GZ_SHA256,
},
});
assert.deepEqual(structuredClone(getManualDownloadOptions(info)), [
{
format: 'appimage',
label: 'AppImage',
url: 'https://api.fluxer.app/dl/desktop/stable/linux/x64/2026.910.101500/appimage',
suggestedName: 'Fluxer-2026.910.101500-linux-x86_64.AppImage',
sha256: APPIMAGE_SHA256,
},
{
format: 'deb',
label: 'DEB package',
url: 'https://api.fluxer.app/dl/desktop/stable/linux/x64/2026.910.101500/deb',
suggestedName: 'Fluxer-2026.910.101500-linux-amd64.deb',
sha256: DEB_SHA256,
},
{
format: 'rpm',
label: 'RPM package',
url: 'https://api.fluxer.app/dl/desktop/stable/linux/x64/2026.910.101500/rpm',
suggestedName: 'Fluxer-2026.910.101500-linux-x86_64.rpm',
sha256: null,
},
{
format: 'tar_gz',
label: 'tar.gz archive',
url: 'https://api.fluxer.app/dl/desktop/stable/linux/x64/2026.910.101500/tar_gz',
suggestedName: 'Fluxer-2026.910.101500-linux-x64.tar.gz',
sha256: TAR_GZ_SHA256,
},
]);
});
test('uses arm64 architecture tokens in every url and name', () => {
const {getManualDownloadOptions} = loadUpdaterDownloads({arch: 'arm64'});
const options = structuredClone(getManualDownloadOptions(latestInfo('2026.910.101500')));
assert.deepEqual(
options.map((option) => [option.url, option.suggestedName]),
[
[
'https://api.fluxer.app/dl/desktop/stable/linux/arm64/2026.910.101500/appimage',
'Fluxer-2026.910.101500-linux-arm64.AppImage',
],
[
'https://api.fluxer.app/dl/desktop/stable/linux/arm64/2026.910.101500/deb',
'Fluxer-2026.910.101500-linux-arm64.deb',
],
[
'https://api.fluxer.app/dl/desktop/stable/linux/arm64/2026.910.101500/rpm',
'Fluxer-2026.910.101500-linux-aarch64.rpm',
],
[
'https://api.fluxer.app/dl/desktop/stable/linux/arm64/2026.910.101500/tar_gz',
'Fluxer-2026.910.101500-linux-arm64.tar.gz',
],
],
);
});
test('names canary and stable artefacts after their own product and api', () => {
const info = latestInfo('2026.910.101500');
const stableDeb = structuredClone(loadUpdaterDownloads({channel: 'stable'}).getManualDownloadOptions(info)).find(
(option) => option.format === 'deb',
);
const canaryDeb = structuredClone(loadUpdaterDownloads({channel: 'canary'}).getManualDownloadOptions(info)).find(
(option) => option.format === 'deb',
);
assert.equal(stableDeb.url, 'https://api.fluxer.app/dl/desktop/stable/linux/x64/2026.910.101500/deb');
assert.equal(stableDeb.suggestedName, 'Fluxer-2026.910.101500-linux-amd64.deb');
assert.equal(canaryDeb.url, 'https://api.canary.fluxer.app/dl/desktop/canary/linux/x64/2026.910.101500/deb');
assert.equal(canaryDeb.suggestedName, 'Fluxer-Canary-2026.910.101500-linux-amd64.deb');
});
test('only ever fetches 2026.908.173325 for a prompt built from that release', () => {
const {getManualDownloadOptions, getManualDownloadUrl} = loadUpdaterDownloads({channel: 'canary'});
const info = latestInfo('2026.908.173325', {
deb: {
url: 'https://web.canary.fluxer.app/api/dl/desktop/canary/linux/x64/2026.908.173325/deb',
sha256: DEB_SHA256,
},
});
const options = structuredClone(getManualDownloadOptions(info));
const deb = options.find((option) => option.format === 'deb');
assert.equal(deb.suggestedName, 'Fluxer-Canary-2026.908.173325-linux-amd64.deb');
assert.equal(deb.url, 'https://api.canary.fluxer.app/dl/desktop/canary/linux/x64/2026.908.173325/deb');
assert.equal(deb.sha256, DEB_SHA256);
assert.equal(options.length, 4);
for (const option of options) {
const [format, version] = new URL(option.url).pathname.split('/').reverse();
assert.equal(format, option.format);
assert.equal(version, '2026.908.173325');
assert.match(option.suggestedName, /^Fluxer-Canary-2026\.908\.173325-linux-/);
}
assert.equal(
getManualDownloadUrl(info),
'https://api.canary.fluxer.app/dl/desktop/canary/linux/x64/2026.908.173325/appimage',
);
});
});
describe('UpdaterDownloads manual download url', () => {
test('prefers the pinned Linux option over the response file urls', () => {
const {getManualDownloadUrl} = loadUpdaterDownloads();
const info = latestInfo('2026.910.101500', {
appimage: {
url: 'https://web.fluxer.app/api/dl/desktop/stable/linux/x64/2026.910.101500/appimage',
sha256: null,
},
});
assert.equal(
getManualDownloadUrl(info),
'https://api.fluxer.app/dl/desktop/stable/linux/x64/2026.910.101500/appimage',
);
});
test('falls back to the response file urls in format order on macOS', () => {
const {getManualDownloadOptions, getManualDownloadUrl} = loadUpdaterDownloads({platform: 'darwin', arch: 'arm64'});
const dmg = {url: 'https://web.fluxer.app/api/dl/desktop/stable/darwin/arm64/2026.910.101500/dmg', sha256: null};
const zip = {url: 'https://web.fluxer.app/api/dl/desktop/stable/darwin/arm64/2026.910.101500/zip', sha256: null};
assert.equal(getManualDownloadOptions(latestInfo('2026.910.101500', {dmg, zip})).length, 0);
assert.equal(getManualDownloadUrl(latestInfo('2026.910.101500', {zip, dmg})), dmg.url);
assert.equal(getManualDownloadUrl(latestInfo('2026.910.101500', {zip})), zip.url);
});
test('falls back to the setup file url on Windows', () => {
const {getManualDownloadOptions, getManualDownloadUrl} = loadUpdaterDownloads({platform: 'win32'});
const setup = {url: 'https://web.fluxer.app/api/dl/desktop/stable/win32/x64/2026.910.101500/setup', sha256: null};
assert.equal(getManualDownloadOptions(latestInfo('2026.910.101500', {setup})).length, 0);
assert.equal(getManualDownloadUrl(latestInfo('2026.910.101500', {setup})), setup.url);
});
test('falls back to the download page when no file resolves', () => {
const stable = loadUpdaterDownloads({channel: 'stable', platform: 'darwin'});
const canary = loadUpdaterDownloads({channel: 'canary', platform: 'win32'});
assert.equal(stable.getManualDownloadUrl(latestInfo('2026.910.101500')), 'https://fluxer.app/download');
assert.equal(canary.getManualDownloadUrl(latestInfo('2026.910.101500')), 'https://canary.fluxer.app/download');
});
});
+119
View File
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
export type UpdaterDownloadOption = {
format: ManualDesktopFormat;
label: string;
url: string;
suggestedName?: string;
sha256?: string | null;
};
type DesktopDownloadArch = 'x64' | 'arm64';
function getDesktopDownloadArch(arch: NodeJS.Architecture): DesktopDownloadArch {
return arch === 'arm64' ? 'arm64' : 'x64';
}
const DESKTOP_DOWNLOAD_ARCH = getDesktopDownloadArch(process.arch);
const UPDATE_API_ENDPOINT = BUILD_CHANNEL === 'canary' ? 'https://api.canary.fluxer.app' : 'https://api.fluxer.app';
export const UPDATE_BASE_URL = `${UPDATE_API_ENDPOINT}/dl/desktop/${BUILD_CHANNEL}/${process.platform}/${DESKTOP_DOWNLOAD_ARCH}`;
export const DOWNLOAD_PAGE_URL =
BUILD_CHANNEL === 'canary' ? 'https://canary.fluxer.app/download' : 'https://fluxer.app/download';
export const MANUAL_DESKTOP_FORMATS = ['setup', 'dmg', 'zip', 'appimage', 'deb', 'rpm', 'tar_gz'] as const;
export type ManualDesktopFormat = (typeof MANUAL_DESKTOP_FORMATS)[number];
export type ManualLatestFile = {url: string; sha256: string | null};
type LinuxManualDesktopFormat = Extract<ManualDesktopFormat, 'appimage' | 'deb' | 'rpm' | 'tar_gz'>;
export type ManualLatestInfo = {
version: string;
pubDate: string | null;
files: Partial<Record<ManualDesktopFormat, ManualLatestFile>>;
};
function getManualDownloadFormatPreference(): Array<ManualDesktopFormat> {
if (process.platform === 'linux') {
return ['appimage', 'deb', 'rpm', 'tar_gz'];
}
if (process.platform === 'darwin') {
return ['dmg', 'zip'];
}
if (process.platform === 'win32') {
return ['setup'];
}
return [];
}
const LINUX_MANUAL_FORMAT_LABELS: Record<LinuxManualDesktopFormat, string> = {
appimage: 'AppImage',
deb: 'DEB package',
rpm: 'RPM package',
tar_gz: 'tar.gz archive',
};
const LINUX_MANUAL_FORMAT_EXTENSIONS: Record<LinuxManualDesktopFormat, string> = {
appimage: '.AppImage',
deb: '.deb',
rpm: '.rpm',
tar_gz: '.tar.gz',
};
const LINUX_MANUAL_ARCH_TOKENS: Record<LinuxManualDesktopFormat, Record<DesktopDownloadArch, string>> = {
appimage: {x64: 'x86_64', arm64: 'arm64'},
deb: {x64: 'amd64', arm64: 'arm64'},
rpm: {x64: 'x86_64', arm64: 'aarch64'},
tar_gz: {x64: 'x64', arm64: 'arm64'},
};
function isLinuxManualDesktopFormat(format: ManualDesktopFormat): format is LinuxManualDesktopFormat {
return format === 'appimage' || format === 'deb' || format === 'rpm' || format === 'tar_gz';
}
function buildManualVersionDownloadUrl(version: string, format: ManualDesktopFormat): string {
return `${UPDATE_BASE_URL}/${version}/${format}`;
}
function getArtifactProductName(): string {
return BUILD_CHANNEL === 'canary' ? 'Fluxer-Canary' : 'Fluxer';
}
function getManualUpdateSuggestedName(format: LinuxManualDesktopFormat, version: string): string {
const archToken = LINUX_MANUAL_ARCH_TOKENS[format][DESKTOP_DOWNLOAD_ARCH];
const extension = LINUX_MANUAL_FORMAT_EXTENSIONS[format];
return `${getArtifactProductName()}-${version}-linux-${archToken}${extension}`;
}
export function getManualDownloadOptions(info: ManualLatestInfo): Array<UpdaterDownloadOption> {
if (process.platform !== 'linux') {
return [];
}
return getManualDownloadFormatPreference()
.filter(isLinuxManualDesktopFormat)
.map((format) => {
const file = info.files[format];
return {
format,
label: LINUX_MANUAL_FORMAT_LABELS[format],
url: buildManualVersionDownloadUrl(info.version, format),
suggestedName: getManualUpdateSuggestedName(format, info.version),
sha256: file?.sha256 ?? null,
};
});
}
export function getManualDownloadUrl(info: ManualLatestInfo): string {
const [preferredOption] = getManualDownloadOptions(info);
if (preferredOption) {
return preferredOption.url;
}
for (const format of getManualDownloadFormatPreference()) {
const url = info.files[format]?.url;
if (url) {
return url;
}
}
return DOWNLOAD_PAGE_URL;
}