fix(desktop): keep Linux builds compatible with glibc 2.35 (#2041)

This commit is contained in:
Hampus
2026-08-27 13:41:08 +02:00
committed by GitHub
parent a800430997
commit 13344096b7
2 changed files with 181 additions and 10 deletions
+175 -4
View File
@@ -14,6 +14,7 @@ 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',
@@ -814,11 +815,165 @@ async function addLinuxLegacyBinarySymlink(context) {
}
}
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'));
}
async function verifyLinuxGlibcCompatibility(context) {
if (context.electronPlatformName !== 'linux') 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) {
@@ -922,12 +1077,19 @@ async function verifyLinuxPackagesContainAppArmorProfile(buildResult) {
async function readElfNeededLibraries(artifactPath) {
try {
const {stdout} = await execFileAsync('readelf', ['-d', artifactPath], {
env: {...process.env, LC_ALL: 'C'},
maxBuffer: 8 * 1024 * 1024,
});
return stdout
.split(/\r?\n/)
.map((line) => line.match(/\(NEEDED\)\s+Shared library: \[([^\]]+)\]/)?.[1])
.filter(Boolean);
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.`);
@@ -937,6 +1099,14 @@ async function readElfNeededLibraries(artifactPath) {
}
}
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',
@@ -1072,6 +1242,7 @@ async function verifyAppImageArtifactsUseSandboxAwareLauncher(buildResult) {
async function verifyLinuxArtifactContracts(buildResult) {
await verifyRpmArtifactsDoNotOwnBuildIds(buildResult);
await verifyLinuxPackagesContainAppArmorProfile(buildResult);
await verifyAppImageArtifactsGlibcCompatibility(buildResult);
await verifyAppImageArtifactsDoNotNeedFuse2(buildResult);
await verifyAppImageArtifactsUseSandboxAwareLauncher(buildResult);
}
+6 -6
View File
@@ -143,14 +143,14 @@ const PLATFORMS: &[Platform] = &[
platform: "linux",
arch: "x64",
desktop_variant: DEFAULT_DESKTOP_VARIANT,
os: "ubuntu-24.04",
os: "ubuntu-22.04",
electron_arch: "x64",
},
Platform {
platform: "linux",
arch: "arm64",
desktop_variant: DEFAULT_DESKTOP_VARIANT,
os: "ubuntu-24.04-arm",
os: "ubuntu-22.04-arm",
electron_arch: "arm64",
},
];
@@ -3588,8 +3588,8 @@ mod tests {
selected,
vec![
"{\"platform\":\"windows\",\"arch\":\"arm64\",\"desktop_variant\":\"default\",\"os\":\"windows-2025\",\"electron_arch\":\"arm64\"}",
"{\"platform\":\"linux\",\"arch\":\"x64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-24.04\",\"electron_arch\":\"x64\"}",
"{\"platform\":\"linux\",\"arch\":\"arm64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-24.04-arm\",\"electron_arch\":\"arm64\"}",
"{\"platform\":\"linux\",\"arch\":\"x64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-22.04\",\"electron_arch\":\"x64\"}",
"{\"platform\":\"linux\",\"arch\":\"arm64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-22.04-arm\",\"electron_arch\":\"arm64\"}",
]
);
}
@@ -3628,8 +3628,8 @@ mod tests {
selected,
vec![
"{\"platform\":\"windows\",\"arch\":\"arm64\",\"desktop_variant\":\"default\",\"os\":\"windows-2025\",\"electron_arch\":\"arm64\"}",
"{\"platform\":\"linux\",\"arch\":\"x64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-24.04\",\"electron_arch\":\"x64\"}",
"{\"platform\":\"linux\",\"arch\":\"arm64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-24.04-arm\",\"electron_arch\":\"arm64\"}",
"{\"platform\":\"linux\",\"arch\":\"x64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-22.04\",\"electron_arch\":\"x64\"}",
"{\"platform\":\"linux\",\"arch\":\"arm64\",\"desktop_variant\":\"default\",\"os\":\"ubuntu-22.04-arm\",\"electron_arch\":\"arm64\"}",
]
);
}