495 lines
12 KiB
JavaScript
495 lines
12 KiB
JavaScript
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const ARCH_LABELS = {
|
|
0: 'ia32',
|
|
1: 'x64',
|
|
2: 'armv7l',
|
|
3: 'arm64',
|
|
4: 'universal',
|
|
};
|
|
|
|
const TARGET_BINARY_FORMAT = {
|
|
darwin: 'mach-o',
|
|
linux: 'elf',
|
|
win32: 'pe',
|
|
};
|
|
|
|
function getArchLabel(arch) {
|
|
return ARCH_LABELS[arch] ?? String(arch);
|
|
}
|
|
|
|
async function walkFiles(rootDir) {
|
|
const files = [];
|
|
const queue = [rootDir];
|
|
|
|
while (queue.length > 0) {
|
|
const currentDir = queue.pop();
|
|
if (!currentDir) {
|
|
continue;
|
|
}
|
|
|
|
let entries;
|
|
try {
|
|
entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const absolutePath = path.join(currentDir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
queue.push(absolutePath);
|
|
continue;
|
|
}
|
|
if (entry.isFile()) {
|
|
files.push(absolutePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
async function findNodePtyRoots(appOutDir) {
|
|
const roots = [];
|
|
const queue = [appOutDir];
|
|
|
|
while (queue.length > 0) {
|
|
const currentDir = queue.pop();
|
|
if (!currentDir) {
|
|
continue;
|
|
}
|
|
|
|
const baseName = path.basename(currentDir);
|
|
if (baseName === 'node-pty') {
|
|
roots.push(currentDir);
|
|
continue;
|
|
}
|
|
|
|
let entries;
|
|
try {
|
|
entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
queue.push(path.join(currentDir, entry.name));
|
|
}
|
|
}
|
|
|
|
return roots;
|
|
}
|
|
|
|
function shouldKeepNodePtyPrebuild(entryName, platform, archLabel) {
|
|
if (!entryName.startsWith(`${platform}-`)) {
|
|
return false;
|
|
}
|
|
|
|
if (platform === 'darwin' && archLabel === 'universal') {
|
|
return (
|
|
entryName === 'darwin-universal' ||
|
|
entryName === 'darwin-arm64' ||
|
|
entryName === 'darwin-x64'
|
|
);
|
|
}
|
|
|
|
return (
|
|
entryName === `${platform}-${archLabel}` ||
|
|
(platform === 'darwin' && entryName === 'darwin-universal')
|
|
);
|
|
}
|
|
|
|
function shouldKeepNodePtyBin(entryName, platform, archLabel) {
|
|
if (!entryName.startsWith(`${platform}-`)) {
|
|
return false;
|
|
}
|
|
|
|
if (platform === 'darwin' && archLabel === 'universal') {
|
|
return (
|
|
entryName.startsWith('darwin-universal-') ||
|
|
entryName.startsWith('darwin-arm64-') ||
|
|
entryName.startsWith('darwin-x64-')
|
|
);
|
|
}
|
|
|
|
return (
|
|
entryName.startsWith(`${platform}-${archLabel}-`) ||
|
|
(platform === 'darwin' && entryName.startsWith('darwin-universal-'))
|
|
);
|
|
}
|
|
|
|
async function pruneNodePtyArtifacts(appOutDir, platform, archLabel) {
|
|
const removedPaths = [];
|
|
const nodePtyRoots = await findNodePtyRoots(appOutDir);
|
|
|
|
for (const nodePtyRoot of nodePtyRoots) {
|
|
for (const [subdirName, shouldKeep] of [
|
|
['prebuilds', shouldKeepNodePtyPrebuild],
|
|
['bin', shouldKeepNodePtyBin],
|
|
]) {
|
|
const subdir = path.join(nodePtyRoot, subdirName);
|
|
let entries;
|
|
try {
|
|
entries = await fs.promises.readdir(subdir, { withFileTypes: true });
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
|
|
if (shouldKeep(entry.name, platform, archLabel)) {
|
|
continue;
|
|
}
|
|
|
|
const absolutePath = path.join(subdir, entry.name);
|
|
await fs.promises.rm(absolutePath, { recursive: true, force: true });
|
|
removedPaths.push(absolutePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return removedPaths;
|
|
}
|
|
|
|
function findNodeModulesSequence(segments, sequence) {
|
|
for (let index = 0; index <= segments.length - sequence.length; index += 1) {
|
|
let matches = true;
|
|
for (let offset = 0; offset < sequence.length; offset += 1) {
|
|
if (segments[index + offset] !== sequence[offset]) {
|
|
matches = false;
|
|
break;
|
|
}
|
|
}
|
|
if (matches) {
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function getKnownPrunableNativeArtifactRoot(appOutDir, filePath, targetPlatform, targetArch) {
|
|
if (targetPlatform !== 'win32') {
|
|
return null;
|
|
}
|
|
|
|
const relativePath = path.relative(appOutDir, filePath);
|
|
const segments = relativePath.split(path.sep);
|
|
|
|
const conptyIndex = findNodeModulesSequence(segments, [
|
|
'node_modules',
|
|
'node-pty',
|
|
'third_party',
|
|
'conpty',
|
|
]);
|
|
const conptyArchIndex = conptyIndex + 5;
|
|
const conptyArchDir = conptyIndex >= 0 ? segments[conptyArchIndex] : null;
|
|
if (conptyArchDir?.startsWith('win10-') && conptyArchDir !== `win10-${targetArch}`) {
|
|
return path.join(appOutDir, ...segments.slice(0, conptyArchIndex + 1));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function isKnownAllowedNativeMismatch(relativePath, format, archs, targetPlatform) {
|
|
const normalizedPath = relativePath.split(path.sep).join('/');
|
|
const ssh2PageantPath = 'node_modules/ssh2/util/pagent.exe';
|
|
|
|
return (
|
|
targetPlatform === 'win32' &&
|
|
(normalizedPath === ssh2PageantPath || normalizedPath.endsWith(`/${ssh2PageantPath}`)) &&
|
|
format === 'pe' &&
|
|
archs.size === 1 &&
|
|
archs.has('ia32')
|
|
);
|
|
}
|
|
|
|
async function pruneKnownIncompatibleNativeArtifacts(appOutDir, targetPlatform, targetArch) {
|
|
const files = await walkFiles(appOutDir);
|
|
const rootsToRemove = new Set();
|
|
|
|
for (const filePath of files) {
|
|
const rootToRemove = getKnownPrunableNativeArtifactRoot(
|
|
appOutDir,
|
|
filePath,
|
|
targetPlatform,
|
|
targetArch
|
|
);
|
|
if (!rootToRemove) {
|
|
continue;
|
|
}
|
|
|
|
const metadata = await detectBinaryMetadata(filePath);
|
|
if (!metadata) {
|
|
continue;
|
|
}
|
|
|
|
if (isBinaryCompatible(metadata.format, metadata.archs, targetPlatform, targetArch)) {
|
|
continue;
|
|
}
|
|
|
|
rootsToRemove.add(rootToRemove);
|
|
}
|
|
|
|
const removedPaths = [];
|
|
for (const absolutePath of rootsToRemove) {
|
|
await fs.promises.rm(absolutePath, { recursive: true, force: true });
|
|
removedPaths.push(absolutePath);
|
|
}
|
|
return removedPaths;
|
|
}
|
|
|
|
function mapMachOCpuType(cpuType) {
|
|
switch (cpuType >>> 0) {
|
|
case 0x00000007:
|
|
return 'ia32';
|
|
case 0x01000007:
|
|
return 'x64';
|
|
case 0x0000000c:
|
|
return 'armv7l';
|
|
case 0x0100000c:
|
|
return 'arm64';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseMachO(buffer) {
|
|
if (buffer.length < 8) {
|
|
return null;
|
|
}
|
|
|
|
const magicHex = buffer.subarray(0, 4).toString('hex');
|
|
const archs = new Set();
|
|
|
|
if (magicHex === 'cafebabe' || magicHex === 'cafebabf') {
|
|
if (buffer.length < 8) {
|
|
return null;
|
|
}
|
|
|
|
const archCount = buffer.readUInt32BE(4);
|
|
const stride = magicHex === 'cafebabf' ? 32 : 20;
|
|
let offset = 8;
|
|
|
|
for (let index = 0; index < archCount; index += 1) {
|
|
if (buffer.length < offset + stride) {
|
|
break;
|
|
}
|
|
const arch = mapMachOCpuType(buffer.readUInt32BE(offset));
|
|
if (arch) {
|
|
archs.add(arch);
|
|
}
|
|
offset += stride;
|
|
}
|
|
|
|
return archs.size > 0 ? { format: 'mach-o', archs } : null;
|
|
}
|
|
|
|
if (magicHex === 'bebafeca' || magicHex === 'bfbafeca') {
|
|
if (buffer.length < 8) {
|
|
return null;
|
|
}
|
|
|
|
const archCount = buffer.readUInt32LE(4);
|
|
const stride = magicHex === 'bfbafeca' ? 32 : 20;
|
|
let offset = 8;
|
|
|
|
for (let index = 0; index < archCount; index += 1) {
|
|
if (buffer.length < offset + stride) {
|
|
break;
|
|
}
|
|
const arch = mapMachOCpuType(buffer.readUInt32LE(offset));
|
|
if (arch) {
|
|
archs.add(arch);
|
|
}
|
|
offset += stride;
|
|
}
|
|
|
|
return archs.size > 0 ? { format: 'mach-o', archs } : null;
|
|
}
|
|
|
|
if (magicHex === 'feedfacf' || magicHex === 'feedface') {
|
|
const arch = mapMachOCpuType(buffer.readUInt32BE(4));
|
|
return arch ? { format: 'mach-o', archs: new Set([arch]) } : null;
|
|
}
|
|
|
|
if (magicHex === 'cffaedfe' || magicHex === 'cefaedfe') {
|
|
const arch = mapMachOCpuType(buffer.readUInt32LE(4));
|
|
return arch ? { format: 'mach-o', archs: new Set([arch]) } : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function parseElf(buffer) {
|
|
if (buffer.length < 20) {
|
|
return null;
|
|
}
|
|
if (
|
|
buffer[0] !== 0x7f ||
|
|
buffer[1] !== 0x45 ||
|
|
buffer[2] !== 0x4c ||
|
|
buffer[3] !== 0x46
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const littleEndian = buffer[5] !== 2;
|
|
const machine = littleEndian ? buffer.readUInt16LE(18) : buffer.readUInt16BE(18);
|
|
const arch =
|
|
machine === 0x03
|
|
? 'ia32'
|
|
: machine === 0x3e
|
|
? 'x64'
|
|
: machine === 0x28
|
|
? 'armv7l'
|
|
: machine === 0xb7
|
|
? 'arm64'
|
|
: null;
|
|
|
|
return arch ? { format: 'elf', archs: new Set([arch]) } : null;
|
|
}
|
|
|
|
function parsePortableExecutable(buffer) {
|
|
if (buffer.length < 0x40) {
|
|
return null;
|
|
}
|
|
if (buffer[0] !== 0x4d || buffer[1] !== 0x5a) {
|
|
return null;
|
|
}
|
|
|
|
const peOffset = buffer.readUInt32LE(0x3c);
|
|
if (buffer.length < peOffset + 6) {
|
|
return null;
|
|
}
|
|
if (
|
|
buffer[peOffset] !== 0x50 ||
|
|
buffer[peOffset + 1] !== 0x45 ||
|
|
buffer[peOffset + 2] !== 0x00 ||
|
|
buffer[peOffset + 3] !== 0x00
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const machine = buffer.readUInt16LE(peOffset + 4);
|
|
const arch =
|
|
machine === 0x014c
|
|
? 'ia32'
|
|
: machine === 0x8664
|
|
? 'x64'
|
|
: machine === 0xaa64
|
|
? 'arm64'
|
|
: machine === 0x01c4
|
|
? 'armv7l'
|
|
: null;
|
|
|
|
return arch ? { format: 'pe', archs: new Set([arch]) } : null;
|
|
}
|
|
|
|
async function detectBinaryMetadata(filePath) {
|
|
const handle = await fs.promises.open(filePath, 'r');
|
|
try {
|
|
const buffer = Buffer.alloc(4096);
|
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
const slice = buffer.subarray(0, bytesRead);
|
|
return parseMachO(slice) ?? parseElf(slice) ?? parsePortableExecutable(slice);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
}
|
|
|
|
function isBinaryCompatible(format, archs, targetPlatform, targetArch) {
|
|
if (format !== TARGET_BINARY_FORMAT[targetPlatform]) {
|
|
return false;
|
|
}
|
|
|
|
if (targetPlatform === 'darwin' && targetArch === 'universal') {
|
|
return archs.has('arm64') || archs.has('x64');
|
|
}
|
|
|
|
return archs.has(targetArch);
|
|
}
|
|
|
|
async function validateNativeBinaries(appOutDir, targetPlatform, targetArch) {
|
|
const mismatches = [];
|
|
const files = await walkFiles(appOutDir);
|
|
|
|
for (const filePath of files) {
|
|
const relativePath = path.relative(appOutDir, filePath);
|
|
const metadata = await detectBinaryMetadata(filePath);
|
|
if (!metadata) {
|
|
continue;
|
|
}
|
|
|
|
if (isBinaryCompatible(metadata.format, metadata.archs, targetPlatform, targetArch)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
isKnownAllowedNativeMismatch(relativePath, metadata.format, metadata.archs, targetPlatform)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
mismatches.push({
|
|
path: relativePath,
|
|
format: metadata.format,
|
|
archs: [...metadata.archs].sort(),
|
|
});
|
|
}
|
|
|
|
return mismatches;
|
|
}
|
|
|
|
async function afterPack(context) {
|
|
const targetPlatform = context.electronPlatformName;
|
|
const targetArch = getArchLabel(context.arch);
|
|
|
|
const removedPaths = [
|
|
...(await pruneNodePtyArtifacts(context.appOutDir, targetPlatform, targetArch)),
|
|
...(await pruneKnownIncompatibleNativeArtifacts(
|
|
context.appOutDir,
|
|
targetPlatform,
|
|
targetArch
|
|
)),
|
|
];
|
|
const mismatches = await validateNativeBinaries(context.appOutDir, targetPlatform, targetArch);
|
|
|
|
if (mismatches.length > 0) {
|
|
const details = mismatches
|
|
.slice(0, 20)
|
|
.map((mismatch) => `- ${mismatch.path} [${mismatch.format}] -> ${mismatch.archs.join(', ')}`)
|
|
.join('\n');
|
|
throw new Error(
|
|
`Found incompatible native binaries in ${targetPlatform}-${targetArch} bundle after pruning.\n${details}`
|
|
);
|
|
}
|
|
|
|
if (removedPaths.length > 0) {
|
|
console.log(
|
|
`[afterPack] pruned ${removedPaths.length} incompatible native artifact(s) for ${targetPlatform}-${targetArch}`
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = afterPack;
|
|
module.exports._internal = {
|
|
detectBinaryMetadata,
|
|
getArchLabel,
|
|
isBinaryCompatible,
|
|
isKnownAllowedNativeMismatch,
|
|
parseElf,
|
|
parseMachO,
|
|
parsePortableExecutable,
|
|
pruneKnownIncompatibleNativeArtifacts,
|
|
pruneNodePtyArtifacts,
|
|
validateNativeBinaries,
|
|
walkFiles,
|
|
};
|