fix: 修复 OpenCode 运行时启动与本地打包

This commit is contained in:
2026-08-09 00:02:49 +08:00
parent d092132d86
commit a6fe14a8d6
20 changed files with 1847 additions and 154 deletions

View File

@@ -3,8 +3,11 @@
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
closeSync,
createReadStream,
existsSync,
openSync,
readSync,
readFileSync,
statSync,
writeFileSync,
@@ -81,6 +84,37 @@ function assertEqual(actual, expected, label) {
}
}
function assertPeMachine(filePath, expectedMachine, label) {
const handle = openSync(filePath, 'r');
try {
const dosHeader = Buffer.alloc(64);
if (readSync(handle, dosHeader, 0, dosHeader.length, 0) !== dosHeader.length) {
throw new Error(`${label}: truncated DOS header`);
}
if (dosHeader.toString('ascii', 0, 2) !== 'MZ') {
throw new Error(`${label}: missing MZ signature`);
}
const peOffset = dosHeader.readUInt32LE(0x3c);
const coffHeader = Buffer.alloc(6);
if (readSync(handle, coffHeader, 0, coffHeader.length, peOffset) !== coffHeader.length) {
throw new Error(`${label}: truncated PE header`);
}
if (coffHeader.readUInt32LE(0) !== 0x00004550) {
throw new Error(`${label}: missing PE signature`);
}
const machine = coffHeader.readUInt16LE(4);
if (machine !== expectedMachine) {
throw new Error(
`${label}: expected PE machine 0x${expectedMachine.toString(16)}, got 0x${machine.toString(16)}`,
);
}
} finally {
closeSync(handle);
}
}
function isPathInside(parentPath, candidatePath) {
const relativePath = relative(resolve(parentPath), resolve(candidatePath));
return relativePath !== ''
@@ -106,12 +140,78 @@ const expectedNode = option('--expected-node', '24.15.0');
const expectedCommit = option('--expected-commit', null);
const expectedBuildId = option('--expected-build-id', null);
const manifestPath = option('--manifest', null);
const appAsarPath = join(dirname(appExecutable), 'resources', 'app.asar');
const appAsarUnpackedPath = join(dirname(appExecutable), 'resources', 'app.asar.unpacked');
for (const filePath of [appExecutable, installerPath]) {
const resourcesDir = join(dirname(appExecutable), 'resources');
const appAsarPath = join(resourcesDir, 'app.asar');
const appAsarUnpackedPath = join(resourcesDir, 'app.asar.unpacked');
const opencodeRuntimeDir = join(resourcesDir, 'opencode-ai');
const opencodePackagePath = join(opencodeRuntimeDir, 'package.json');
const opencodeExecutable = join(opencodeRuntimeDir, 'bin', 'opencode.exe');
const pythonRuntimeDir = join(resourcesDir, 'python');
const pythonExecutable = join(pythonRuntimeDir, 'python.exe');
const toolsBinDir = join(resourcesDir, 'bin');
const uvExecutable = join(toolsBinDir, 'uv.exe');
for (const filePath of [
appExecutable,
installerPath,
appAsarPath,
opencodePackagePath,
opencodeExecutable,
pythonExecutable,
uvExecutable,
]) {
if (!existsSync(filePath)) throw new Error(`Missing artifact: ${filePath}`);
}
const PE_MACHINE_AMD64 = 0x8664;
for (const [label, executable] of [
['Makelore executable', appExecutable],
['OpenCode executable', opencodeExecutable],
['Python executable', pythonExecutable],
['uv executable', uvExecutable],
]) {
assertPeMachine(executable, PE_MACHINE_AMD64, label);
}
const declaredOpencodeVersion = packageJson.devDependencies?.['opencode-ai']
?? packageJson.dependencies?.['opencode-ai'];
const packagedOpencode = JSON.parse(readFileSync(opencodePackagePath, 'utf8'));
assertEqual(packagedOpencode.version, declaredOpencodeVersion, 'packaged OpenCode version');
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? 'C:\\Windows';
const installLocalProbeEnv = {
...process.env,
PATH: [join(systemRoot, 'System32'), systemRoot].join(';'),
};
for (const key of ['NODE_PATH', 'BUN_INSTALL', 'OPENCODE_BIN', 'OPENCODE_PATH', 'NIANCODE_PYTHON_PATH', 'NIANCODE_UV_PATH']) {
delete installLocalProbeEnv[key];
}
const opencodeVersion = run(opencodeExecutable, ['--version'], {
cwd: opencodeRuntimeDir,
env: installLocalProbeEnv,
});
assertEqual(opencodeVersion, declaredOpencodeVersion, 'OpenCode executable version');
const uvVersion = run(uvExecutable, ['--version'], {
cwd: toolsBinDir,
env: installLocalProbeEnv,
});
if (!/^uv\s+\d+\.\d+\.\d+/.test(uvVersion)) {
throw new Error(`Invalid bundled uv version output: ${uvVersion}`);
}
const pythonProbe = JSON.parse(run(pythonExecutable, [
'-I',
'-c',
'import json,pip,sqlite3,ssl,sys; print(json.dumps({"executable":sys.executable,"pip":pip.__file__,"sqlite3":sqlite3.__file__,"ssl":ssl.__file__}))',
], {
cwd: pythonRuntimeDir,
env: installLocalProbeEnv,
}));
assertEqual(resolve(pythonProbe.executable), resolve(pythonExecutable), 'Python executable path');
for (const [moduleName, modulePath] of Object.entries(pythonProbe).filter(([name]) => name !== 'executable')) {
if (typeof modulePath !== 'string' || !isPathInside(pythonRuntimeDir, modulePath)) {
throw new Error(`Python ${moduleName} was not loaded from the installation: ${modulePath}`);
}
}
const verificationHead = run('git', ['rev-parse', 'HEAD']);
const gitStatus = run('git', ['status', '--porcelain']);
if (gitStatus && !args.includes('--allow-dirty')) {
@@ -127,6 +227,8 @@ const appRequire = createRequire(join(dirname(process.execPath), 'resources', 'a
const packagedPackage = appRequire('./package.json');
const msgpackResolved = appRequire.resolve('msgpackr');
const canvasResolved = appRequire.resolve('@napi-rs/canvas');
const playwrightPackageResolved = appRequire.resolve('@playwright/mcp/package.json');
const playwrightCliResolved = join(dirname(playwrightPackageResolved), 'cli.js');
const { pack, unpack } = appRequire('msgpackr');
const { createCanvas } = appRequire('@napi-rs/canvas');
const packedValue = unpack(pack({ ok: true, text: '\u5362\u6b22' }));
@@ -164,6 +266,8 @@ console.log('NIANCODE_ARTIFACT_PROBE=' + JSON.stringify({
},
msgpackResolved,
canvasResolved,
playwrightPackageResolved,
playwrightCliResolved,
nativeModules,
packedValue,
canvas: [canvas.width, canvas.height],
@@ -207,6 +311,16 @@ if (!isPathInside(appAsarPath, probe.msgpackResolved)) {
if (!isPathInside(appAsarPath, probe.canvasResolved)) {
throw new Error(`@napi-rs/canvas was not resolved from app.asar: ${probe.canvasResolved}`);
}
if (!isPathInside(appAsarPath, probe.playwrightPackageResolved)) {
throw new Error(`@playwright/mcp was not resolved from app.asar: ${probe.playwrightPackageResolved}`);
}
if (!isPathInside(appAsarPath, probe.playwrightCliResolved)) {
throw new Error(`@playwright/mcp CLI was not resolved from app.asar: ${probe.playwrightCliResolved}`);
}
run(appExecutable, [probe.playwrightCliResolved, '--help'], {
cwd: dirname(appExecutable),
env: { ...installLocalProbeEnv, ELECTRON_RUN_AS_NODE: '1' },
});
const msgpackNative = probe.nativeModules.find(({ modulePath, physicalPath, exists }) => (
exists
&& isPathInside(appAsarPath, modulePath)
@@ -245,6 +359,19 @@ const evidence = {
installerBytes,
installerSha256,
runtime: { electron: probe.electron, node: probe.node },
installLocalRuntimes: {
opencode: {
executable: opencodeExecutable,
version: opencodeVersion,
sha256: await sha256(opencodeExecutable),
},
playwrightMcp: {
packageJson: probe.playwrightPackageResolved,
cli: probe.playwrightCliResolved,
},
python: pythonProbe,
uv: { executable: uvExecutable, version: uvVersion },
},
nativeModules: {
msgpackr: probe.msgpackResolved,
canvas: probe.canvasResolved,