mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
Init AppImage support
This commit is contained in:
@@ -90,6 +90,7 @@ const NODE_PACKAGES = new Set([
|
||||
'@signalapp/mock-server',
|
||||
'@tailwindcss/cli',
|
||||
'@tailwindcss/postcss',
|
||||
'better-blockmap',
|
||||
'chokidar-cli',
|
||||
'cross-env',
|
||||
'electron-builder',
|
||||
|
||||
@@ -135,6 +135,7 @@ import { getOwn } from '../ts/util/getOwn.std.js';
|
||||
import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas.std.js';
|
||||
import { getAppErrorIcon } from '../ts/util/getAppErrorIcon.node.js';
|
||||
import { promptOSAuth } from '../ts/util/os/promptOSAuthMain.main.js';
|
||||
import { appRelaunch } from '../ts/util/relaunch.main.js';
|
||||
import { sendDummyKeystroke } from './WindowsNotifications.main.js';
|
||||
|
||||
const { chmod, realpath, writeFile } = fsExtra;
|
||||
@@ -1935,7 +1936,7 @@ const onDatabaseInitializationError = async (error: Error) => {
|
||||
log.error(
|
||||
'onDatabaseInitializationError: Requesting immediate restart after quit'
|
||||
);
|
||||
app.relaunch();
|
||||
appRelaunch();
|
||||
}
|
||||
} else if (buttonIndex === goToSupportPageButtonIndex) {
|
||||
drop(
|
||||
@@ -2343,6 +2344,11 @@ app.on('ready', async () => {
|
||||
|
||||
function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
|
||||
const { platform } = process;
|
||||
const platformForMenu =
|
||||
platform === 'linux' && process.env.APPIMAGE != null
|
||||
? 'linux-appimage'
|
||||
: platform;
|
||||
|
||||
const version = app.getVersion();
|
||||
menuOptions = {
|
||||
// options
|
||||
@@ -2351,7 +2357,7 @@ function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
|
||||
includeSetup: false,
|
||||
isNightly: isNightly(version),
|
||||
isProduction: isProduction(version),
|
||||
platform,
|
||||
platform: platformForMenu,
|
||||
|
||||
// actions
|
||||
forceUpdate,
|
||||
@@ -2630,7 +2636,7 @@ ipc.on('draw-attention', () => {
|
||||
|
||||
ipc.on('restart', () => {
|
||||
log.info('Relaunching application');
|
||||
app.relaunch();
|
||||
appRelaunch();
|
||||
app.quit();
|
||||
});
|
||||
ipc.on('shutdown', () => {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"updatesUrl": "https://updates2.signal.org/desktop",
|
||||
"resourcesUrl": "https://updates2.signal.org",
|
||||
"updatesPublicKey": "05fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401",
|
||||
"appImageUpdatesPublicKey": "05dc20ac83f663c517718b6057f2fe34e273b88455f2149513912f24a9a380cc63",
|
||||
"sfuUrl": "https://sfu.staging.voip.signal.org/",
|
||||
"challengeUrl": "https://signalcaptchas.org/staging/challenge/generate.html",
|
||||
"registrationChallengeUrl": "https://signalcaptchas.org/staging/registration/generate.html",
|
||||
|
||||
@@ -301,6 +301,7 @@
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"better-blockmap": "1.0.2",
|
||||
"casual": "1.6.2",
|
||||
"chai": "4.4.1",
|
||||
"chai-as-promised": "7.1.1",
|
||||
@@ -526,11 +527,17 @@
|
||||
"StartupWMClass": "signal"
|
||||
}
|
||||
},
|
||||
"artifactName": "${name}_${version}_${arch}.${ext}",
|
||||
"target": [
|
||||
"deb"
|
||||
],
|
||||
"icon": "build/icons/png",
|
||||
"publish": [],
|
||||
"publish": [
|
||||
{
|
||||
"provider": "generic",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "build",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -622,6 +622,9 @@ importers:
|
||||
babel-plugin-lodash:
|
||||
specifier: 3.3.4
|
||||
version: 3.3.4
|
||||
better-blockmap:
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
casual:
|
||||
specifier: 1.6.2
|
||||
version: 1.6.2(patch_hash=b88b5052437cbdc1882137778b76ca5037f71b2a030ae9ef39dc97f51670d599)
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { tmpdir } from 'node:os';
|
||||
import { mkdtemp, rm, rename, stat } from 'node:fs/promises';
|
||||
|
||||
import { mkdtemp, rm, rename, stat, writeFile } from 'node:fs/promises';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import type { ArtifactCreated } from 'electron-builder';
|
||||
import { BlockMap } from 'better-blockmap';
|
||||
|
||||
export async function artifactBuildCompleted({
|
||||
target,
|
||||
@@ -15,6 +17,14 @@ export async function artifactBuildCompleted({
|
||||
packager,
|
||||
updateInfo,
|
||||
}: ArtifactCreated): Promise<void> {
|
||||
if (packager.platform.name === 'linux' && file.endsWith('.AppImage')) {
|
||||
const blockMapPath = `${file}.blockmap`;
|
||||
console.log(`Generating blockmap ${blockMapPath}`);
|
||||
const blockMapGenerator = new BlockMap({ detectZipBoundary: true });
|
||||
await pipeline(createReadStream(file), blockMapGenerator);
|
||||
await writeFile(blockMapPath, blockMapGenerator.compress());
|
||||
}
|
||||
|
||||
if (packager.platform.name !== 'mac') {
|
||||
return;
|
||||
}
|
||||
|
||||
15
ts/scripts/better-blockmap.d.ts
vendored
Normal file
15
ts/scripts/better-blockmap.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
declare module 'better-blockmap' {
|
||||
import { Writable } from 'node:stream';
|
||||
|
||||
type BlockMapOptions = {
|
||||
detectZipBoundary?: boolean;
|
||||
};
|
||||
|
||||
export class BlockMap extends Writable {
|
||||
constructor(options?: BlockMapOptions);
|
||||
compress(compression?: 'gzip' | 'deflate'): Buffer;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export const isAutoDownloadUpdatesSupported = (
|
||||
if (isNotUpdatable(appVersion)) {
|
||||
return false;
|
||||
}
|
||||
return OS.isWindows() || OS.isMacOS();
|
||||
return OS.isWindows() || OS.isMacOS() || OS.isLinuxAppImage();
|
||||
};
|
||||
|
||||
export const shouldHideExpiringMessageBody = (
|
||||
|
||||
@@ -282,6 +282,10 @@ export abstract class Updater {
|
||||
markShouldQuit();
|
||||
}
|
||||
|
||||
protected getUpdatesPublicKey(): Buffer {
|
||||
return hexToBinary(config.get('updatesPublicKey'));
|
||||
}
|
||||
|
||||
//
|
||||
// Private methods
|
||||
//
|
||||
@@ -390,12 +394,11 @@ export abstract class Updater {
|
||||
|
||||
const { updateFilePath, signature } = downloadResult;
|
||||
|
||||
const publicKey = hexToBinary(config.get('updatesPublicKey'));
|
||||
const verified = await verifySignature(
|
||||
updateFilePath,
|
||||
this.version,
|
||||
signature,
|
||||
publicKey
|
||||
this.getUpdatesPublicKey()
|
||||
);
|
||||
if (!verified) {
|
||||
// Note: We don't delete the cache here, because we don't want to continually
|
||||
@@ -989,6 +992,10 @@ export function getUpdatesFileName(): string {
|
||||
return `${prefix}-mac.yml`;
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
return `${prefix}-linux.yml`;
|
||||
}
|
||||
|
||||
return `${prefix}.yml`;
|
||||
}
|
||||
|
||||
@@ -1020,7 +1027,7 @@ export function getVersion(info: JSONUpdateSchema): string | null {
|
||||
return info && info.version;
|
||||
}
|
||||
|
||||
const validFile = /^[A-Za-z0-9.-]+$/;
|
||||
const validFile = /^[A-Za-z0-9._-]+$/;
|
||||
export function isUpdateFileNameValid(name: string): boolean {
|
||||
return validFile.test(name);
|
||||
}
|
||||
@@ -1041,6 +1048,8 @@ export function getUpdateFileName(
|
||||
fileFilter = ({ url }) => url.includes(arch) && url.endsWith('.zip');
|
||||
} else if (platform === 'win32') {
|
||||
fileFilter = ({ url }) => url.includes(arch) && url.endsWith('.exe');
|
||||
} else if (platform === 'linux' && process.env.APPIMAGE != null) {
|
||||
fileFilter = ({ url }) => url.endsWith('.AppImage');
|
||||
}
|
||||
|
||||
if (fileFilter) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { app } from 'electron';
|
||||
import type { Updater, UpdaterOptionsType } from './common.main.js';
|
||||
import { MacOSUpdater } from './macos.main.js';
|
||||
import { WindowsUpdater } from './windows.main.js';
|
||||
import { LinuxAppImageUpdater } from './linuxAppImage.main.js';
|
||||
import { initLinux } from './linux.main.js';
|
||||
|
||||
let initialized = false;
|
||||
@@ -38,7 +39,11 @@ export async function start(options: UpdaterOptionsType): Promise<void> {
|
||||
} else if (platform === 'darwin') {
|
||||
updater = new MacOSUpdater(options);
|
||||
} else if (platform === 'linux') {
|
||||
initLinux(options);
|
||||
if (process.env.APPIMAGE != null) {
|
||||
updater = new LinuxAppImageUpdater(options);
|
||||
} else {
|
||||
initLinux(options);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`updater/start: Unsupported platform ${platform}`);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { LoggerType } from '../types/Logging.std.js';
|
||||
import { DialogType } from '../types/Dialogs.std.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
import type { UpdaterOptionsType } from './common.main.js';
|
||||
import { appRelaunch } from '../util/relaunch.main.js';
|
||||
|
||||
const MIN_UBUNTU_VERSION = '22.04';
|
||||
|
||||
@@ -60,7 +61,7 @@ export function initLinux({ logger, getMainWindow }: UpdaterOptionsType): void {
|
||||
ipcMain.handle('start-update', () => {
|
||||
logger?.info('updater/linux: restarting');
|
||||
markShouldQuit();
|
||||
app.relaunch();
|
||||
appRelaunch();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
|
||||
74
ts/updater/linuxAppImage.main.ts
Normal file
74
ts/updater/linuxAppImage.main.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { copyFile, unlink } from 'node:fs/promises';
|
||||
import { chmod } from 'fs-extra';
|
||||
|
||||
import config from 'config';
|
||||
import { app } from 'electron';
|
||||
|
||||
import { Updater } from './common.main.js';
|
||||
import { appRelaunch } from '../util/relaunch.main.js';
|
||||
import { hexToBinary } from './signature.node.js';
|
||||
|
||||
export class LinuxAppImageUpdater extends Updater {
|
||||
#installing = false;
|
||||
|
||||
protected async deletePreviousInstallers(): Promise<void> {
|
||||
// No installers are cached beyond the most recent one
|
||||
}
|
||||
|
||||
protected async installUpdate(
|
||||
updateFilePath: string
|
||||
): Promise<() => Promise<void>> {
|
||||
const { logger } = this;
|
||||
|
||||
return async () => {
|
||||
logger.info('downloadAndInstall: installing...');
|
||||
try {
|
||||
await this.#install(updateFilePath);
|
||||
this.#installing = true;
|
||||
} catch (error) {
|
||||
this.markCannotUpdate(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If interrupted at this point, we only want to restart (not reattempt install)
|
||||
this.setUpdateListener(this.restart);
|
||||
this.restart();
|
||||
};
|
||||
}
|
||||
|
||||
protected restart(): void {
|
||||
this.logger.info('downloadAndInstall: restarting...');
|
||||
|
||||
this.markRestarting();
|
||||
appRelaunch();
|
||||
app.quit();
|
||||
}
|
||||
|
||||
override getUpdatesPublicKey(): Buffer {
|
||||
return hexToBinary(config.get('appImageUpdatesPublicKey'));
|
||||
}
|
||||
|
||||
async #install(updateFilePath: string): Promise<void> {
|
||||
if (this.#installing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { logger } = this;
|
||||
|
||||
logger.info('linuxAppImage/install: installing package...');
|
||||
|
||||
const appImageFile = process.env.APPIMAGE;
|
||||
if (appImageFile == null) {
|
||||
throw new Error('APPIMAGE env is not defined!');
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/1712051/1910191
|
||||
await unlink(appImageFile);
|
||||
await copyFile(updateFilePath, appImageFile);
|
||||
await chmod(appImageFile, 0o700);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,17 @@ function getLinuxName(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
const name = match[1];
|
||||
if (isAppImage()) {
|
||||
return `${name} (AppImage)`;
|
||||
}
|
||||
// Flatpak is noted already in /etc/os-release
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function isAppImage(): boolean {
|
||||
return process.platform === 'linux' && process.env.APPIMAGE != null;
|
||||
}
|
||||
|
||||
function isFlatpak(): boolean {
|
||||
@@ -45,6 +55,7 @@ function isLinuxUsingKDE(): boolean {
|
||||
const OS = {
|
||||
...getOSFunctions(os.release()),
|
||||
getLinuxName,
|
||||
isAppImage,
|
||||
isFlatpak,
|
||||
isLinuxUsingKDE,
|
||||
isWaylandEnabled,
|
||||
|
||||
@@ -23,6 +23,7 @@ export type OSType = {
|
||||
getClassName: () => string;
|
||||
getName: () => string;
|
||||
isLinux: (minVersion?: string) => boolean;
|
||||
isLinuxAppImage: () => boolean;
|
||||
isMacOS: (minVersion?: string) => boolean;
|
||||
isWindows: (minVersion?: string) => boolean;
|
||||
};
|
||||
@@ -32,6 +33,10 @@ export function getOSFunctions(osRelease: string): OSType {
|
||||
const isLinux = createIsPlatform('linux', osRelease);
|
||||
const isWindows = createIsPlatform('win32', osRelease);
|
||||
|
||||
const isLinuxAppImage = (): boolean => {
|
||||
return process.platform === 'linux' && process.env.APPIMAGE != null;
|
||||
};
|
||||
|
||||
const getName = (): string => {
|
||||
if (isMacOS()) {
|
||||
return 'macOS';
|
||||
@@ -56,6 +61,7 @@ export function getOSFunctions(osRelease: string): OSType {
|
||||
getClassName,
|
||||
getName,
|
||||
isLinux,
|
||||
isLinuxAppImage,
|
||||
isMacOS,
|
||||
isWindows,
|
||||
};
|
||||
|
||||
21
ts/util/relaunch.main.ts
Normal file
21
ts/util/relaunch.main.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { app } from 'electron';
|
||||
import type { RelaunchOptions } from 'electron';
|
||||
|
||||
import OS from './os/osMain.node.js';
|
||||
|
||||
// app.relaunch() doesn't work in AppImage, so this is a workaround
|
||||
export function appRelaunch(): void {
|
||||
if (!OS.isAppImage()) {
|
||||
app.relaunch();
|
||||
return;
|
||||
}
|
||||
|
||||
const options: RelaunchOptions = {
|
||||
args: ['--appimage-extract-and-run', ...process.argv],
|
||||
execPath: process.env.APPIMAGE,
|
||||
};
|
||||
app.relaunch(options);
|
||||
}
|
||||
Reference in New Issue
Block a user