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

@@ -2,6 +2,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
@@ -9,12 +10,33 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
const require = createRequire(import.meta.url);
const outputDir = path.join(root, 'build', 'opencode-ai');
const rootPackageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const targetArgs = process.argv.slice(2);
for (const arg of targetArgs) {
if (!/^--(?:platform|arch)=\S+$/.test(arg)) {
throw new Error(`Unknown bundle target option: ${arg}`);
}
}
function targetOption(name, fallback) {
const prefix = `--${name}=`;
const option = targetArgs.find((arg) => arg.startsWith(prefix));
return option ? option.slice(prefix.length) : fallback;
}
const targetPlatform = targetOption('platform', process.platform);
const targetArch = targetOption('arch', process.arch);
function fail(message) {
console.error(`[bundle-opencode] ${message}`);
process.exit(1);
}
if (targetPlatform !== process.platform) {
fail(`Cross-OS OpenCode staging is not supported: host ${process.platform}, target ${targetPlatform}.`);
}
function resolvePackageRoot(packageName) {
try {
return path.dirname(require.resolve(`${packageName}/package.json`, {
@@ -32,6 +54,13 @@ if (!packageRoot) {
const packageJsonPath = path.join(packageRoot, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const declaredVersion = rootPackageJson.devDependencies?.['opencode-ai']
?? rootPackageJson.dependencies?.['opencode-ai'];
if (declaredVersion !== packageJson.version) {
fail(
`The opencode-ai dependency must be pinned exactly to the bundled version. Declared ${declaredVersion ?? 'missing'}, installed ${packageJson.version ?? 'unknown'}.`,
);
}
const packageRequire = createRequire(packageJsonPath);
const bin = packageJson.bin;
const hasOpencodeBin = typeof bin === 'string'
@@ -53,11 +82,6 @@ fs.cpSync(packageRoot, outputDir, {
},
});
const runtimeDependencies = {
...(packageJson.dependencies && typeof packageJson.dependencies === 'object' ? packageJson.dependencies : {}),
...(packageJson.optionalDependencies && typeof packageJson.optionalDependencies === 'object' ? packageJson.optionalDependencies : {}),
};
const platformMap = {
darwin: 'darwin',
linux: 'linux',
@@ -69,30 +93,9 @@ const archMap = {
arm: 'arm',
};
function copyRuntimeDependency(packageName) {
let dependencyPackageJsonPath;
try {
dependencyPackageJsonPath = packageRequire.resolve(`${packageName}/package.json`);
} catch {
console.warn(`[bundle-opencode] optional dependency not installed: ${packageName}`);
return false;
}
const dependencyRoot = path.dirname(dependencyPackageJsonPath);
const outputPackageDir = path.join(outputDir, 'node_modules', ...packageName.split('/'));
fs.rmSync(outputPackageDir, { recursive: true, force: true });
fs.mkdirSync(path.dirname(outputPackageDir), { recursive: true });
fs.cpSync(dependencyRoot, outputPackageDir, {
recursive: true,
dereference: true,
filter: (src) => !src.includes(`${path.sep}.git${path.sep}`),
});
return true;
}
function getNativePackageCandidates() {
const platform = platformMap[process.platform] ?? process.platform;
const arch = archMap[process.arch] ?? process.arch;
const platform = platformMap[targetPlatform] ?? targetPlatform;
const arch = archMap[targetArch] ?? targetArch;
const base = `opencode-${platform}-${arch}`;
if (arch === 'x64') {
@@ -103,7 +106,7 @@ function getNativePackageCandidates() {
}
function bundleNativeLauncher() {
const binaryName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
const binaryName = targetPlatform === 'win32' ? 'opencode.exe' : 'opencode';
const outputBinary = path.join(outputDir, 'bin', binaryName);
for (const packageName of getNativePackageCandidates()) {
@@ -126,12 +129,35 @@ function bundleNativeLauncher() {
}
fail(
`Missing native opencode binary for ${process.platform}/${process.arch}. Tried ${getNativePackageCandidates().join(', ')}.`,
`Missing native opencode binary for ${targetPlatform}/${targetArch}. Tried ${getNativePackageCandidates().join(', ')}.`,
);
}
const copiedDependencies = Object.keys(runtimeDependencies).filter(copyRuntimeDependency);
const nativeLauncher = bundleNativeLauncher();
const verificationEnv = {
...process.env,
PATH: process.platform === 'win32'
? [process.env.SystemRoot && path.join(process.env.SystemRoot, 'System32'), process.env.SystemRoot]
.filter(Boolean)
.join(path.delimiter)
: '/usr/bin:/bin',
};
delete verificationEnv.NODE_PATH;
delete verificationEnv.BUN_INSTALL;
const versionProbe = spawnSync(nativeLauncher.outputBinary, ['--version'], {
cwd: outputDir,
encoding: 'utf8',
env: verificationEnv,
});
if (versionProbe.error || versionProbe.status !== 0) {
fail(
`Bundled native launcher failed its self-contained version probe: ${versionProbe.error?.message ?? versionProbe.stderr ?? versionProbe.stdout}`,
);
}
const reportedVersion = `${versionProbe.stdout ?? ''}${versionProbe.stderr ?? ''}`.trim();
if (reportedVersion !== packageJson.version) {
fail(`Bundled native launcher reported ${reportedVersion || 'no version'}, expected ${packageJson.version}.`);
}
console.log(
`[bundle-opencode] bundled opencode-ai@${packageJson.version ?? 'unknown'} with ${copiedDependencies.length} runtime dependency package(s) and native launcher ${nativeLauncher.packageName} -> ${path.relative(root, outputDir)}`,
`[bundle-opencode] bundled and verified self-contained opencode-ai@${packageJson.version} launcher ${nativeLauncher.packageName} -> ${path.relative(root, outputDir)}`,
);