Files
fluxer/fluxer_desktop/electron-builder.config.cjs
T

1518 lines
54 KiB
JavaScript

// SPDX-License-Identifier: AGPL-3.0-or-later
const isCanary = process.env.BUILD_CHANNEL === 'canary';
const {execFile} = require('node:child_process');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const {promisify} = require('node:util');
const execFileAsync = promisify(execFile);
const productName = isCanary ? 'Fluxer Canary' : 'Fluxer';
const artifactProductName = isCanary ? 'Fluxer-Canary' : 'Fluxer';
const appId = isCanary ? 'app.fluxer.canary' : 'app.fluxer';
const iconDir = isCanary ? 'icons-canary' : 'icons-stable';
const packageName = isCanary ? 'fluxer_desktop_canary' : 'fluxer_desktop';
const linuxPackageName = isCanary ? 'fluxer-canary' : 'fluxer';
const linuxDesktopActionIds = ['open-settings', 'new-dm'];
const linuxDesktopActionList = `${linuxDesktopActionIds.join(';')};`;
const linuxGlibcBaseline = Object.freeze({major: 2, minor: 35, patch: 0, name: 'GLIBC_2.35'});
const rpmBuildIdFilePrefix = '/usr/lib/.build-id';
const rpmBuildIdLinkFpmArgs = [
'--rpm-rpmbuild-define',
'_build_id_links none',
'--rpm-rpmbuild-define',
'_missing_build_ids_terminate_build 0',
];
const macOSMinimumSystemVersion = '12.0';
const isLinuxBuild = process.argv.includes('--linux');
const isMacBuild = process.argv.includes('--mac');
const isWindowsBuild = process.argv.includes('--win');
const targetPlatform = isLinuxBuild ? 'linux' : isMacBuild ? 'darwin' : isWindowsBuild ? 'win32' : process.platform;
const metadataName = isLinuxBuild ? linuxPackageName : packageName;
const provisioningProfile = isCanary
? 'build_resources/profiles/Fluxer_Canary.provisionprofile'
: 'build_resources/profiles/Fluxer.provisionprofile';
const supportedTargetArchs = ['x64', 'arm64'];
const supportedMacTargetArchs = [...supportedTargetArchs, 'universal'];
const electronArch = process.env.ELECTRON_ARCH;
const cliTargetArch = supportedMacTargetArchs.find((arch) => process.argv.includes(`--${arch}`)) || null;
const targetNativeArch = electronArch || cliTargetArch;
if (electronArch && !supportedMacTargetArchs.includes(electronArch)) {
throw new Error(`Unsupported ELECTRON_ARCH: ${electronArch}`);
}
if (targetNativeArch === 'universal' && targetPlatform !== 'darwin') {
throw new Error(`ELECTRON_ARCH=universal is only supported for macOS builds, received platform ${targetPlatform}`);
}
const targetArchs = electronArch && electronArch !== 'universal' ? [electronArch] : supportedTargetArchs;
const macTargetArchs = targetNativeArch ? [targetNativeArch] : supportedTargetArchs;
const winGameCaptureTargetArchs =
targetPlatform === 'win32' && targetNativeArch ? [targetNativeArch] : supportedTargetArchs;
const winTargets = [
{
target: 'dir',
arch: targetArchs,
},
];
const fluxerNativePackages = [
'@fluxer/mac-app-audio',
'@fluxer/mac-clipboard',
'@fluxer/mac-screen-capture',
'@fluxer/mac-sysctl',
'@fluxer/mac-tcc',
'@fluxer/macos-input-hook',
'@fluxer/win-process-loopback',
'@fluxer/win-game-capture',
'@fluxer/win-clipboard',
'@fluxer/win-shell',
'@fluxer/win-toast',
'@fluxer/windows-input-hook',
'@fluxer/linux-audio-capture',
'@fluxer/linux-portals',
'@fluxer/linux-screen-capture',
'@fluxer/linux-notifications',
'@fluxer/linux-evdev',
'@fluxer/linux-input-hook',
'@fluxer/system-hunspell',
'@fluxer/hardware-encoder',
'@fluxer/platform-info',
'@fluxer/webauthn',
];
const fluxerNativePackagesByPlatform = {
darwin: [
'@fluxer/mac-app-audio',
'@fluxer/mac-clipboard',
'@fluxer/mac-screen-capture',
'@fluxer/mac-sysctl',
'@fluxer/mac-tcc',
'@fluxer/macos-input-hook',
'@fluxer/platform-info',
'@fluxer/webauthn',
'@fluxer/hardware-encoder',
],
win32: [
'@fluxer/win-process-loopback',
'@fluxer/win-game-capture',
'@fluxer/win-clipboard',
'@fluxer/win-shell',
'@fluxer/win-toast',
'@fluxer/windows-input-hook',
'@fluxer/platform-info',
'@fluxer/webauthn',
'@fluxer/hardware-encoder',
],
linux: [
'@fluxer/linux-audio-capture',
'@fluxer/linux-portals',
'@fluxer/linux-screen-capture',
'@fluxer/linux-notifications',
'@fluxer/linux-evdev',
'@fluxer/linux-input-hook',
'@fluxer/system-hunspell',
'@fluxer/platform-info',
'@fluxer/webauthn',
'@fluxer/hardware-encoder',
],
};
const velopackNativeFiles = [
'velopack_nodeffi_linux_arm64_gnu.node',
'velopack_nodeffi_linux_x64_gnu.node',
'velopack_nodeffi_osx.node',
'velopack_nodeffi_win_arm64_msvc.node',
'velopack_nodeffi_win_x64_msvc.node',
'velopack_nodeffi_win_x86_msvc.node',
];
const nativeRuntimeFilePatterns = [
'node_modules/@fluxer/mac-app-audio/package.json',
'node_modules/@fluxer/mac-app-audio/index.js',
'node_modules/@fluxer/mac-app-audio/loader-diagnostics.cjs',
'node_modules/@fluxer/mac-app-audio/*.node',
'node_modules/@fluxer/mac-screen-capture/package.json',
'node_modules/@fluxer/mac-screen-capture/index.js',
'node_modules/@fluxer/mac-screen-capture/loader-diagnostics.cjs',
'node_modules/@fluxer/mac-screen-capture/*.node',
'node_modules/@fluxer/mac-clipboard/package.json',
'node_modules/@fluxer/mac-clipboard/index.js',
'node_modules/@fluxer/mac-clipboard/loader-diagnostics.cjs',
'node_modules/@fluxer/mac-clipboard/*.node',
'node_modules/@fluxer/mac-sysctl/package.json',
'node_modules/@fluxer/mac-sysctl/index.js',
'node_modules/@fluxer/mac-sysctl/loader-diagnostics.cjs',
'node_modules/@fluxer/mac-sysctl/*.node',
'node_modules/@fluxer/mac-tcc/package.json',
'node_modules/@fluxer/mac-tcc/index.js',
'node_modules/@fluxer/mac-tcc/loader-diagnostics.cjs',
'node_modules/@fluxer/mac-tcc/*.node',
'node_modules/@fluxer/win-process-loopback/package.json',
'node_modules/@fluxer/win-process-loopback/index.js',
'node_modules/@fluxer/win-process-loopback/binding.js',
'node_modules/@fluxer/win-process-loopback/loader-diagnostics.cjs',
'node_modules/@fluxer/win-process-loopback/*.node',
'node_modules/@fluxer/win-game-capture/package.json',
'node_modules/@fluxer/win-game-capture/index.js',
'node_modules/@fluxer/win-game-capture/loader-diagnostics.cjs',
...winGameCaptureTargetArchs.map(
(arch) => `node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/@fluxer/win-clipboard/package.json',
'node_modules/@fluxer/win-clipboard/index.js',
'node_modules/@fluxer/win-clipboard/loader-diagnostics.cjs',
'node_modules/@fluxer/win-clipboard/*.node',
'node_modules/@fluxer/win-shell/package.json',
'node_modules/@fluxer/win-shell/index.js',
'node_modules/@fluxer/win-shell/loader-diagnostics.cjs',
'node_modules/@fluxer/win-shell/*.node',
'node_modules/@fluxer/win-toast/package.json',
'node_modules/@fluxer/win-toast/index.js',
'node_modules/@fluxer/win-toast/loader-diagnostics.cjs',
'node_modules/@fluxer/win-toast/*.node',
'node_modules/@fluxer/linux-audio-capture/package.json',
'node_modules/@fluxer/linux-audio-capture/index.js',
'node_modules/@fluxer/linux-audio-capture/loader-diagnostics.cjs',
'node_modules/@fluxer/linux-audio-capture/*.node',
'node_modules/@fluxer/linux-portals/package.json',
'node_modules/@fluxer/linux-portals/index.js',
'node_modules/@fluxer/linux-portals/loader-diagnostics.cjs',
'node_modules/@fluxer/linux-portals/*.node',
'node_modules/@fluxer/linux-screen-capture/package.json',
'node_modules/@fluxer/linux-screen-capture/index.js',
'node_modules/@fluxer/linux-screen-capture/loader-diagnostics.cjs',
'node_modules/@fluxer/linux-screen-capture/*.node',
'node_modules/@fluxer/linux-screen-capture/THIRD_PARTY_OBS_VKCAPTURE.md',
'node_modules/@fluxer/linux-screen-capture/obs-vkcapture/**/*',
'node_modules/@fluxer/linux-notifications/package.json',
'node_modules/@fluxer/linux-notifications/index.js',
'node_modules/@fluxer/linux-notifications/loader-diagnostics.cjs',
'node_modules/@fluxer/linux-notifications/*.node',
'node_modules/@fluxer/linux-evdev/package.json',
'node_modules/@fluxer/linux-evdev/index.js',
'node_modules/@fluxer/linux-evdev/loader-diagnostics.cjs',
'node_modules/@fluxer/linux-evdev/*.node',
'node_modules/@fluxer/system-hunspell/package.json',
'node_modules/@fluxer/system-hunspell/index.js',
'node_modules/@fluxer/system-hunspell/loader-diagnostics.cjs',
'node_modules/@fluxer/system-hunspell/*.node',
'node_modules/@fluxer/macos-input-hook/package.json',
'node_modules/@fluxer/macos-input-hook/index.js',
'node_modules/@fluxer/macos-input-hook/loader-diagnostics.cjs',
'node_modules/@fluxer/macos-input-hook/*.node',
'node_modules/@fluxer/windows-input-hook/package.json',
'node_modules/@fluxer/windows-input-hook/index.js',
'node_modules/@fluxer/windows-input-hook/loader-diagnostics.cjs',
'node_modules/@fluxer/windows-input-hook/*.node',
'node_modules/@fluxer/linux-input-hook/package.json',
'node_modules/@fluxer/linux-input-hook/index.js',
'node_modules/@fluxer/linux-input-hook/loader-diagnostics.cjs',
'node_modules/@fluxer/linux-input-hook/*.node',
'node_modules/@fluxer/platform-info/package.json',
'node_modules/@fluxer/platform-info/index.js',
'node_modules/@fluxer/platform-info/pure.cjs',
'node_modules/@fluxer/platform-info/loader-diagnostics.cjs',
'node_modules/@fluxer/platform-info/*.node',
'node_modules/@fluxer/webauthn/package.json',
'node_modules/@fluxer/webauthn/index.js',
'node_modules/@fluxer/webauthn/index.d.ts',
'node_modules/@fluxer/webauthn/pure.cjs',
'node_modules/@fluxer/webauthn/loader-diagnostics.cjs',
'node_modules/@fluxer/webauthn/*.node',
'node_modules/@fluxer/webauthn/*.so*',
'node_modules/@fluxer/hardware-encoder/package.json',
'node_modules/@fluxer/hardware-encoder/index.js',
'node_modules/@fluxer/hardware-encoder/index.d.ts',
'node_modules/@fluxer/hardware-encoder/*.node',
'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/loader-diagnostics.cjs',
'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/pure.cjs',
'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node',
...winGameCaptureTargetArchs.map(
(arch) =>
`node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/.pnpm/@fluxer+win-clipboard@*/node_modules/@fluxer/win-clipboard/*.node',
'node_modules/.pnpm/@fluxer+win-shell@*/node_modules/@fluxer/win-shell/*.node',
'node_modules/.pnpm/@fluxer+win-toast@*/node_modules/@fluxer/win-toast/*.node',
'node_modules/.pnpm/@fluxer+windows-input-hook@*/node_modules/@fluxer/windows-input-hook/*.node',
'node_modules/.pnpm/@fluxer+linux-audio-capture@*/node_modules/@fluxer/linux-audio-capture/*.node',
'node_modules/.pnpm/@fluxer+linux-portals@*/node_modules/@fluxer/linux-portals/*.node',
'node_modules/.pnpm/@fluxer+linux-screen-capture@*/node_modules/@fluxer/linux-screen-capture/*.node',
'node_modules/.pnpm/@fluxer+linux-screen-capture@*/node_modules/@fluxer/linux-screen-capture/THIRD_PARTY_OBS_VKCAPTURE.md',
'node_modules/.pnpm/@fluxer+linux-screen-capture@*/node_modules/@fluxer/linux-screen-capture/obs-vkcapture/**/*',
'node_modules/.pnpm/@fluxer+linux-notifications@*/node_modules/@fluxer/linux-notifications/*.node',
'node_modules/.pnpm/@fluxer+linux-evdev@*/node_modules/@fluxer/linux-evdev/*.node',
'node_modules/.pnpm/@fluxer+linux-input-hook@*/node_modules/@fluxer/linux-input-hook/*.node',
'node_modules/.pnpm/@fluxer+mac-app-audio@*/node_modules/@fluxer/mac-app-audio/*.node',
'node_modules/.pnpm/@fluxer+mac-screen-capture@*/node_modules/@fluxer/mac-screen-capture/*.node',
'node_modules/.pnpm/@fluxer+mac-clipboard@*/node_modules/@fluxer/mac-clipboard/*.node',
'node_modules/.pnpm/@fluxer+mac-sysctl@*/node_modules/@fluxer/mac-sysctl/*.node',
'node_modules/.pnpm/@fluxer+mac-tcc@*/node_modules/@fluxer/mac-tcc/*.node',
'node_modules/.pnpm/@fluxer+macos-input-hook@*/node_modules/@fluxer/macos-input-hook/*.node',
'node_modules/.pnpm/@fluxer+platform-info@*/node_modules/@fluxer/platform-info/*.node',
'node_modules/.pnpm/@fluxer+webauthn@*/node_modules/@fluxer/webauthn/*.node',
'node_modules/.pnpm/@fluxer+webauthn@*/node_modules/@fluxer/webauthn/*.so*',
'node_modules/.pnpm/@fluxer+hardware-encoder@*/node_modules/@fluxer/hardware-encoder/*.node',
];
const nativeBuildArtifactExcludes = [
'!node_modules/@fluxer/**/src/**/*',
'!node_modules/@fluxer/**/target/**/*',
'!node_modules/@fluxer/**/build/**/*',
'!node_modules/@fluxer/**/Cargo.toml',
'!node_modules/@fluxer/**/Cargo.lock',
'!node_modules/@fluxer/**/CMakeLists.txt',
'!node_modules/@fluxer/**/tsconfig.json',
'!node_modules/@fluxer/**/*.d.ts',
'!node_modules/@fluxer/**/*.d.ts.map',
'!node_modules/@fluxer/**/*.map',
'!node_modules/@fluxer/**/*.rs',
'!node_modules/@fluxer/**/*.swift',
];
const packagedRuntimeArtifactExcludes = [
'!dist/**/*.map',
'!node_modules/**/.cache/**/*',
'!node_modules/**/.github/**/*',
'!node_modules/**/.yarn/**/*',
'!node_modules/**/.yarnrc.yml',
'!node_modules/**/*.map',
'!node_modules/**/*.d.ts',
'!node_modules/**/*.d.ts.map',
'!node_modules/**/*.tsbuildinfo',
'!node_modules/**/tsconfig.json',
'!node_modules/**/tsconfig.*.json',
'!node_modules/**/README*',
'!node_modules/**/CHANGELOG*',
'!node_modules/hunspell-asm/src/**/*',
'!node_modules/hunspell-asm/dist/esm/**/*',
'!node_modules/hunspell-asm/dist/types/**/*',
'!node_modules/hunspell-asm/dist/cjs/lib/browser/**/*',
'!node_modules/emscripten-wasm-loader/src/**/*',
'!node_modules/emscripten-wasm-loader/dist/esm/**/*',
'!node_modules/emscripten-wasm-loader/dist/types/**/*',
];
const bundledDependencyExcludes = [
'!node_modules/@homebridge/dbus-native/**/*',
'!node_modules/@simplewebauthn/browser/**/*',
'!node_modules/duplexer/**/*',
'!node_modules/event-stream/**/*',
'!node_modules/from/**/*',
'!node_modules/hexy/**/*',
'!node_modules/long/**/*',
'!node_modules/map-stream/**/*',
'!node_modules/minimist/**/*',
'!node_modules/pause-stream/**/*',
'!node_modules/safe-buffer/**/*',
'!node_modules/sax/**/*',
'!node_modules/split/**/*',
'!node_modules/stream-combiner/**/*',
'!node_modules/through/**/*',
'!node_modules/xml2js/**/*',
'!node_modules/xmlbuilder/**/*',
];
const platformNativeRuntimeExcludes = platformNativeExcludes(targetPlatform, targetNativeArch);
const platformRuntimeDependencyExcludes =
targetPlatform === 'darwin'
? []
: ['!node_modules/github-url-to-object/**/*', '!node_modules/ms/**/*', '!node_modules/update-electron-app/**/*'];
const linuxDesktopEntry = {
Name: productName,
GenericName: 'Instant Messenger',
Comment: isCanary ? 'Canary build of Fluxer' : 'Instant messaging and VoIP',
Keywords: 'chat;im;messaging;messenger;voip;voice;video;call;',
Categories: 'Network;InstantMessaging;Chat;',
StartupWMClass: linuxPackageName,
StartupNotify: 'true',
SingleMainWindow: 'true',
MimeType: 'x-scheme-handler/fluxer;',
'X-GNOME-UsesNotifications': 'true',
};
const linuxDesktopEntryWithActions = {
...linuxDesktopEntry,
Actions: linuxDesktopActionList,
};
const linuxInstalledExecPath = quoteDesktopExecArg(path.posix.join('/opt', productName, linuxPackageName));
const linuxDesktopActions = {
'open-settings': {
Name: 'Open Settings',
Exec: buildLinuxDesktopTaskExec('open-settings'),
},
'new-dm': {
Name: 'New Direct Message',
Exec: buildLinuxDesktopTaskExec('new-dm'),
},
};
function velopackNativeFile(platform, arch) {
if (platform === 'darwin') return 'velopack_nodeffi_osx.node';
if (!arch) return null;
if (platform === 'win32') return `velopack_nodeffi_win_${arch}_msvc.node`;
if (platform === 'linux') return `velopack_nodeffi_linux_${arch}_gnu.node`;
return null;
}
function pnpmStoreDirName(packageName) {
return packageName.replace('/', '+');
}
function windowsGameCaptureArtifactExcludes(arch) {
const packageRoots = [
'node_modules/@fluxer/win-game-capture',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture',
];
const excludedNodeArchs = [...supportedTargetArchs, 'ia32'].filter((candidate) => candidate !== arch);
return packageRoots.flatMap((packageRoot) => [
...excludedNodeArchs.map((excludedArch) => `!${packageRoot}/win-game-capture.win32-${excludedArch}-msvc.node`),
]);
}
function platformNativeExcludes(platform, arch) {
const keepFluxerPackages = new Set(fluxerNativePackagesByPlatform[platform] ?? []);
const fluxerPackageExcludes = fluxerNativePackages
.filter((packageName) => !keepFluxerPackages.has(packageName))
.flatMap((packageName) => [
`!node_modules/${packageName}/**/*`,
`!node_modules/.pnpm/${pnpmStoreDirName(packageName)}@*/**/*`,
]);
if (platform !== 'win32') {
return [...fluxerPackageExcludes, '!node_modules/velopack/**/*'];
}
const keepVelopackNativeFile = velopackNativeFile(platform, arch);
if (!keepVelopackNativeFile) {
throw new Error(
`Cannot determine the Velopack native module for win32 without a target architecture; set ELECTRON_ARCH or pass --x64/--arm64 (received ${JSON.stringify(arch)})`,
);
}
return [
...fluxerPackageExcludes,
...windowsGameCaptureArtifactExcludes(arch),
...velopackNativeFiles
.filter((fileName) => fileName !== keepVelopackNativeFile)
.map((fileName) => `!node_modules/velopack/lib/native/${fileName}`),
];
}
function quoteDesktopExecArg(value) {
return `"${value.replace(/(["`$\\])/g, '\\$1')}"`;
}
function buildLinuxDesktopTaskExec(taskId) {
return `${linuxInstalledExecPath} --fluxer-task=${taskId} %U`;
}
function normalizeArch(arch) {
if (arch === 'x64' || arch === 'arm64') {
return arch;
}
if (arch === 'universal') return 'universal';
if (arch === 1) return 'x64';
if (arch === 3) return 'arm64';
if (arch === 4) return 'universal';
return electronArch || process.arch;
}
function platformTag(platform, arch) {
if (platform === 'darwin') return `darwin-${arch}`;
if (platform === 'win32') return `win32-${arch}-msvc`;
if (platform === 'linux') return `linux-${arch}-gnu`;
return null;
}
function addWindowsGameCaptureArtifacts(artifacts, tag) {
artifacts.push({
packageName: '@fluxer/win-game-capture',
relativePath: `win-game-capture.${tag}.node`,
});
}
function expectedNativeRuntimeArtifacts(platform, arch) {
if (platform === 'darwin' && arch === 'universal') {
return [
...expectedNativeRuntimeArtifactsForArch(platform, 'arm64'),
...expectedNativeRuntimeArtifactsForArch(platform, 'x64'),
];
}
return expectedNativeRuntimeArtifactsForArch(platform, arch);
}
function expectedNativeRuntimeArtifactsForArch(platform, arch) {
const tag = platformTag(platform, arch);
if (!tag) return [];
const artifacts = [];
artifacts.push({
packageName: '@fluxer/webauthn',
relativePath: `webauthn.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/hardware-encoder',
relativePath: `hardware-encoder.${tag}.node`,
});
if (platform === 'darwin') {
artifacts.push({
packageName: '@fluxer/mac-app-audio',
relativePath: `mac-app-audio.darwin-${arch}.node`,
});
artifacts.push({
packageName: '@fluxer/mac-screen-capture',
relativePath: `mac-screen-capture.darwin-${arch}.node`,
});
artifacts.push({
packageName: '@fluxer/mac-clipboard',
relativePath: `mac-clipboard.darwin-${arch}.node`,
});
artifacts.push({
packageName: '@fluxer/mac-sysctl',
relativePath: `mac-sysctl.darwin-${arch}.node`,
});
artifacts.push({
packageName: '@fluxer/mac-tcc',
relativePath: `mac-tcc.darwin-${arch}.node`,
});
artifacts.push({
packageName: '@fluxer/macos-input-hook',
relativePath: `macos-input-hook.darwin-${arch}.node`,
});
artifacts.push({
packageName: '@fluxer/platform-info',
relativePath: `platform-info.${tag}.node`,
});
} else if (platform === 'win32') {
artifacts.push({
packageName: '@fluxer/win-process-loopback',
relativePath: `win-process-loopback.${tag}.node`,
});
addWindowsGameCaptureArtifacts(artifacts, tag);
artifacts.push({
packageName: '@fluxer/win-clipboard',
relativePath: `win-clipboard.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/win-shell',
relativePath: `win-shell.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/win-toast',
relativePath: `win-toast.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/windows-input-hook',
relativePath: `windows-input-hook.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/platform-info',
relativePath: `platform-info.${tag}.node`,
});
} else if (platform === 'linux') {
artifacts.push({
packageName: '@fluxer/linux-audio-capture',
relativePath: `linux-audio-capture.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/linux-portals',
relativePath: `linux-portals.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/linux-screen-capture',
relativePath: `linux-screen-capture.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/linux-notifications',
relativePath: `linux-notifications.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/linux-evdev',
relativePath: `linux-evdev.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/system-hunspell',
relativePath: `system-hunspell.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/linux-input-hook',
relativePath: `linux-input-hook.${tag}.node`,
});
artifacts.push({
packageName: '@fluxer/platform-info',
relativePath: `platform-info.${tag}.node`,
});
}
return artifacts;
}
function isLinuxSharedLibraryArtifact(fileName) {
return /\.so(?:\.|$)/.test(fileName);
}
async function expectedNativeRuntimeArtifactsForAppDir(platform, arch, appDir) {
const artifacts = expectedNativeRuntimeArtifacts(platform, arch);
if (platform !== 'linux') return artifacts;
const webAuthnRoot = path.join(appDir, 'node_modules', '@fluxer', 'webauthn');
const linuxWebAuthnRuntimeLibraries = new Set(['libfido2.so.1']);
try {
for (const entry of await fs.readdir(webAuthnRoot, {withFileTypes: true})) {
if (entry.isFile() && isLinuxSharedLibraryArtifact(entry.name)) {
linuxWebAuthnRuntimeLibraries.add(entry.name);
}
}
} catch (error) {
if (!error || error.code !== 'ENOENT') throw error;
}
for (const libraryName of [...linuxWebAuthnRuntimeLibraries].sort()) {
artifacts.push({
packageName: '@fluxer/webauthn',
relativePath: libraryName,
});
}
return artifacts;
}
function packagePathParts(packageName) {
const parts = packageName.split('/');
if (parts.length === 2 && parts[0].startsWith('@')) {
return parts;
}
return [packageName];
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.length > 0;
}
function resolveAppDir(context) {
const candidates = [
context.appDir,
context.packager?.info?.appDir,
context.packager?.appDir,
context.packager?.projectDir,
context.packager?.info?.projectDir,
process.cwd(),
];
const appDir = candidates.find(isNonEmptyString);
if (!appDir) {
throw new Error('Unable to resolve electron-builder app directory for native artifact verification.');
}
return appDir;
}
async function fileExists(filePath) {
return fs
.stat(filePath)
.then((stat) => stat.isFile())
.catch((error) => {
if (error && error.code === 'ENOENT') {
return false;
}
throw error;
});
}
function darwinMachOArchFromLabel(label) {
if (label.includes('darwin-arm64')) return 'arm64';
if (label.includes('darwin-x64')) return 'x86_64';
return null;
}
function expectedDarwinMachOArch(arch) {
if (arch === 'x64') return 'x86_64';
if (arch === 'arm64') return 'arm64';
return null;
}
async function darwinMachOArchitectures(filePath) {
const {stdout} = await execFileAsync('lipo', ['-archs', filePath]);
return stdout.trim().split(/\s+/).filter(Boolean);
}
async function darwinMachOFileTypes(filePath) {
const {stdout} = await execFileAsync('otool', ['-hv', filePath]);
const knownFileTypes = new Set(['OBJECT', 'EXECUTE', 'FVMLIB', 'CORE', 'PRELOAD', 'DYLIB', 'DYLINKER', 'BUNDLE']);
return stdout
.split(/\r?\n/)
.flatMap((line) => line.trim().split(/\s+/))
.filter((token) => knownFileTypes.has(token));
}
async function darwinMachOLoadCommands(filePath) {
const {stdout} = await execFileAsync('otool', ['-l', filePath]);
return stdout
.split(/\r?\n/)
.map((line) => line.trim().match(/^cmd\s+(\S+)$/)?.[1])
.filter(Boolean);
}
async function verifyDarwinNativeArchitectures(platform, arch, entries, stage) {
if (platform !== 'darwin') return;
const isUniversal = arch === 'universal';
const expectedArch = expectedDarwinMachOArch(arch);
if (!expectedArch && !isUniversal) return;
const mismatches = [];
for (const entry of entries) {
if (!(await fileExists(entry.path))) continue;
const entryExpectedArch = isUniversal ? darwinMachOArchFromLabel(entry.label) : expectedArch;
if (!entryExpectedArch) continue;
const archs = await darwinMachOArchitectures(entry.path);
const fileTypes = await darwinMachOFileTypes(entry.path);
if (!archs.includes(entryExpectedArch)) {
mismatches.push(`${entry.label}: has ${archs.join(', ') || '<none>'}; expected ${entryExpectedArch}`);
}
if (entryExpectedArch === 'x86_64' && archs.includes('x86_64h') && !archs.includes('x86_64')) {
mismatches.push(`${entry.label}: has x86_64h only; expected baseline x86_64 for Intel compatibility`);
}
if (fileTypes.length === 0 || fileTypes.some((fileType) => fileType !== 'BUNDLE' && fileType !== 'DYLIB')) {
mismatches.push(
`${entry.label}: Mach-O file type ${fileTypes.join(', ') || '<none>'}; expected DYLIB or BUNDLE for Node addon loading`,
);
}
if (fileTypes.includes('BUNDLE')) {
const loadCommands = await darwinMachOLoadCommands(entry.path);
if (loadCommands.includes('LC_ID_DYLIB')) {
mismatches.push(
`${entry.label}: Mach-O BUNDLE contains LC_ID_DYLIB; dyld rejects this combination when Electron loads the addon`,
);
}
}
}
if (mismatches.length > 0) {
throw new Error(
[
`Wrong native runtime architecture(s) ${stage} for ${platform}/${arch}:`,
...mismatches.map((entry) => ` - ${entry}`),
].join('\n'),
);
}
}
async function verifyNativePackageInputs(context) {
if (process.env.FLUXER_SKIP_NATIVE === 'true') return;
const platform = context.electronPlatformName;
const arch = normalizeArch(context.arch);
const appDir = resolveAppDir(context);
const missing = [];
const entries = [];
for (const artifact of await expectedNativeRuntimeArtifactsForAppDir(platform, arch, appDir)) {
const artifactPath = path.join(
appDir,
'node_modules',
...packagePathParts(artifact.packageName),
artifact.relativePath,
);
entries.push({
label: `${artifact.packageName}/${artifact.relativePath}`,
path: artifactPath,
});
if (!(await fileExists(artifactPath))) {
missing.push(`${artifact.packageName}/${artifact.relativePath}`);
}
}
if (missing.length > 0) {
throw new Error(
[
`Missing native runtime artifact(s) before packaging for ${platform}/${arch}:`,
...missing.map((entry) => ` - ${entry}`),
'Run `pnpm build` from fluxer_desktop so native build outputs are synced into node_modules before electron-builder runs.',
].join('\n'),
);
}
await verifyDarwinNativeArchitectures(platform, arch, entries, 'before packaging');
}
async function verifyPackagedNativeArtifacts(context) {
const platform = context.electronPlatformName;
const arch = normalizeArch(context.arch);
const appDir = resolveAppDir(context);
const missing = [];
const entries = [];
for (const artifact of await expectedNativeRuntimeArtifactsForAppDir(platform, arch, appDir)) {
const artifactPath = path.join(
context.appOutDir,
'resources',
'app.asar.unpacked',
'node_modules',
...packagePathParts(artifact.packageName),
artifact.relativePath,
);
entries.push({
label: `${artifact.packageName}/${artifact.relativePath}`,
path: artifactPath,
});
if (!(await fileExists(artifactPath))) {
missing.push(`${artifact.packageName}/${artifact.relativePath}`);
}
}
if (missing.length > 0) {
throw new Error(
[
`Missing unpacked native runtime artifact(s) after packaging for ${platform}/${arch}:`,
...missing.map((entry) => ` - ${entry}`),
'Check electron-builder asar.unpack patterns and native package artifact sync.',
].join('\n'),
);
}
await verifyDarwinNativeArchitectures(platform, arch, entries, 'after packaging');
}
async function copyMissingPackagedNativeArtifacts(context) {
const platform = context.electronPlatformName;
const arch = normalizeArch(context.arch);
const appDir = resolveAppDir(context);
for (const artifact of await expectedNativeRuntimeArtifactsForAppDir(platform, arch, appDir)) {
const packageParts = packagePathParts(artifact.packageName);
const sourcePath = path.join(appDir, 'node_modules', ...packageParts, artifact.relativePath);
const targetPath = path.join(
context.appOutDir,
'resources',
'app.asar.unpacked',
'node_modules',
...packageParts,
artifact.relativePath,
);
if ((await fileExists(targetPath)) || !(await fileExists(sourcePath))) {
continue;
}
await fs.mkdir(path.dirname(targetPath), {recursive: true});
await fs.copyFile(sourcePath, targetPath);
}
}
async function removeIfExists(targetPath) {
await fs.rm(targetPath, {force: true, recursive: true});
}
async function cleanupNativeBuildIntermediates(context) {
const unpackedNodeModules = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules');
const entries = await fs.readdir(unpackedNodeModules, {withFileTypes: true}).catch((error) => {
if (error && error.code === 'ENOENT') {
return [];
}
throw error;
});
const packageDirs = [];
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const entryPath = path.join(unpackedNodeModules, entry.name);
if (entry.name.startsWith('@')) {
const scopedEntries = await fs.readdir(entryPath, {withFileTypes: true});
for (const scopedEntry of scopedEntries) {
if (scopedEntry.isDirectory()) {
packageDirs.push(path.join(entryPath, scopedEntry.name));
}
}
} else {
packageDirs.push(entryPath);
}
}
await Promise.all(packageDirs.map((packageDir) => removeIfExists(path.join(packageDir, 'build', 'Release', 'obj'))));
}
async function addLinuxLegacyBinarySymlink(context) {
if (context.electronPlatformName !== 'linux') return;
const legacyName = packageName;
const currentName = linuxPackageName;
if (legacyName === currentName) return;
const linkPath = path.join(context.appOutDir, legacyName);
try {
await fs.symlink(currentName, linkPath);
} catch (error) {
if (!error || error.code !== 'EEXIST') throw error;
}
}
async function isElfFile(filePath) {
const handle = await fs.open(filePath, 'r');
try {
const magic = Buffer.alloc(4);
const {bytesRead} = await handle.read(magic, 0, magic.length, 0);
return bytesRead === magic.length && magic.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]));
} finally {
await handle.close();
}
}
async function findPackagedElfFiles(rootDir) {
const results = [];
async function visit(directory) {
const entries = await fs.readdir(directory, {withFileTypes: true});
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await visit(entryPath);
} else if (entry.isFile() && (await isElfFile(entryPath))) {
results.push(entryPath);
}
}
}
await visit(rootDir);
return results.sort();
}
function compareGlibcVersions(left, right) {
for (const key of ['major', 'minor', 'patch']) {
if (left[key] !== right[key]) return left[key] - right[key];
}
return 0;
}
async function inspectElfGlibcRequirements(elfPath) {
let stdout;
try {
({stdout} = await execFileAsync('readelf', ['--version-info', '--dynamic', '--wide', elfPath], {
env: {...process.env, LC_ALL: 'C'},
maxBuffer: 16 * 1024 * 1024,
}));
} catch (error) {
if (error && error.code === 'ENOENT') {
throw new Error(`Cannot verify Linux glibc compatibility: readelf executable is not available.`);
}
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
throw new Error(`Cannot inspect ELF file ${elfPath}: ${stderr || error?.message || String(error)}`);
}
const versions = new Map();
const unsupportedRequirements = new Set();
const hasGlibcRequirement = /\bGLIBC_[A-Za-z0-9_.]+\b/.test(stdout);
let readingVersionNeeds = false;
let foundVersionNeeds = false;
let hasVersionNeedsTag = false;
let needsGlibc = false;
let usesDtRelr = false;
for (const line of stdout.split(/\r?\n/)) {
const trimmed = line.trim();
if (/\(VERNEED\)/.test(trimmed)) {
hasVersionNeedsTag = true;
}
if (/^0x0*(?:23|24|25)\s+\(/i.test(trimmed) || /\((?:RELR|RELRSZ|RELRENT)\)/.test(trimmed)) {
usesDtRelr = true;
}
if (/\(NEEDED\)/.test(trimmed)) {
const neededMatch = /\(NEEDED\)\s+Shared library: \[([^\]]+)\]/.exec(trimmed);
if (!neededMatch) {
throw new Error(`Cannot parse a required library for ELF file ${elfPath}: ${trimmed}`);
}
if (neededMatch[1] === 'libc.so.6') needsGlibc = true;
}
if (trimmed.startsWith('Version needs section ')) {
readingVersionNeeds = true;
foundVersionNeeds = true;
continue;
}
if (trimmed.startsWith('Version symbols section ') || trimmed.startsWith('Version definition section ')) {
readingVersionNeeds = false;
continue;
}
if (!readingVersionNeeds) continue;
const requirement = /\bName:\s+(\S+)/.exec(trimmed)?.[1];
if (!requirement?.startsWith('GLIBC_')) continue;
const match = /^GLIBC_(\d+)\.(\d+)(?:\.(\d+))?$/.exec(requirement);
if (!match) {
unsupportedRequirements.add(requirement);
continue;
}
const version = {
major: Number.parseInt(match[1], 10),
minor: Number.parseInt(match[2], 10),
patch: Number.parseInt(match[3] ?? '0', 10),
name: requirement,
};
if (![version.major, version.minor, version.patch].every(Number.isSafeInteger)) {
throw new Error(`readelf returned an invalid glibc version for ${elfPath}: ${requirement}`);
}
versions.set(version.name, version);
}
if ((hasGlibcRequirement || hasVersionNeedsTag) && !foundVersionNeeds) {
throw new Error(`Cannot verify glibc requirements for ELF file ${elfPath}: readelf omitted version needs.`);
}
if (needsGlibc && (!foundVersionNeeds || (versions.size === 0 && unsupportedRequirements.size === 0))) {
throw new Error(`Cannot verify glibc requirements for ELF file ${elfPath}: readelf returned no version needs.`);
}
return {
versions: Array.from(versions.values()),
unsupportedRequirements: Array.from(unsupportedRequirements).sort(),
usesDtRelr,
};
}
async function findLinuxGlibcCompatibilityViolations(elfFiles) {
const violations = [];
for (const elfPath of elfFiles) {
const {versions, unsupportedRequirements, usesDtRelr} = await inspectElfGlibcRequirements(elfPath);
const maximum = versions.sort(compareGlibcVersions).at(-1);
const requirements = [...unsupportedRequirements];
if (maximum && compareGlibcVersions(maximum, linuxGlibcBaseline) > 0) {
requirements.push(maximum.name);
}
if (usesDtRelr) requirements.push('DT_RELR (glibc 2.36+)');
if (requirements.length > 0) violations.push({elfPath, requirements});
}
return violations;
}
function throwLinuxGlibcCompatibilityError(violations, formatPath) {
if (violations.length === 0) return;
const lines = [
`Linux package exceeds the supported ${linuxGlibcBaseline.name} ABI baseline.`,
'Build Linux artifacts on Ubuntu 22.04 and keep every shipped ELF at or below that glibc requirement.',
];
for (const {elfPath, requirements} of violations) {
lines.push(` - ${formatPath(elfPath)} requires ${requirements.join(', ')}`);
}
throw new Error(lines.join('\n'));
}
function linuxDistributableTargetNames(context) {
if (!Array.isArray(context.targets)) {
throw new Error('Cannot verify Linux glibc compatibility: electron-builder did not provide a target list.');
}
return context.targets.map((target) => target.name).filter((name) => name !== 'dir');
}
async function verifyLinuxGlibcCompatibility(context) {
if (context.electronPlatformName !== 'linux') return;
const distributableTargets = linuxDistributableTargetNames(context);
if (distributableTargets.length === 0) {
console.log(
`Skipped the ${linuxGlibcBaseline.name} ABI baseline check: this pack produces no distributable Linux artifact.`,
);
return;
}
const elfFiles = await findPackagedElfFiles(context.appOutDir);
if (elfFiles.length === 0) {
throw new Error(`Linux package output contains no ELF files: ${context.appOutDir}`);
}
const violations = await findLinuxGlibcCompatibilityViolations(elfFiles);
throwLinuxGlibcCompatibilityError(violations, (elfPath) => path.relative(context.appOutDir, elfPath));
console.log(
`Verified ${elfFiles.length} packaged Linux ELF files against the ${linuxGlibcBaseline.name} ABI baseline.`,
);
}
async function afterPack(context) {
await copyMissingPackagedNativeArtifacts(context);
await cleanupNativeBuildIntermediates(context);
await addLinuxLegacyBinarySymlink(context);
await verifyPackagedNativeArtifacts(context);
await verifyLinuxGlibcCompatibility(context);
}
async function listRpmPackageFiles(artifactPath) {
try {
const {stdout} = await execFileAsync('rpm', ['-qpl', artifactPath], {
maxBuffer: 16 * 1024 * 1024,
});
return stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
} catch (error) {
if (error && error.code === 'ENOENT') {
throw new Error(`Cannot inspect RPM artifact ${artifactPath}: rpm executable is not available.`);
}
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
throw new Error(`Cannot inspect RPM artifact ${artifactPath}: ${stderr || error?.message || String(error)}`);
}
}
async function listDebPackageFiles(artifactPath) {
try {
const {stdout} = await execFileAsync('dpkg-deb', ['--contents', artifactPath], {
maxBuffer: 16 * 1024 * 1024,
});
return stdout
.split(/\r?\n/)
.map((line) => line.match(/^\S+\s+\S+\s+\d+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+(.+)$/)?.[1])
.filter(Boolean)
.map((filePath) => filePath.replace(/^\.\//, '/'));
} catch (error) {
if (error && error.code === 'ENOENT') {
throw new Error(`Cannot inspect DEB artifact ${artifactPath}: dpkg-deb executable is not available.`);
}
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
throw new Error(`Cannot inspect DEB artifact ${artifactPath}: ${stderr || error?.message || String(error)}`);
}
}
async function verifyRpmArtifactsDoNotOwnBuildIds(buildResult) {
const rpmArtifacts = (buildResult.artifactPaths ?? []).filter(
(artifactPath) => path.extname(artifactPath) === '.rpm',
);
const violations = [];
for (const artifactPath of rpmArtifacts) {
const packageFiles = await listRpmPackageFiles(artifactPath);
const buildIdFiles = packageFiles.filter(
(filePath) => filePath === rpmBuildIdFilePrefix || filePath.startsWith(`${rpmBuildIdFilePrefix}/`),
);
if (buildIdFiles.length > 0) {
violations.push({artifactPath, buildIdFiles});
}
}
if (violations.length === 0) return [];
const lines = [
`RPM artifact(s) must not own ${rpmBuildIdFilePrefix} entries.`,
'These global rpmbuild-generated links collide with other Electron RPMs that bundle the same upstream ELF binaries.',
];
for (const {artifactPath, buildIdFiles} of violations) {
lines.push(` - ${path.basename(artifactPath)}:`);
for (const filePath of buildIdFiles.slice(0, 12)) {
lines.push(` ${filePath}`);
}
if (buildIdFiles.length > 12) {
lines.push(` ... ${buildIdFiles.length - 12} more`);
}
}
throw new Error(lines.join('\n'));
}
function packageFilesContainAppArmorProfile(packageFiles) {
return packageFiles.some((filePath) => filePath.endsWith('/resources/apparmor-profile'));
}
async function verifyLinuxPackagesContainAppArmorProfile(buildResult) {
const packageArtifacts = (buildResult.artifactPaths ?? []).filter((artifactPath) =>
['.deb', '.rpm'].includes(path.extname(artifactPath)),
);
const violations = [];
for (const artifactPath of packageArtifacts) {
const extension = path.extname(artifactPath);
const packageFiles =
extension === '.deb' ? await listDebPackageFiles(artifactPath) : await listRpmPackageFiles(artifactPath);
if (!packageFilesContainAppArmorProfile(packageFiles)) {
violations.push({artifactPath});
}
}
if (violations.length === 0) return;
const lines = [
'Linux package artifact(s) must include the Electron AppArmor profile.',
'Ubuntu 24.04+ restricts unprivileged user namespaces; packaged Electron apps need this profile so the Chromium sandbox can run without forcing --no-sandbox.',
];
for (const {artifactPath} of violations) {
lines.push(` - ${path.basename(artifactPath)}`);
}
throw new Error(lines.join('\n'));
}
async function readElfNeededLibraries(artifactPath) {
try {
const {stdout} = await execFileAsync('readelf', ['-d', artifactPath], {
env: {...process.env, LC_ALL: 'C'},
maxBuffer: 8 * 1024 * 1024,
});
const neededLibraries = [];
for (const line of stdout.split(/\r?\n/)) {
if (!/\(NEEDED\)/.test(line)) continue;
const match = /\(NEEDED\).*\[([^\]]+)\]/.exec(line);
if (!match) {
throw new Error(`Cannot parse a required library for AppImage artifact ${artifactPath}: ${line.trim()}`);
}
neededLibraries.push(match[1]);
}
return neededLibraries;
} catch (error) {
if (error && error.code === 'ENOENT') {
throw new Error(`Cannot inspect AppImage artifact ${artifactPath}: readelf executable is not available.`);
}
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
throw new Error(`Cannot inspect AppImage artifact ${artifactPath}: ${stderr || error?.message || String(error)}`);
}
}
async function verifyAppImageArtifactsGlibcCompatibility(buildResult) {
const appImageArtifacts = (buildResult.artifactPaths ?? []).filter(
(artifactPath) => path.extname(artifactPath) === '.AppImage',
);
const violations = await findLinuxGlibcCompatibilityViolations(appImageArtifacts);
throwLinuxGlibcCompatibilityError(violations, (artifactPath) => path.basename(artifactPath));
}
async function verifyAppImageArtifactsDoNotNeedFuse2(buildResult) {
const appImageArtifacts = (buildResult.artifactPaths ?? []).filter(
(artifactPath) => path.extname(artifactPath) === '.AppImage',
);
const violations = [];
for (const artifactPath of appImageArtifacts) {
const neededLibraries = await readElfNeededLibraries(artifactPath);
if (neededLibraries.includes('libfuse.so.2')) {
violations.push({artifactPath, neededLibraries});
}
}
if (violations.length === 0) return;
const lines = [
'AppImage artifact(s) must not depend on libfuse.so.2.',
'Use electron-builder toolsets.appimage 1.0.3 so the AppImage runtime is static and works on modern distributions without libfuse2.',
];
for (const {artifactPath, neededLibraries} of violations) {
lines.push(` - ${path.basename(artifactPath)} imports: ${neededLibraries.join(', ') || '<none>'}`);
}
throw new Error(lines.join('\n'));
}
function canExecuteAppImageArtifact(artifactPath) {
const artifactName = path.basename(artifactPath).toLowerCase();
const hostArch = process.arch;
if (artifactName.includes('arm64') || artifactName.includes('aarch64')) {
return hostArch === 'arm64';
}
if (artifactName.includes('x86_64') || artifactName.includes('amd64') || artifactName.includes('x64')) {
return hostArch === 'x64';
}
return true;
}
async function extractAppImagePattern(artifactPath, pattern, tempDir) {
try {
await execFileAsync(path.resolve(artifactPath), ['--appimage-extract', pattern], {
cwd: tempDir,
maxBuffer: 8 * 1024 * 1024,
});
} catch (error) {
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
throw new Error(
`Cannot extract ${pattern} from AppImage artifact ${artifactPath}: ${stderr || error?.message || String(error)}`,
);
}
}
async function findExtractedFiles(rootDir, predicate) {
const results = [];
async function visit(directory) {
const entries = await fs.readdir(directory, {withFileTypes: true});
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await visit(entryPath);
} else if (entry.isFile() && predicate(entryPath)) {
results.push(entryPath);
}
}
}
await visit(rootDir);
return results;
}
async function inspectAppImageLauncher(artifactPath) {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fluxer-appimage-'));
try {
await extractAppImagePattern(artifactPath, 'AppRun', tempDir);
await extractAppImagePattern(artifactPath, '*.desktop', tempDir);
const squashfsRoot = path.join(tempDir, 'squashfs-root');
const appRun = await fs.readFile(path.join(squashfsRoot, 'AppRun'), 'utf8');
const desktopFiles = await findExtractedFiles(squashfsRoot, (filePath) => path.extname(filePath) === '.desktop');
const violations = [];
if (desktopFiles.length === 0) {
violations.push('does not contain a desktop entry');
}
const appRunUsesNamespaceProbe = /unshare\s+(?:-Ur|--user)\s+true/.test(appRun);
const appRunFallsBackToNoSandbox = /NO_SANDBOX=\(?--no-sandbox\)?/.test(appRun);
if (!appRunUsesNamespaceProbe || !appRunFallsBackToNoSandbox) {
violations.push('AppRun does not use the expected user-namespace probe before falling back to --no-sandbox');
}
for (const desktopFile of desktopFiles) {
const desktopEntry = await fs.readFile(desktopFile, 'utf8');
for (const line of desktopEntry.split(/\r?\n/)) {
if (line.startsWith('Exec=') && line.includes('--no-sandbox')) {
violations.push(`${path.basename(desktopFile)} has an unconditional --no-sandbox Exec line`);
}
}
}
return violations;
} finally {
await fs.rm(tempDir, {recursive: true, force: true});
}
}
async function verifyAppImageArtifactsUseSandboxAwareLauncher(buildResult) {
const appImageArtifacts = (buildResult.artifactPaths ?? []).filter(
(artifactPath) => path.extname(artifactPath) === '.AppImage',
);
const violations = [];
for (const artifactPath of appImageArtifacts) {
if (!canExecuteAppImageArtifact(artifactPath)) {
console.warn(
`Skipping AppImage launcher extraction for ${path.basename(artifactPath)} because it does not match host architecture ${process.arch}.`,
);
continue;
}
const artifactViolations = await inspectAppImageLauncher(artifactPath);
if (artifactViolations.length > 0) {
violations.push({artifactPath, artifactViolations});
}
}
if (violations.length === 0) return;
const lines = [
'AppImage artifact(s) must use the static-runtime, sandbox-aware launcher contract.',
'The desktop entry must not pass --no-sandbox unconditionally; AppRun may add it only after the user-namespace probe fails.',
];
for (const {artifactPath, artifactViolations} of violations) {
lines.push(` - ${path.basename(artifactPath)}:`);
for (const violation of artifactViolations) {
lines.push(` ${violation}`);
}
}
throw new Error(lines.join('\n'));
}
async function verifyLinuxArtifactContracts(buildResult) {
await verifyRpmArtifactsDoNotOwnBuildIds(buildResult);
await verifyLinuxPackagesContainAppArmorProfile(buildResult);
await verifyAppImageArtifactsGlibcCompatibility(buildResult);
await verifyAppImageArtifactsDoNotNeedFuse2(buildResult);
await verifyAppImageArtifactsUseSandboxAwareLauncher(buildResult);
}
module.exports = {
appId,
productName,
copyright: 'Copyright © 2026 Fluxer Platform AB',
artifactName: `${artifactProductName}-\${version}-\${os}-\${arch}.\${ext}`,
directories: {
buildResources: 'build_resources',
output: 'dist-electron',
},
files: [
'dist/**/*',
'package.json',
...nativeRuntimeFilePatterns,
'node_modules/hunspell-asm/**/*',
...nativeBuildArtifactExcludes,
...packagedRuntimeArtifactExcludes,
...bundledDependencyExcludes,
...platformNativeRuntimeExcludes,
...platformRuntimeDependencyExcludes,
],
extraMetadata: {
main: 'dist/main/index.js',
name: metadataName,
...(process.env.VERSION ? {version: process.env.VERSION} : {}),
...(targetPlatform === 'linux' ? {desktopName: `${linuxPackageName}.desktop`} : {}),
},
extraResources: [
{
from: `build_resources/${iconDir}/`,
to: 'icons',
filter: [
'16x16.png',
'24x24.png',
'32x32.png',
'48x48.png',
'64x64.png',
'128x128.png',
'256x256.png',
'512x512.png',
'FluxerTrayTemplate.png',
'FluxerTrayTemplate@2x.png',
'icon.ico',
'icon.png',
],
},
{
from: `build_resources/${iconDir}/badges/`,
to: 'badges',
filter: ['**/*'],
},
],
asar: {
smartUnpack: false,
unpack: [
'**/*.node',
'node_modules/@fluxer/win-process-loopback/*.node',
...winGameCaptureTargetArchs.map(
(arch) => `node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/@fluxer/win-clipboard/*.node',
'node_modules/@fluxer/win-shell/*.node',
'node_modules/@fluxer/win-toast/*.node',
'node_modules/@fluxer/linux-audio-capture/*.node',
'node_modules/@fluxer/linux-portals/*.node',
'node_modules/@fluxer/linux-screen-capture/*.node',
'node_modules/@fluxer/linux-screen-capture/obs-vkcapture/**/*',
'node_modules/@fluxer/linux-notifications/*.node',
'node_modules/@fluxer/linux-evdev/*.node',
'node_modules/@fluxer/system-hunspell/*.node',
'node_modules/@fluxer/macos-input-hook/*.node',
'node_modules/@fluxer/mac-app-audio/*.node',
'node_modules/@fluxer/mac-screen-capture/*.node',
'node_modules/@fluxer/mac-clipboard/*.node',
'node_modules/@fluxer/mac-sysctl/*.node',
'node_modules/@fluxer/mac-tcc/*.node',
'node_modules/@fluxer/windows-input-hook/*.node',
'node_modules/@fluxer/linux-input-hook/*.node',
'node_modules/@fluxer/platform-info/*.node',
'node_modules/@fluxer/webauthn/*.node',
'node_modules/@fluxer/webauthn/*.so*',
'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node',
...winGameCaptureTargetArchs.map(
(arch) =>
`node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/.pnpm/@fluxer+win-clipboard@*/node_modules/@fluxer/win-clipboard/*.node',
'node_modules/.pnpm/@fluxer+win-shell@*/node_modules/@fluxer/win-shell/*.node',
'node_modules/.pnpm/@fluxer+win-toast@*/node_modules/@fluxer/win-toast/*.node',
'node_modules/.pnpm/@fluxer+windows-input-hook@*/node_modules/@fluxer/windows-input-hook/*.node',
'node_modules/.pnpm/@fluxer+linux-audio-capture@*/node_modules/@fluxer/linux-audio-capture/*.node',
'node_modules/.pnpm/@fluxer+linux-portals@*/node_modules/@fluxer/linux-portals/*.node',
'node_modules/.pnpm/@fluxer+linux-screen-capture@*/node_modules/@fluxer/linux-screen-capture/*.node',
'node_modules/.pnpm/@fluxer+linux-screen-capture@*/node_modules/@fluxer/linux-screen-capture/obs-vkcapture/**/*',
'node_modules/.pnpm/@fluxer+linux-notifications@*/node_modules/@fluxer/linux-notifications/*.node',
'node_modules/.pnpm/@fluxer+linux-evdev@*/node_modules/@fluxer/linux-evdev/*.node',
'node_modules/.pnpm/@fluxer+linux-input-hook@*/node_modules/@fluxer/linux-input-hook/*.node',
'node_modules/.pnpm/@fluxer+system-hunspell@*/node_modules/@fluxer/system-hunspell/*.node',
'node_modules/.pnpm/@fluxer+macos-input-hook@*/node_modules/@fluxer/macos-input-hook/*.node',
'node_modules/.pnpm/@fluxer+mac-app-audio@*/node_modules/@fluxer/mac-app-audio/*.node',
'node_modules/.pnpm/@fluxer+mac-screen-capture@*/node_modules/@fluxer/mac-screen-capture/*.node',
'node_modules/.pnpm/@fluxer+mac-clipboard@*/node_modules/@fluxer/mac-clipboard/*.node',
'node_modules/.pnpm/@fluxer+mac-sysctl@*/node_modules/@fluxer/mac-sysctl/*.node',
'node_modules/.pnpm/@fluxer+mac-tcc@*/node_modules/@fluxer/mac-tcc/*.node',
'node_modules/.pnpm/@fluxer+platform-info@*/node_modules/@fluxer/platform-info/*.node',
'node_modules/.pnpm/@fluxer+webauthn@*/node_modules/@fluxer/webauthn/*.node',
'node_modules/.pnpm/@fluxer+webauthn@*/node_modules/@fluxer/webauthn/*.so*',
],
},
compression: 'normal',
nativeModules: {
npmRebuild: false,
},
protocols: [
{
name: appId,
role: 'Viewer',
schemes: ['fluxer'],
},
],
beforePack: verifyNativePackageInputs,
afterPack,
afterAllArtifactBuild: verifyLinuxArtifactContracts,
toolsets: {
appimage: '1.0.3',
},
mac: {
category: 'public.app-category.social-networking',
universal: {
x64ArchFiles: '**/@fluxer/**/*.node',
},
minimumSystemVersion: macOSMinimumSystemVersion,
icon: `build_resources/${iconDir}/_compiled/AppIcon.icns`,
darkModeSupport: true,
notarize: true,
sign: {
hardenedRuntime: true,
provisioningProfile,
entitlements: isCanary
? 'build_resources/entitlements.mac.canary.plist'
: 'build_resources/entitlements.mac.stable.plist',
entitlementsInherit: 'build_resources/entitlements.mac.inherit.plist',
},
target: [
{
target: 'dmg',
arch: macTargetArchs,
},
{
target: 'zip',
arch: macTargetArchs,
},
],
extendInfo: {
NSMicrophoneUsageDescription: 'Fluxer needs access to your microphone to enable voice chat features.',
NSCameraUsageDescription: 'Fluxer needs access to your camera to enable video chat features.',
NSAppleEventsUsageDescription: 'Fluxer needs access to Apple Events for automation features.',
NSAudioCaptureUsageDescription: 'Fluxer captures audio from the screen or window you choose to share.',
NSScreenCaptureUsageDescription: 'Fluxer captures the screen or window you choose to share.',
},
},
dmg: {
contents: [
{
x: 130,
y: 220,
},
{
x: 410,
y: 220,
type: 'link',
path: '/Applications',
},
],
},
win: {
icon: `build_resources/${iconDir}/icon.ico`,
target: winTargets,
},
portable: {
artifactName: `${artifactProductName}-\${version}-portable-\${os}-\${arch}.\${ext}`,
},
linux: {
icon: `build_resources/${iconDir}/1024x1024.png`,
category: 'Network;InstantMessaging;Chat;',
target: [
{
target: 'AppImage',
arch: targetArchs,
},
{
target: 'deb',
arch: targetArchs,
},
{
target: 'rpm',
arch: targetArchs,
},
{
target: 'tar.gz',
arch: targetArchs,
},
],
desktop: {
entry: linuxDesktopEntry,
},
},
deb: {
packageCategory: 'net',
desktop: {
entry: linuxDesktopEntryWithActions,
desktopActions: linuxDesktopActions,
},
depends: [
'libgtk-3-0',
'libnotify4',
'libnss3',
'libxss1',
'libxtst6',
'xdg-utils',
'libatspi2.0-0',
'libuuid1',
'libsecret-1-0',
'libpulse0',
'libpipewire-0.3-0',
'libstdc++6',
'libgcc-s1',
],
},
rpm: {
desktop: {
entry: linuxDesktopEntryWithActions,
desktopActions: linuxDesktopActions,
},
fpm: rpmBuildIdLinkFpmArgs,
depends: [
'gtk3',
'libnotify',
'nss',
'libXScrnSaver',
'libXtst',
'xdg-utils',
'at-spi2-core',
'libuuid',
'libsecret',
'pulseaudio-libs',
'pipewire-libs',
'libstdc++',
'libgcc',
],
},
publish: null,
};