fix(windows): harden packaging and codex smoke
This commit is contained in:
parent
836e0355db
commit
ddfa4cc59d
4 changed files with 281 additions and 10 deletions
|
|
@ -159,6 +159,93 @@ async function pruneNodePtyArtifacts(appOutDir, platform, archLabel) {
|
||||||
return removedPaths;
|
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) {
|
function mapMachOCpuType(cpuType) {
|
||||||
switch (cpuType >>> 0) {
|
switch (cpuType >>> 0) {
|
||||||
case 0x00000007:
|
case 0x00000007:
|
||||||
|
|
@ -335,6 +422,7 @@ async function validateNativeBinaries(appOutDir, targetPlatform, targetArch) {
|
||||||
const files = await walkFiles(appOutDir);
|
const files = await walkFiles(appOutDir);
|
||||||
|
|
||||||
for (const filePath of files) {
|
for (const filePath of files) {
|
||||||
|
const relativePath = path.relative(appOutDir, filePath);
|
||||||
const metadata = await detectBinaryMetadata(filePath);
|
const metadata = await detectBinaryMetadata(filePath);
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -344,8 +432,14 @@ async function validateNativeBinaries(appOutDir, targetPlatform, targetArch) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isKnownAllowedNativeMismatch(relativePath, metadata.format, metadata.archs, targetPlatform)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
mismatches.push({
|
mismatches.push({
|
||||||
path: path.relative(appOutDir, filePath),
|
path: relativePath,
|
||||||
format: metadata.format,
|
format: metadata.format,
|
||||||
archs: [...metadata.archs].sort(),
|
archs: [...metadata.archs].sort(),
|
||||||
});
|
});
|
||||||
|
|
@ -358,7 +452,14 @@ async function afterPack(context) {
|
||||||
const targetPlatform = context.electronPlatformName;
|
const targetPlatform = context.electronPlatformName;
|
||||||
const targetArch = getArchLabel(context.arch);
|
const targetArch = getArchLabel(context.arch);
|
||||||
|
|
||||||
const removedPaths = await pruneNodePtyArtifacts(context.appOutDir, targetPlatform, targetArch);
|
const removedPaths = [
|
||||||
|
...(await pruneNodePtyArtifacts(context.appOutDir, targetPlatform, targetArch)),
|
||||||
|
...(await pruneKnownIncompatibleNativeArtifacts(
|
||||||
|
context.appOutDir,
|
||||||
|
targetPlatform,
|
||||||
|
targetArch
|
||||||
|
)),
|
||||||
|
];
|
||||||
const mismatches = await validateNativeBinaries(context.appOutDir, targetPlatform, targetArch);
|
const mismatches = await validateNativeBinaries(context.appOutDir, targetPlatform, targetArch);
|
||||||
|
|
||||||
if (mismatches.length > 0) {
|
if (mismatches.length > 0) {
|
||||||
|
|
@ -383,9 +484,11 @@ module.exports._internal = {
|
||||||
detectBinaryMetadata,
|
detectBinaryMetadata,
|
||||||
getArchLabel,
|
getArchLabel,
|
||||||
isBinaryCompatible,
|
isBinaryCompatible,
|
||||||
|
isKnownAllowedNativeMismatch,
|
||||||
parseElf,
|
parseElf,
|
||||||
parseMachO,
|
parseMachO,
|
||||||
parsePortableExecutable,
|
parsePortableExecutable,
|
||||||
|
pruneKnownIncompatibleNativeArtifacts,
|
||||||
pruneNodePtyArtifacts,
|
pruneNodePtyArtifacts,
|
||||||
validateNativeBinaries,
|
validateNativeBinaries,
|
||||||
walkFiles,
|
walkFiles,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,18 @@ const afterPackModule = require('./afterPack.cjs');
|
||||||
|
|
||||||
const { validateNativeBinaries } = afterPackModule._internal;
|
const { validateNativeBinaries } = afterPackModule._internal;
|
||||||
|
|
||||||
|
function isAllowedPostPackMismatch(mismatch, platform, arch) {
|
||||||
|
const relativePath = mismatch.path.split(path.sep).join('/');
|
||||||
|
return (
|
||||||
|
platform === 'win32' &&
|
||||||
|
arch === 'x64' &&
|
||||||
|
relativePath === 'resources/elevate.exe' &&
|
||||||
|
mismatch.format === 'pe' &&
|
||||||
|
mismatch.archs.length === 1 &&
|
||||||
|
mismatch.archs[0] === 'ia32'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const [bundlePathArg, platform, arch] = process.argv.slice(2);
|
const [bundlePathArg, platform, arch] = process.argv.slice(2);
|
||||||
|
|
||||||
|
|
@ -14,16 +26,22 @@ async function main() {
|
||||||
|
|
||||||
const bundlePath = path.resolve(bundlePathArg);
|
const bundlePath = path.resolve(bundlePathArg);
|
||||||
const mismatches = await validateNativeBinaries(bundlePath, platform, arch);
|
const mismatches = await validateNativeBinaries(bundlePath, platform, arch);
|
||||||
|
const blockingMismatches = mismatches.filter(
|
||||||
|
(mismatch) => !isAllowedPostPackMismatch(mismatch, platform, arch)
|
||||||
|
);
|
||||||
|
|
||||||
if (mismatches.length === 0) {
|
if (blockingMismatches.length === 0) {
|
||||||
console.log(`[verifyBundle] OK ${platform}-${arch}: ${bundlePath}`);
|
const allowedCount = mismatches.length - blockingMismatches.length;
|
||||||
|
const suffix =
|
||||||
|
allowedCount > 0 ? ` (${allowedCount} allowed post-pack helper mismatch ignored)` : '';
|
||||||
|
console.log(`[verifyBundle] OK ${platform}-${arch}: ${bundlePath}${suffix}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
`[verifyBundle] Found ${mismatches.length} incompatible native binaries in ${platform}-${arch}: ${bundlePath}`
|
`[verifyBundle] Found ${blockingMismatches.length} incompatible native binaries in ${platform}-${arch}: ${bundlePath}`
|
||||||
);
|
);
|
||||||
for (const mismatch of mismatches.slice(0, 50)) {
|
for (const mismatch of blockingMismatches.slice(0, 50)) {
|
||||||
console.error(`- ${mismatch.path} [${mismatch.format}] -> ${mismatch.archs.join(', ')}`);
|
console.error(`- ${mismatch.path} [${mismatch.format}] -> ${mismatch.archs.join(', ')}`);
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
@ -326,12 +327,93 @@ function parseArgs(argv) {
|
||||||
return { all, jsonPath, list, selected };
|
return { all, jsonPath, list, selected };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasPathSeparator(value) {
|
||||||
|
return value.includes('/') || value.includes('\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWindowsSpawnBinary(binary) {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPathSeparator(binary)) {
|
||||||
|
if (!path.extname(binary) && existsSync(`${binary}.cmd`)) {
|
||||||
|
return `${binary}.cmd`;
|
||||||
|
}
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereResult = spawnSync('where.exe', [binary], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
if (whereResult.status !== 0 || !whereResult.stdout) {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = whereResult.stdout
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((candidate) => candidate.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const extensionlessShim = candidates.find(
|
||||||
|
(candidate) => !path.extname(candidate) && existsSync(`${candidate}.cmd`)
|
||||||
|
);
|
||||||
|
if (extensionlessShim) {
|
||||||
|
return `${extensionlessShim}.cmd`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
candidates.find((candidate) => /\.exe$/i.test(candidate)) ??
|
||||||
|
candidates.find((candidate) => /\.(?:cmd|bat)$/i.test(candidate)) ??
|
||||||
|
candidates[0] ??
|
||||||
|
binary
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteWindowsCmdArg(value) {
|
||||||
|
const text = String(value);
|
||||||
|
if (text.length === 0) {
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
if (!/[ \t\r\n"&|<>^()%!]/.test(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return `"${text.replace(/%/g, '%%').replace(/(["^&|<>])/g, '^$1')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSpawnInvocation(command) {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return {
|
||||||
|
bin: command.bin,
|
||||||
|
args: command.args,
|
||||||
|
options: { windowsHide: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedBin = resolveWindowsSpawnBinary(command.bin);
|
||||||
|
if (/\.(?:cmd|bat)$/i.test(resolvedBin)) {
|
||||||
|
const commandLine = [resolvedBin, ...command.args].map(quoteWindowsCmdArg).join(' ');
|
||||||
|
return {
|
||||||
|
bin: process.env.ComSpec || 'cmd.exe',
|
||||||
|
args: ['/d', '/s', '/c', commandLine],
|
||||||
|
options: { windowsHide: true, windowsVerbatimArguments: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bin: resolvedBin,
|
||||||
|
args: command.args,
|
||||||
|
options: { windowsHide: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function runCommand(command) {
|
function runCommand(command) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(command.bin, command.args, {
|
const spawnInvocation = buildSpawnInvocation(command);
|
||||||
|
const child = spawn(spawnInvocation.bin, spawnInvocation.args, {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
env: process.env,
|
env: process.env,
|
||||||
cwd: command.cwd,
|
cwd: command.cwd,
|
||||||
|
...spawnInvocation.options,
|
||||||
});
|
});
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ function createElfBuffer(arch: 'arm64' | 'x64'): Buffer {
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPortableExecutableBuffer(arch: 'arm64' | 'x64'): Buffer {
|
function createPortableExecutableBuffer(arch: 'arm64' | 'ia32' | 'x64'): Buffer {
|
||||||
const machine = arch === 'arm64' ? 0xaa64 : 0x8664;
|
const machine = arch === 'arm64' ? 0xaa64 : arch === 'ia32' ? 0x014c : 0x8664;
|
||||||
const buffer = Buffer.alloc(256);
|
const buffer = Buffer.alloc(256);
|
||||||
buffer[0] = 0x4d;
|
buffer[0] = 0x4d;
|
||||||
buffer[1] = 0x5a;
|
buffer[1] = 0x5a;
|
||||||
|
|
@ -224,4 +224,72 @@ describe('electron-builder afterPack', () => {
|
||||||
)
|
)
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts a clean x64 Windows bundle with optional standalone helper binaries', async () => {
|
||||||
|
const tempDir = createTempDir();
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
|
writeFile(path.join(tempDir, 'Agent Teams UI.exe'), createPortableExecutableBuffer('x64'));
|
||||||
|
writeFile(
|
||||||
|
path.join(
|
||||||
|
tempDir,
|
||||||
|
'resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
'node_modules',
|
||||||
|
'node-pty',
|
||||||
|
'build',
|
||||||
|
'Release',
|
||||||
|
'pty.node'
|
||||||
|
),
|
||||||
|
createPortableExecutableBuffer('x64')
|
||||||
|
);
|
||||||
|
const pageantPath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
'node_modules',
|
||||||
|
'ssh2',
|
||||||
|
'util',
|
||||||
|
'pagent.exe'
|
||||||
|
);
|
||||||
|
const armConptyDir = path.join(
|
||||||
|
tempDir,
|
||||||
|
'resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
'node_modules',
|
||||||
|
'node-pty',
|
||||||
|
'third_party',
|
||||||
|
'conpty',
|
||||||
|
'1.23.251008001',
|
||||||
|
'win10-arm64'
|
||||||
|
);
|
||||||
|
writeFile(pageantPath, createPortableExecutableBuffer('ia32'));
|
||||||
|
writeFile(path.join(armConptyDir, 'conpty.dll'), createPortableExecutableBuffer('arm64'));
|
||||||
|
writeFile(path.join(armConptyDir, 'OpenConsole.exe'), createPortableExecutableBuffer('arm64'));
|
||||||
|
|
||||||
|
await afterPackModule({
|
||||||
|
appOutDir: tempDir,
|
||||||
|
electronPlatformName: 'win32',
|
||||||
|
arch: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fs.existsSync(pageantPath)).toBe(true);
|
||||||
|
expect(fs.existsSync(armConptyDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still reports unrelated ia32 Windows binaries in an x64 bundle', async () => {
|
||||||
|
const tempDir = createTempDir();
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
|
const badBinaryPath = path.join(tempDir, 'resources', 'app.asar.unpacked', 'bad-helper.exe');
|
||||||
|
writeFile(badBinaryPath, createPortableExecutableBuffer('ia32'));
|
||||||
|
|
||||||
|
await expect(validateNativeBinaries(tempDir, 'win32', 'x64')).resolves.toEqual([
|
||||||
|
{
|
||||||
|
path: path.join('resources', 'app.asar.unpacked', 'bad-helper.exe'),
|
||||||
|
format: 'pe',
|
||||||
|
archs: ['ia32'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue