Просмотр исходного кода

nix: bundle js dist with bun and patch tree-sitter wasm paths (#4644)

Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Github Action <[email protected]>
Albert O'Shea 4 месяцев назад
Родитель
Сommit
a3a239967f

+ 40 - 0
nix/bundle.ts

@@ -0,0 +1,40 @@
+#!/usr/bin/env bun
+
+import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
+import path from "path"
+import fs from "fs"
+
+const dir = process.cwd()
+const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
+const worker = "./src/cli/cmd/tui/worker.ts"
+const version = process.env.OPENCODE_VERSION ?? "local"
+const channel = process.env.OPENCODE_CHANNEL ?? "local"
+
+fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
+
+const result = await Bun.build({
+  entrypoints: ["./src/index.ts", worker, parser],
+  outdir: "./dist",
+  target: "bun",
+  sourcemap: "none",
+  tsconfig: "./tsconfig.json",
+  plugins: [solidPlugin],
+  external: ["@opentui/core"],
+  define: {
+    OPENCODE_VERSION: `'${version}'`,
+    OPENCODE_CHANNEL: `'${channel}'`,
+    // Leave undefined so runtime picks bundled/dist worker or fallback in code.
+    OPENCODE_WORKER_PATH: "undefined",
+    OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
+  },
+})
+
+if (!result.success) {
+  console.error("bundle failed")
+  for (const log of result.logs) console.error(log)
+  process.exit(1)
+}
+
+const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
+fs.mkdirSync(path.dirname(parserOut), { recursive: true })
+await Bun.write(parserOut, Bun.file(parser))

+ 59 - 34
nix/opencode.nix

@@ -1,4 +1,4 @@
-{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
+{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
 args:
 let
   scripts = args.scripts;
@@ -28,64 +28,89 @@ stdenvNoCC.mkDerivation (finalAttrs: {
     makeBinaryWrapper
   ];
 
-  configurePhase = ''
-    runHook preConfigure
-    cp -R ${finalAttrs.node_modules}/. .
-    runHook postConfigure
-  '';
-
   env.MODELS_DEV_API_JSON = args.modelsDev;
   env.OPENCODE_VERSION = args.version;
   env.OPENCODE_CHANNEL = "stable";
+  dontConfigure = true;
 
   buildPhase = ''
     runHook preBuild
 
-    cp ${scripts + "/bun-build.ts"} bun-build.ts
+    cp -r ${finalAttrs.node_modules}/node_modules .
+    cp -r ${finalAttrs.node_modules}/packages .
 
-    substituteInPlace bun-build.ts \
-      --replace '@VERSION@' "${finalAttrs.version}"
+    (
+      cd packages/opencode
 
-    export BUN_COMPILE_TARGET=${args.target}
-    bun --bun bun-build.ts
+      chmod -R u+w ./node_modules
+      mkdir -p ./node_modules/@opencode-ai
+      rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
+      ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
+      ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
+      ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
+
+      cp ${./bundle.ts} ./bundle.ts
+      chmod +x ./bundle.ts
+      bun run ./bundle.ts
+    )
 
     runHook postBuild
   '';
 
-  dontStrip = true;
-
   installPhase = ''
     runHook preInstall
 
     cd packages/opencode
-    if [ ! -f opencode ]; then
-      echo "ERROR: opencode binary not found in $(pwd)"
-      ls -la
+    if [ ! -d dist ]; then
+      echo "ERROR: dist directory missing after bundle step"
       exit 1
     fi
-    if [ ! -f opencode-worker.js ]; then
-      echo "ERROR: opencode worker bundle not found in $(pwd)"
-      ls -la
+
+    mkdir -p $out/lib/opencode
+    cp -r dist $out/lib/opencode/
+    chmod -R u+w $out/lib/opencode/dist
+
+    # Select bundled worker assets deterministically (sorted find output)
+    worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
+    parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
+    if [ -z "$worker_file" ]; then
+      echo "ERROR: bundled worker not found"
       exit 1
     fi
 
-    install -Dm755 opencode $out/bin/opencode
-    install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
-    if [ -f opencode-assets.manifest ]; then
-      while IFS= read -r asset; do
-        [ -z "$asset" ] && continue
-        if [ ! -f "$asset" ]; then
-          echo "ERROR: referenced asset \"$asset\" missing"
-          exit 1
-        fi
-        install -Dm644 "$asset" "$out/bin/$(basename "$asset")"
-      done < opencode-assets.manifest
-    fi
+    main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
+    wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
+    for patch_file in "$worker_file" "$parser_worker_file"; do
+      [ -z "$patch_file" ] && continue
+      [ ! -f "$patch_file" ] && continue
+      if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
+        # Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
+        bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
+      fi
+    done
+
+    mkdir -p $out/lib/opencode/node_modules
+    cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
+    mkdir -p $out/lib/opencode/node_modules/@opentui
+
+    mkdir -p $out/bin
+    makeWrapper ${bun}/bin/bun $out/bin/opencode \
+      --add-flags "run" \
+      --add-flags "$out/lib/opencode/dist/src/index.js" \
+      --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
+      --argv0 opencode
+
     runHook postInstall
   '';
 
-  postFixup = ''
-    wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}
+  postInstall = ''
+    for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
+      if [ -d "$pkg" ]; then
+        pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
+        ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
+          $out/lib/opencode/node_modules/@opentui/$pkgName
+      fi
+    done
   '';
 
   meta = {

+ 24 - 7
nix/scripts/canonicalize-node-modules.ts

@@ -24,15 +24,13 @@ for (const entry of directories) {
   if (!info.isDirectory()) {
     continue
   }
-  const marker = entry.lastIndexOf("@")
-  if (marker <= 0) {
+  const parsed = parseEntry(entry)
+  if (!parsed) {
     continue
   }
-  const slug = entry.slice(0, marker).replace(/\+/g, "/")
-  const version = entry.slice(marker + 1)
-  const list = versions.get(slug) ?? []
-  list.push({ dir: full, version, label: entry })
-  versions.set(slug, list)
+  const list = versions.get(parsed.name) ?? []
+  list.push({ dir: full, version: parsed.version, label: entry })
+  versions.set(parsed.name, list)
 }
 
 const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
@@ -79,6 +77,12 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
   await mkdir(parent, { recursive: true })
   const linkPath = join(parent, leaf)
   const desired = join(entry.dir, "node_modules", slug)
+  const exists = await lstat(desired)
+    .then(info => info.isDirectory())
+    .catch(() => false)
+  if (!exists) {
+    continue
+  }
   const relativeTarget = relative(parent, desired)
   const resolved = relativeTarget.length === 0 ? "." : relativeTarget
   await rm(linkPath, { recursive: true, force: true })
@@ -94,3 +98,16 @@ for (const line of rewrites.slice(0, 20)) {
 if (rewrites.length > 20) {
   console.log("  ...")
 }
+
+function parseEntry(label: string) {
+  const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
+  if (marker <= 0) {
+    return null
+  }
+  const name = label.slice(0, marker).replace(/\+/g, "/")
+  const version = label.slice(marker + 1)
+  if (!name || !version) {
+    return null
+  }
+  return { name, version }
+}

+ 39 - 0
nix/scripts/patch-wasm.ts

@@ -0,0 +1,39 @@
+#!/usr/bin/env bun
+
+import fs from "fs"
+import path from "path"
+
+/**
+ * Rewrite tree-sitter wasm references inside a JS file to absolute paths.
+ * argv: [node, script, file, mainWasm, ...wasmPaths]
+ */
+const [, , file, mainWasm, ...wasmPaths] = process.argv
+
+if (!file || !mainWasm) {
+  console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
+  process.exit(1)
+}
+
+const content = fs.readFileSync(file, "utf8")
+const byName = new Map<string, string>()
+
+for (const wasm of wasmPaths) {
+  const name = path.basename(wasm)
+  byName.set(name, wasm)
+}
+
+let next = content
+
+for (const [name, wasmPath] of byName) {
+  next = next.replaceAll(name, wasmPath)
+}
+
+next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
+
+// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
+next = next.replace(/(\.\/)+/g, "./")
+next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
+next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
+next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
+
+if (next !== content) fs.writeFileSync(file, next)

+ 6 - 8
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -58,16 +58,14 @@ export const TuiThreadCommand = cmd({
     // Resolve relative paths against PWD to preserve behavior when using --cwd flag
     const baseCwd = process.env.PWD ?? process.cwd()
     const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
-    const defaultWorker = new URL("./worker.ts", import.meta.url)
-    // Nix build creates a bundled worker next to the binary; prefer it when present.
+    const localWorker = new URL("./worker.ts", import.meta.url)
+    const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
     const execDir = path.dirname(process.execPath)
-    const bundledWorker = path.join(execDir, "opencode-worker.js")
-    const hasBundledWorker = await Bun.file(bundledWorker).exists()
-    const workerPath = (() => {
+    const workerPath = await iife(async () => {
       if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
-      if (hasBundledWorker) return bundledWorker
-      return defaultWorker
-    })()
+      if (await Bun.file(distWorker).exists()) return distWorker
+      return localWorker
+    })
     try {
       process.chdir(cwd)
     } catch (e) {