mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-23 11:23:57 +00:00
feat(simulation): slim production bridge (#42584)
This commit is contained in:
parent
6d0c30a125
commit
e3bbfdf270
36 changed files with 630 additions and 502 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { $ } from "bun"
|
||||
import { readdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { brotliCompressSync, constants } from "node:zlib"
|
||||
import { collectFiles } from "./files"
|
||||
|
||||
export async function buildAppArchive(channel: string, options?: { skipBuild?: boolean }) {
|
||||
if (options?.skipBuild) return compress({})
|
||||
|
|
@ -9,8 +9,10 @@ export async function buildAppArchive(channel: string, options?: { skipBuild?: b
|
|||
await $`bun run build`.cwd(root).env({ ...process.env, OPENCODE_CHANNEL: channel })
|
||||
const assets = Object.fromEntries(
|
||||
await Promise.all(
|
||||
(await files(path.join(root, "dist")))
|
||||
(await collectFiles(path.join(root, "dist")))
|
||||
.map((key) => key.replaceAll(path.sep, "/"))
|
||||
.filter((key) => !key.endsWith(".map"))
|
||||
.toSorted()
|
||||
.map(async (key) => {
|
||||
const source = path.join(root, "dist", key)
|
||||
const body = Buffer.from(await Bun.file(source).arrayBuffer())
|
||||
|
|
@ -31,16 +33,3 @@ function compress(assets: object) {
|
|||
function isText(key: string) {
|
||||
return key === "_headers" || /\.(?:css|html|js|json|svg|txt|webmanifest|xml)$/.test(key)
|
||||
}
|
||||
|
||||
async function files(root: string, current = root): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
(await readdir(current, { withFileTypes: true })).map((entry) => {
|
||||
const target = path.join(current, entry.name)
|
||||
return entry.isDirectory() ? files(root, target) : [path.relative(root, target).replaceAll(path.sep, "/")]
|
||||
}),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.toSorted()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "
|
|||
import { mainConfig } from "../vite.node.config"
|
||||
import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"
|
||||
import { buildAppArchive } from "./app-assets"
|
||||
import { verifyArtifact } from "./verify-artifact"
|
||||
|
||||
const NODE_VERSION = "26.4.0"
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
|
|
@ -91,6 +92,7 @@ for (const target of targets) {
|
|||
await copyNodeAssets(assets)
|
||||
await build(mainConfig(input))
|
||||
await assertTextImportsInlined("dist-node/opencode.mjs")
|
||||
if (bundleOnly) await verifyArtifact("dist-node/opencode.mjs")
|
||||
|
||||
const host = target.platform === process.platform && target.arch === process.arch
|
||||
if (host) {
|
||||
|
|
@ -139,6 +141,7 @@ for (const target of targets) {
|
|||
2,
|
||||
)}\n`,
|
||||
)
|
||||
await verifyArtifact(path.join(outdir, name))
|
||||
if (host) await smoke(output)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
|
|||
import type { BunPlugin } from "bun"
|
||||
import pkg from "../package.json"
|
||||
import { buildAppArchive } from "./app-assets"
|
||||
import { verifyArtifact, verifySimulationGraph } from "./verify-artifact"
|
||||
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
const binary = "opencode2"
|
||||
|
|
@ -76,6 +77,16 @@ const appAssetsPlugin: BunPlugin = {
|
|||
}
|
||||
|
||||
for (const item of targets) {
|
||||
const simulationInputs = new Set<string>()
|
||||
const simulationGraphPlugin: BunPlugin = {
|
||||
name: "opencode-simulation-graph",
|
||||
setup(build) {
|
||||
build.onLoad(
|
||||
{ filter: /packages[/\\]simulation[/\\]src[/\\](frontend[/\\](simulation|server)|control-server)\.ts$/ },
|
||||
(args) => void simulationInputs.add(args.path),
|
||||
)
|
||||
},
|
||||
}
|
||||
const parcelWatcherPackage = `@parcel/watcher-${item.os}-${item.arch}${item.os === "linux" ? `-${item.abi ?? "glibc"}` : ""}`
|
||||
const parcelWatcherPlugin: BunPlugin = {
|
||||
name: "parcel-watcher-binding",
|
||||
|
|
@ -92,7 +103,7 @@ for (const item of targets) {
|
|||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [appAssetsPlugin, solidPlugin, parcelWatcherPlugin],
|
||||
plugins: [appAssetsPlugin, solidPlugin, parcelWatcherPlugin, simulationGraphPlugin],
|
||||
external: ["node-gyp"],
|
||||
format: "esm",
|
||||
minify: true,
|
||||
|
|
@ -123,6 +134,7 @@ for (const item of targets) {
|
|||
for (const log of result.logs) console.error(log)
|
||||
process.exit(1)
|
||||
}
|
||||
verifySimulationGraph(simulationInputs)
|
||||
|
||||
await Bun.write(
|
||||
path.join(outdir, name, "package.json"),
|
||||
|
|
@ -139,6 +151,7 @@ for (const item of targets) {
|
|||
2,
|
||||
),
|
||||
)
|
||||
await verifyArtifact(path.join(outdir, name))
|
||||
}
|
||||
|
||||
function targetName(item: (typeof allTargets)[number]) {
|
||||
|
|
|
|||
13
packages/cli/script/files.ts
Normal file
13
packages/cli/script/files.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { readdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
export async function collectFiles(root: string, current = root): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
(await readdir(current, { withFileTypes: true })).map(async (entry) => {
|
||||
const target = path.join(current, entry.name)
|
||||
return entry.isDirectory() ? collectFiles(root, target) : [path.relative(root, target)]
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { createHash } from "node:crypto"
|
||||
import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises"
|
||||
import { copyFile, mkdir, readFile, stat } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { getNodeAssets } from "@opentui/core/node-assets"
|
||||
import { attentionSoundAssets, type NodeTarget, photonWasmAsset, shellParserWasmAssets } from "../src/node/target"
|
||||
import { collectFiles } from "./files"
|
||||
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
|
||||
|
|
@ -16,17 +17,6 @@ export type NodeAsset = {
|
|||
readonly source: string
|
||||
}
|
||||
|
||||
async function files(root: string, current = root): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
(await readdir(current, { withFileTypes: true })).map((entry) => {
|
||||
const target = path.join(current, entry.name)
|
||||
return entry.isDirectory() ? files(root, target) : [path.relative(root, target)]
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
}
|
||||
|
||||
export async function collectNodeAssets(target: NodeTarget) {
|
||||
const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage))
|
||||
const ptyRoot = path.resolve(path.dirname(ptyEntry), "..")
|
||||
|
|
@ -51,7 +41,7 @@ export async function collectNodeAssets(target: NodeTarget) {
|
|||
key,
|
||||
source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)),
|
||||
})),
|
||||
...(await files(ptyRoot))
|
||||
...(await collectFiles(ptyRoot))
|
||||
.filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb"))
|
||||
.map((relative) => ({
|
||||
key: `${target.nodePtyPackage}/${relative}`,
|
||||
|
|
@ -85,5 +75,7 @@ export async function copyNodeAssets(assets: readonly NodeAsset[]) {
|
|||
|
||||
export async function seaAssetMap() {
|
||||
const root = path.join(dir, "dist-node", "assets")
|
||||
return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]))
|
||||
return Object.fromEntries(
|
||||
(await collectFiles(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ try {
|
|||
if (!(await exitsWithin(winner, 10_000))) throw new Error("Compiled service did not stop")
|
||||
for (let attempt = 0; attempt < 200 && (await Bun.file(registration).exists()); attempt++) await Bun.sleep(25)
|
||||
if (await Bun.file(registration).exists()) throw new Error("Compiled service registration was not removed")
|
||||
await driveSmoke()
|
||||
} catch (cause) {
|
||||
failure = cause
|
||||
} finally {
|
||||
|
|
@ -111,6 +112,50 @@ function spawnService() {
|
|||
return process
|
||||
}
|
||||
|
||||
async function driveSmoke() {
|
||||
const name = "compiled-artifact"
|
||||
const drive = path.resolve(import.meta.dir, "../../drive/src/cli/index.ts")
|
||||
const driveEnv = {
|
||||
...env,
|
||||
DRIVE_REGISTRY_DIR: path.join(root, "drive-registry"),
|
||||
OPENCODE_DRIVE_KEEP_ARTIFACTS: "1",
|
||||
OPENCODE_DRIVE_MEDIA_DIR: path.join(root, "drive-media"),
|
||||
}
|
||||
let started = false
|
||||
try {
|
||||
await runDrive(["start", "--name", name, "--", binary], drive, driveEnv)
|
||||
started = true
|
||||
const screenshot = (
|
||||
await runDrive(
|
||||
["send", "--name", name, "--command.ui.screenshot", '{"name":"compiled-capture"}'],
|
||||
drive,
|
||||
driveEnv,
|
||||
)
|
||||
).trim()
|
||||
const bytes = Buffer.from(await Bun.file(screenshot).arrayBuffer())
|
||||
if (!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
||||
throw new Error("Compiled Drive bridge did not produce a PNG capture")
|
||||
} finally {
|
||||
if (started) await runDrive(["stop", "--name", name], drive, driveEnv)
|
||||
}
|
||||
}
|
||||
|
||||
async function runDrive(args: ReadonlyArray<string>, drive: string, driveEnv: Record<string, string | undefined>) {
|
||||
const child = Bun.spawn([process.execPath, drive, ...args], {
|
||||
cwd: root,
|
||||
env: driveEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [status, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
])
|
||||
if (status !== 0) throw new Error(`opencode-drive ${args[0]} failed:\n${stderr}`)
|
||||
return stdout
|
||||
}
|
||||
|
||||
async function waitForRegistration() {
|
||||
const directory = path.join(root, "state", "opencode")
|
||||
for (let attempt = 0; attempt < 400; attempt++) {
|
||||
|
|
|
|||
69
packages/cli/script/verify-artifact.ts
Normal file
69
packages/cli/script/verify-artifact.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { stat } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { collectFiles } from "./files"
|
||||
|
||||
const forbidden = [
|
||||
"@napi-rs/canvas",
|
||||
"@fontsource/commit-mono",
|
||||
"@fontsource/noto-sans",
|
||||
"SimulationPng",
|
||||
"frontend/png",
|
||||
"Failed to register screenshot font",
|
||||
"commit-mono-latin-400-normal",
|
||||
"noto-sans-symbols-symbols-400-normal",
|
||||
"noto-sans-math-math-400-normal",
|
||||
"CommitMono-400-Regular.otf",
|
||||
"NotoSansSymbols.ttf",
|
||||
"packages/drive/src/recording/render",
|
||||
"src/frontend/png.ts",
|
||||
"skia.darwin-",
|
||||
"skia.linux-",
|
||||
"skia.win32-",
|
||||
]
|
||||
const overlap = Math.max(...forbidden.map((value) => value.length)) - 1
|
||||
|
||||
export async function verifyArtifact(target: string) {
|
||||
const files = await artifactFiles(target)
|
||||
if (files.length === 0) throw new Error(`Artifact contains no published files: ${target}`)
|
||||
for (const file of files) await scan(file)
|
||||
}
|
||||
|
||||
export function verifySimulationGraph(inputs: Iterable<string>) {
|
||||
const modules = Array.from(inputs, (input) => input.replaceAll("\\", "/"))
|
||||
const required = [
|
||||
"/packages/simulation/src/frontend/simulation.ts",
|
||||
"/packages/simulation/src/frontend/server.ts",
|
||||
"/packages/simulation/src/control-server.ts",
|
||||
]
|
||||
const missing = required.filter((input) => !modules.some((module) => module.endsWith(input)))
|
||||
if (missing.length > 0) throw new Error(`Build graph is missing simulation bridge inputs: ${missing.join(", ")}`)
|
||||
const leaked = modules.find(
|
||||
(module) =>
|
||||
module.includes("/packages/simulation/src/frontend/png.") || module.includes("/packages/drive/src/recording/"),
|
||||
)
|
||||
if (leaked) throw new Error(`Build graph contains Drive-only rendering input: ${leaked}`)
|
||||
}
|
||||
|
||||
async function artifactFiles(target: string): Promise<string[]> {
|
||||
if ((await stat(target)).isFile()) return [target]
|
||||
return (await collectFiles(target)).map((file) => path.join(target, file))
|
||||
}
|
||||
|
||||
async function scan(file: string) {
|
||||
let trailing = ""
|
||||
const reader = Bun.file(file).stream().getReader()
|
||||
while (true) {
|
||||
const chunk = await reader.read()
|
||||
if (chunk.done) return
|
||||
const text = trailing + Buffer.from(chunk.value).toString("latin1")
|
||||
const leaked = forbidden.find((marker) => text.includes(marker))
|
||||
if (leaked) throw new Error(`Artifact file ${file} contains forbidden simulation payload: ${leaked}`)
|
||||
trailing = text.slice(-overlap)
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const target = process.argv[2]
|
||||
if (!target) throw new Error("Usage: bun run script/verify-artifact.ts <file-or-directory>")
|
||||
await verifyArtifact(target)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue