mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
perf(desktop): ship the bundled CLI version instead of spawning for it (#49762)
This commit is contained in:
@@ -202,7 +202,7 @@ for (const channel of ["dev", "beta"] as const) {
|
||||
{
|
||||
from: "resources/",
|
||||
to: "",
|
||||
filter: ["opencode-cli", "opencode-cli.exe"],
|
||||
filter: ["opencode-cli", "opencode-cli.exe", "opencode-cli.version"],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ const getBase = (appId: string): Configuration => ({
|
||||
{
|
||||
from: "resources/",
|
||||
to: "",
|
||||
filter: ["opencode-cli", "opencode-cli.exe"],
|
||||
filter: ["opencode-cli", "opencode-cli.exe", "opencode-cli.version"],
|
||||
},
|
||||
],
|
||||
afterPack: async (context) => {
|
||||
@@ -100,6 +100,8 @@ const getBase = (appId: string): Configuration => ({
|
||||
)
|
||||
const file = await stat(cli)
|
||||
if (!file.isFile() || file.size === 0) throw new Error(`Bundled CLI must be a non-empty file: ${cli}`)
|
||||
const version = path.join(path.dirname(cli), "opencode-cli.version")
|
||||
if ((await stat(version)).size === 0) throw new Error(`Bundled CLI version must be a non-empty file: ${version}`)
|
||||
},
|
||||
mac: {
|
||||
category: "public.app-category.developer-tools",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { $ } from "bun"
|
||||
import { chmod, copyFile, mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { dirname, join } from "node:path"
|
||||
|
||||
const CLI_VERSION = "dev"
|
||||
|
||||
@@ -75,10 +75,7 @@ export async function downloadCliToResources(version = CLI_VERSION, dest = windo
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-cli-"))
|
||||
try {
|
||||
await $`bun install --no-save --cwd ${directory} ${`${cli.package}@${version}`} ${`--os=${cli.os}`} ${`--cpu=${cli.cpu}`}`
|
||||
await copyCliToResources(
|
||||
join(directory, "node_modules", cli.package, "bin", cli.os === "win32" ? "opencode.exe" : "opencode"),
|
||||
dest,
|
||||
)
|
||||
await copyCliToResources(join(directory, "node_modules", cli.package), dest)
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
@@ -89,12 +86,23 @@ export async function downloadCliToResources(version = CLI_VERSION, dest = windo
|
||||
export async function copyBuiltCliToResources(root: string, dest = windowsify("resources/opencode-cli")) {
|
||||
const cli = getCurrentCli()
|
||||
const directory = cli.package.replace("@opencode/", "")
|
||||
await copyCliToResources(join(root, directory, "bin", cli.os === "win32" ? "opencode.exe" : "opencode"), dest)
|
||||
await copyCliToResources(join(root, directory), dest)
|
||||
}
|
||||
|
||||
async function copyCliToResources(source: string, dest: string) {
|
||||
await copyFile(source, dest)
|
||||
// The package directory is an npm package: its package.json version is the string the executable
|
||||
// prints for --version. Writing it next to the executable spares the desktop a ~400 ms spawn of the
|
||||
// 200 MB binary on first launch.
|
||||
async function copyCliToResources(pkg: string, dest: string) {
|
||||
const cli = getCurrentCli()
|
||||
await copyFile(join(pkg, "bin", cli.os === "win32" ? "opencode.exe" : "opencode"), dest)
|
||||
await prepareCli(dest)
|
||||
const manifest = (await Bun.file(join(pkg, "package.json")).json()) as { version?: string }
|
||||
if (!manifest.version) throw new Error(`Bundled CLI package has no version: ${pkg}`)
|
||||
await Bun.write(versionFile(dest), manifest.version)
|
||||
}
|
||||
|
||||
export function versionFile(cli: string) {
|
||||
return join(dirname(cli), "opencode-cli.version")
|
||||
}
|
||||
|
||||
async function prepareCli(dest: string) {
|
||||
|
||||
@@ -3,9 +3,11 @@ export * as DesktopCli from "./desktop-cli"
|
||||
import { execFile, spawn } from "node:child_process"
|
||||
import { promisify } from "node:util"
|
||||
import { app } from "electron"
|
||||
import { Context, Effect, FileSystem, Layer, Path } from "effect"
|
||||
import { Context, Effect, FileSystem, Layer, Option, Path } from "effect"
|
||||
import installer from "../../../../../install?raw"
|
||||
import { DesktopPaths } from "../paths"
|
||||
import { BUNDLED_CLI_VERSION_KEY } from "../storage/keys"
|
||||
import { getStore } from "../storage/store"
|
||||
import { parseCliVersion } from "./cli-version"
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
@@ -79,11 +81,46 @@ const resolveBundledCli = Effect.fn("DesktopCli.resolveBundled")(function* (isol
|
||||
? path.join(process.resourcesPath, executableName())
|
||||
: path.join(paths.developmentResourcesRoot, isolated ? developmentExecutableName() : executableName())
|
||||
yield* Effect.logInfo("v2 CLI executable resolved", { bundled, packaged: app.isPackaged })
|
||||
const version = parseCliVersion(yield* run(bundled, ["--version"]))
|
||||
const version = yield* bundledVersion(bundled)
|
||||
const binary = app.isPackaged || isolated ? yield* installCli(bundled, version) : bundled
|
||||
return { version, binary, command: [binary] }
|
||||
})
|
||||
|
||||
// Spawning the bundled executable for `--version` costs ~400 ms of startup on a 200 MB binary (and
|
||||
// several seconds on the first launch after an update, while the antivirus scans it). The build
|
||||
// writes the version next to the executable, so a packaged app never spawns; the per-identity cache
|
||||
// covers executables that arrived without that file.
|
||||
const bundledVersion = Effect.fn("DesktopCli.bundledVersion")(function* (bundled: string) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
const shipped = yield* fs
|
||||
.readFileString(path.join(path.dirname(bundled), "opencode-cli.version"))
|
||||
.pipe(Effect.map((text) => text.trim()), Effect.orElseSucceed(() => ""))
|
||||
if (shipped) {
|
||||
yield* Effect.logInfo("v2 CLI version bundled", { version: shipped })
|
||||
return shipped
|
||||
}
|
||||
const stat = yield* fs.stat(bundled).pipe(Effect.orElseSucceed(() => undefined))
|
||||
const identity = stat ? `${stat.size}:${Option.getOrUndefined(stat.mtime)?.getTime() ?? ""}` : undefined
|
||||
const store = getStore()
|
||||
const cached = store.get(BUNDLED_CLI_VERSION_KEY)
|
||||
if (identity && isVersionCache(cached) && cached.path === bundled && cached.identity === identity) {
|
||||
yield* Effect.logInfo("v2 CLI version reused", { version: cached.version })
|
||||
return cached.version
|
||||
}
|
||||
const version = parseCliVersion(yield* run(bundled, ["--version"]))
|
||||
if (identity) store.set(BUNDLED_CLI_VERSION_KEY, { path: bundled, identity, version } satisfies VersionCache)
|
||||
return version
|
||||
})
|
||||
|
||||
type VersionCache = { path: string; identity: string; version: string }
|
||||
|
||||
function isVersionCache(value: unknown): value is VersionCache {
|
||||
if (!value || typeof value !== "object") return false
|
||||
const cache = value as Record<string, unknown>
|
||||
return typeof cache.path === "string" && typeof cache.identity === "string" && typeof cache.version === "string"
|
||||
}
|
||||
|
||||
export const cleanStages = Effect.fn("DesktopCli.cleanStages")(function* (binary: string) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
|
||||
@@ -5,3 +5,4 @@ export const WSL_SERVERS_KEY = "wslServers"
|
||||
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
|
||||
export const BACKGROUND_COLOR_KEY = "backgroundColor"
|
||||
export const WINDOW_IDS_KEY = "windowIds"
|
||||
export const BUNDLED_CLI_VERSION_KEY = "bundledCliVersion"
|
||||
|
||||
Reference in New Issue
Block a user