mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
fix(desktop): keep Linux builds compatible with glibc 2.35 (#2041)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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\"}",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user