فهرست منبع

feat(nix): overhaul nix flake and packages (#9032)

Caleb Norton 3 هفته پیش
والد
کامیت
dac099a489
10فایلهای تغییر یافته به همراه212 افزوده شده و 653 حذف شده
  1. 0 77
      .github/workflows/update-nix-hashes.yml
  2. 3 3
      flake.lock
  3. 21 89
      flake.nix
  4. 0 40
      nix/bundle.ts
  5. 62 129
      nix/desktop.nix
  6. 0 62
      nix/node-modules.nix
  7. 121 90
      nix/opencode.nix
  8. 0 120
      nix/scripts/bun-build.ts
  9. 0 43
      nix/scripts/patch-wasm.ts
  10. 5 0
      packages/opencode/script/build.ts

+ 0 - 77
.github/workflows/update-nix-hashes.yml

@@ -19,84 +19,7 @@ on:
       - ".github/workflows/update-nix-hashes.yml"
 
 jobs:
-  update-flake:
-    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
-    runs-on: blacksmith-4vcpu-ubuntu-2404
-    env:
-      TITLE: flake.lock
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v6
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          fetch-depth: 0
-          ref: ${{ github.head_ref || github.ref_name }}
-          repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
-
-      - name: Setup Nix
-        uses: nixbuild/nix-quick-install-action@v34
-
-      - name: Configure git
-        run: |
-          git config --global user.email "[email protected]"
-          git config --global user.name "Github Action"
-
-      - name: Update ${{ env.TITLE }}
-        run: |
-          set -euo pipefail
-          echo "Updating $TITLE..."
-          nix flake update
-          echo "$TITLE updated successfully"
-
-      - name: Commit ${{ env.TITLE }} changes
-        env:
-          TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
-        run: |
-          set -euo pipefail
-
-          echo "Checking for changes in tracked files..."
-
-          summarize() {
-            local status="$1"
-            {
-              echo "### Nix $TITLE"
-              echo ""
-              echo "- ref: ${GITHUB_REF_NAME}"
-              echo "- status: ${status}"
-            } >> "$GITHUB_STEP_SUMMARY"
-            if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
-              echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
-            fi
-            echo "" >> "$GITHUB_STEP_SUMMARY"
-          }
-          FILES=(flake.lock flake.nix)
-          STATUS="$(git status --short -- "${FILES[@]}" || true)"
-          if [ -z "$STATUS" ]; then
-            echo "No changes detected."
-            summarize "no changes"
-            exit 0
-          fi
-
-          echo "Changes detected:"
-          echo "$STATUS"
-          echo "Staging files..."
-          git add "${FILES[@]}"
-          echo "Committing changes..."
-          git commit -m "Update $TITLE"
-          echo "Changes committed"
-
-          BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
-          echo "Pulling latest from branch: $BRANCH"
-          git pull --rebase --autostash origin "$BRANCH"
-          echo "Pushing changes to branch: $BRANCH"
-          git push origin HEAD:"$BRANCH"
-          echo "Changes pushed successfully"
-
-          summarize "committed $(git rev-parse --short HEAD)"
-
   compute-node-modules-hash:
-    needs: update-flake
     if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
     strategy:
       fail-fast: false

+ 3 - 3
flake.lock

@@ -2,11 +2,11 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1768456270,
-        "narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=",
+        "lastModified": 1768302833,
+        "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "f4606b01b39e09065df37905a2133905246db9ed",
+        "rev": "61db79b0c6b838d9894923920b612048e1201926",
         "type": "github"
       },
       "original": {

+ 21 - 89
flake.nix

@@ -6,11 +6,7 @@
   };
 
   outputs =
-    {
-      self,
-      nixpkgs,
-      ...
-    }:
+    { self, nixpkgs, ... }:
     let
       systems = [
         "aarch64-linux"
@@ -18,99 +14,35 @@
         "aarch64-darwin"
         "x86_64-darwin"
       ];
-      inherit (nixpkgs) lib;
-      forEachSystem = lib.genAttrs systems;
-      pkgsFor = system: nixpkgs.legacyPackages.${system};
-      packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
-      bunTarget = {
-        "aarch64-linux" = "bun-linux-arm64";
-        "x86_64-linux" = "bun-linux-x64";
-        "aarch64-darwin" = "bun-darwin-arm64";
-        "x86_64-darwin" = "bun-darwin-x64";
-      };
-
-      # Parse "bun-{os}-{cpu}" to {os, cpu}
-      parseBunTarget =
-        target:
-        let
-          parts = lib.splitString "-" target;
-        in
-        {
-          os = builtins.elemAt parts 1;
-          cpu = builtins.elemAt parts 2;
-        };
-
-      hashesFile = "${./nix}/hashes.json";
-      hashesData =
-        if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
-      # Lookup hash: supports per-system ({system: hash}) or legacy single hash
-      nodeModulesHashFor =
-        system:
-        if builtins.isAttrs hashesData.nodeModules then
-          hashesData.nodeModules.${system}
-        else
-          hashesData.nodeModules;
-      modelsDev = forEachSystem (
-        system:
-        let
-          pkgs = pkgsFor system;
-        in
-        pkgs."models-dev"
-      );
+      forEachSystem = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
+      rev = self.shortRev or self.dirtyShortRev or "dirty";
     in
     {
-      devShells = forEachSystem (
-        system:
-        let
-          pkgs = pkgsFor system;
-        in
-        {
-          default = pkgs.mkShell {
-            packages = with pkgs; [
-              bun
-              nodejs_20
-              pkg-config
-              openssl
-              git
-            ];
-          };
-        }
-      );
+      devShells = forEachSystem (pkgs: {
+        default = pkgs.mkShell {
+          packages = with pkgs; [
+            bun
+            nodejs_20
+            pkg-config
+            openssl
+            git
+          ];
+        };
+      });
 
       packages = forEachSystem (
-        system:
+        pkgs:
         let
-          pkgs = pkgsFor system;
-          bunPlatform = parseBunTarget bunTarget.${system};
-          mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
-            hash = nodeModulesHashFor system;
-            bunCpu = bunPlatform.cpu;
-            bunOs = bunPlatform.os;
+          opencode = pkgs.callPackage ./nix/opencode.nix {
+            inherit rev;
           };
-          mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
-          mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
-
-          opencodePkg = mkOpencode {
-            inherit (packageJson) version;
-            src = ./.;
-            scripts = ./nix/scripts;
-            target = bunTarget.${system};
-            modelsDev = "${modelsDev.${system}}/dist/_api.json";
-            inherit mkNodeModules;
-          };
-
-          desktopPkg = mkDesktop {
-            inherit (packageJson) version;
-            src = ./.;
-            scripts = ./nix/scripts;
-            mkNodeModules = mkNodeModules;
-            opencode = opencodePkg;
+          desktop = pkgs.callPackage ./nix/desktop.nix {
+            inherit opencode;
           };
         in
         {
-          default = self.packages.${system}.opencode;
-          opencode = opencodePkg;
-          desktop = desktopPkg;
+          default = opencode;
+          inherit opencode desktop;
         }
       );
     };

+ 0 - 40
nix/bundle.ts

@@ -1,40 +0,0 @@
-#!/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))

+ 62 - 129
nix/desktop.nix

@@ -2,166 +2,99 @@
   lib,
   stdenv,
   rustPlatform,
-  bun,
   pkg-config,
-  dbus ? null,
-  openssl,
-  glib ? null,
-  gtk3 ? null,
-  libsoup_3 ? null,
-  webkitgtk_4_1 ? null,
-  librsvg ? null,
-  libappindicator-gtk3 ? null,
+  cargo-tauri,
+  bun,
+  nodejs,
   cargo,
   rustc,
-  makeBinaryWrapper,
-  copyDesktopItems,
-  makeDesktopItem,
-  nodejs,
   jq,
+  wrapGAppsHook4,
+  makeWrapper,
+  dbus,
+  glib,
+  gtk4,
+  libsoup_3,
+  librsvg,
+  libappindicator,
+  glib-networking,
+  openssl,
+  webkitgtk_4_1,
+  gst_all_1,
+  opencode,
 }:
-args:
-let
-  scripts = args.scripts;
-  mkModules =
-    attrs:
-    args.mkNodeModules (
-      attrs
-      // {
-        canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
-        normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
-      }
-    );
-in
-rustPlatform.buildRustPackage rec {
+rustPlatform.buildRustPackage (finalAttrs: {
   pname = "opencode-desktop";
-  version = args.version;
+  inherit (opencode)
+    version
+    src
+    node_modules
+    patches
+    ;
 
-  src = args.src;
-
-  # We need to set the root for cargo, but we also need access to the whole repo.
-  postUnpack = ''
-    # Update sourceRoot to point to the tauri app
-    sourceRoot+=/packages/desktop/src-tauri
-  '';
-
-  cargoLock = {
-    lockFile = ../packages/desktop/src-tauri/Cargo.lock;
-    allowBuiltinFetchGit = true;
-  };
-
-  node_modules = mkModules {
-    version = version;
-    src = src;
-  };
+  cargoRoot = "packages/desktop/src-tauri";
+  cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock;
+  buildAndTestSubdir = finalAttrs.cargoRoot;
 
   nativeBuildInputs = [
     pkg-config
+    cargo-tauri.hook
     bun
-    makeBinaryWrapper
-    copyDesktopItems
+    nodejs # for patchShebangs node_modules
     cargo
     rustc
-    nodejs
     jq
-  ];
-
-  # based on packages/desktop/src-tauri/release/appstream.metainfo.xml
-  desktopItems = lib.optionals stdenv.isLinux [
-    (makeDesktopItem {
-      name = "ai.opencode.opencode";
-      desktopName = "OpenCode";
-      comment = "Open source AI coding agent";
-      exec = "opencode-desktop";
-      icon = "opencode";
-      terminal = false;
-      type = "Application";
-      categories = [ "Development" "IDE" ];
-      startupWMClass = "opencode";
-    })
-  ];
-
-  buildInputs = [
-    openssl
+    makeWrapper
   ]
-  ++ lib.optionals stdenv.isLinux [
+  ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
+
+  buildInputs = lib.optionals stdenv.isLinux [
     dbus
     glib
-    gtk3
+    gtk4
     libsoup_3
-    webkitgtk_4_1
     librsvg
-    libappindicator-gtk3
+    libappindicator
+    glib-networking
+    openssl
+    webkitgtk_4_1
+    gst_all_1.gstreamer
+    gst_all_1.gst-plugins-base
+    gst_all_1.gst-plugins-good
   ];
 
-  preBuild = ''
-    # Restore node_modules
-    pushd ../../..
-
-    # Copy node_modules from the fixed-output derivation
-    # We use cp -r --no-preserve=mode to ensure we can write to them if needed,
-    # though we usually just read.
-    cp -r ${node_modules}/node_modules .
-    cp -r ${node_modules}/packages .
+  strictDeps = true;
 
-    # Ensure node_modules is writable so patchShebangs can update script headers
-    chmod -R u+w node_modules
-    # Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo)
-    chmod -R u+w packages
-    # Patch shebangs so scripts can run
+  preBuild = ''
+    cp -a ${finalAttrs.node_modules}/{node_modules,packages} .
+    chmod -R u+w node_modules packages
     patchShebangs node_modules
+    patchShebangs packages/desktop/node_modules
 
-    # Copy sidecar
     mkdir -p packages/desktop/src-tauri/sidecars
-    targetTriple=${stdenv.hostPlatform.rust.rustcTarget}
-    cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple
-
-    # Merge prod config into tauri.conf.json
-    if ! jq -s '.[0] * .[1]' \
-      packages/desktop/src-tauri/tauri.conf.json \
-      packages/desktop/src-tauri/tauri.prod.conf.json \
-      > packages/desktop/src-tauri/tauri.conf.json.tmp; then
-      echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2
-      exit 1
-    fi
-    mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json
-
-    # Build the frontend
-    cd packages/desktop
-
-    # The 'build' script runs 'bun run typecheck && vite build'.
-    bun run build
-
-    popd
+    cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget}
   '';
 
-  # Tauri bundles the assets during the rust build phase (which happens after preBuild).
-  # It looks for them in the location specified in tauri.conf.json.
-
-  postInstall = lib.optionalString stdenv.isLinux ''
-    # Install icon
-    mkdir -p $out/share/icons/hicolor/128x128/apps
-    cp ../../../packages/desktop/src-tauri/icons/prod/128x128.png $out/share/icons/hicolor/128x128/apps/opencode.png
+  # see publish-tauri job in .github/workflows/publish.yml
+  tauriBuildFlags = [
+    "--config"
+    "tauri.prod.conf.json"
+    "--no-sign" # no code signing or auto updates
+  ];
 
-    # Wrap the binary to ensure it finds the libraries
-    wrapProgram $out/bin/opencode-desktop \
-      --prefix LD_LIBRARY_PATH : ${
-        lib.makeLibraryPath [
-          gtk3
-          webkitgtk_4_1
-          librsvg
-          glib
-          libsoup_3
-        ]
-      }
+  # FIXME: workaround for concerns about case insensitive filesystems
+  # should be removed once binary is renamed or decided otherwise
+  # darwin output is a .app bundle so no conflict
+  postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
+    mv $out/bin/OpenCode $out/bin/opencode-desktop
+    sed -i 's|^Exec=OpenCode$|Exec=opencode-desktop|' $out/share/applications/OpenCode.desktop
   '';
 
-  meta = with lib; {
+  meta = {
     description = "OpenCode Desktop App";
     homepage = "https://opencode.ai";
-    license = licenses.mit;
-    maintainers = with maintainers; [ ];
+    license = lib.licenses.mit;
     mainProgram = "opencode-desktop";
-    platforms = platforms.linux ++ platforms.darwin;
+    inherit (opencode.meta) platforms;
   };
-}
+})

+ 0 - 62
nix/node-modules.nix

@@ -1,62 +0,0 @@
-{
-  hash,
-  lib,
-  stdenvNoCC,
-  bun,
-  cacert,
-  curl,
-  bunCpu,
-  bunOs,
-}:
-args:
-stdenvNoCC.mkDerivation {
-  pname = "opencode-node_modules";
-  inherit (args) version src;
-
-  impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
-    "GIT_PROXY_COMMAND"
-    "SOCKS_SERVER"
-  ];
-
-  nativeBuildInputs = [
-    bun
-    cacert
-    curl
-  ];
-
-  dontConfigure = true;
-
-  buildPhase = ''
-    runHook preBuild
-    export HOME=$(mktemp -d)
-    export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
-    bun install \
-      --cpu="${bunCpu}" \
-      --os="${bunOs}" \
-      --frozen-lockfile \
-      --ignore-scripts \
-      --no-progress \
-      --linker=isolated
-    bun --bun ${args.canonicalizeScript}
-    bun --bun ${args.normalizeBinsScript}
-    runHook postBuild
-  '';
-
-  installPhase = ''
-    runHook preInstall
-    mkdir -p $out
-    while IFS= read -r dir; do
-      rel="''${dir#./}"
-      dest="$out/$rel"
-      mkdir -p "$(dirname "$dest")"
-      cp -R "$dir" "$dest"
-    done < <(find . -type d -name node_modules -prune | sort)
-    runHook postInstall
-  '';
-
-  dontFixup = true;
-
-  outputHashAlgo = "sha256";
-  outputHashMode = "recursive";
-  outputHash = hash;
-}

+ 121 - 90
nix/opencode.nix

@@ -2,60 +2,115 @@
   lib,
   stdenvNoCC,
   bun,
-  ripgrep,
+  sysctl,
   makeBinaryWrapper,
+  models-dev,
+  ripgrep,
+  installShellFiles,
+  versionCheckHook,
+  writableTmpDirAsHomeHook,
+  rev ? "dirty",
 }:
-args:
 let
-  inherit (args) scripts;
-  mkModules =
-    attrs:
-    args.mkNodeModules (
-      attrs
-      // {
-        canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
-        normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
-      }
-    );
+  packageJson = lib.pipe ../packages/opencode/package.json [
+    builtins.readFile
+    builtins.fromJSON
+  ];
 in
 stdenvNoCC.mkDerivation (finalAttrs: {
   pname = "opencode";
-  inherit (args) version src;
+  version = "${packageJson.version}-${rev}";
+
+  src = lib.fileset.toSource {
+    root = ../.;
+    fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
+      lib.fileset.unions [
+        ../packages
+        ../bun.lock
+        ../package.json
+        ../patches
+        ../install
+      ]
+    );
+  };
 
-  node_modules = mkModules {
+  node_modules = stdenvNoCC.mkDerivation {
+    pname = "${finalAttrs.pname}-node_modules";
     inherit (finalAttrs) version src;
+
+    impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
+      "GIT_PROXY_COMMAND"
+      "SOCKS_SERVER"
+    ];
+
+    nativeBuildInputs = [
+      bun
+    ];
+
+    dontConfigure = true;
+
+    buildPhase = ''
+      runHook preBuild
+      export HOME=$(mktemp -d)
+      export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
+      bun install \
+        --cpu="${if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64"}" \
+        --os="${if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin"}" \
+        --frozen-lockfile \
+        --ignore-scripts \
+        --no-progress \
+        --linker=isolated
+      bun --bun ${./scripts/canonicalize-node-modules.ts}
+      bun --bun ${./scripts/normalize-bun-binaries.ts}
+      runHook postBuild
+    '';
+
+    installPhase = ''
+      runHook preInstall
+
+      mkdir -p $out
+      find . -type d -name node_modules -exec cp -R --parents {} $out \;
+
+      runHook postInstall
+    '';
+
+    dontFixup = true;
+
+    outputHashAlgo = "sha256";
+    outputHashMode = "recursive";
+    outputHash =
+      (lib.pipe ./hashes.json [
+        builtins.readFile
+        builtins.fromJSON
+      ]).nodeModules.${stdenvNoCC.hostPlatform.system};
   };
 
   nativeBuildInputs = [
     bun
+    installShellFiles
     makeBinaryWrapper
+    models-dev
+    writableTmpDirAsHomeHook
   ];
 
-  env.MODELS_DEV_API_JSON = args.modelsDev;
-  env.OPENCODE_VERSION = args.version;
-  env.OPENCODE_CHANNEL = "stable";
-  dontConfigure = true;
+  configurePhase = ''
+    runHook preConfigure
 
-  buildPhase = ''
-    runHook preBuild
+    cp -R ${finalAttrs.node_modules}/. .
 
-    cp -r ${finalAttrs.node_modules}/node_modules .
-    cp -r ${finalAttrs.node_modules}/packages .
+    runHook postConfigure
+  '';
 
-    (
-      cd packages/opencode
+  env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
+  env.OPENCODE_VERSION = finalAttrs.version;
+  env.OPENCODE_CHANNEL = "local";
 
-      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
+  buildPhase = ''
+    runHook preBuild
 
-      cp ${./bundle.ts} ./bundle.ts
-      chmod +x ./bundle.ts
-      bun run ./bundle.ts
-    )
+    cd ./packages/opencode
+    bun --bun ./script/build.ts --single --skip-install
+    bun --bun ./script/schema.ts schema.json
 
     runHook postBuild
   '';
@@ -63,76 +118,52 @@ stdenvNoCC.mkDerivation (finalAttrs: {
   installPhase = ''
     runHook preInstall
 
-    cd packages/opencode
-    if [ ! -d dist ]; then
-      echo "ERROR: dist directory missing after bundle step"
-      exit 1
-    fi
-
-    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
-
-    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 [ ripgrep ]} \
-      --argv0 opencode
+    install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
+    install -Dm644 schema.json $out/share/opencode/schema.json
+
+    wrapProgram $out/bin/opencode \
+      --prefix PATH : ${
+        lib.makeBinPath (
+          [
+            ripgrep
+          ]
+          # bun runs sysctl to detect if dunning on rosetta2
+          ++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
+        )
+      }
 
     runHook postInstall
   '';
 
-  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
+  postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
+    # trick yargs into also generating zsh completions
+    installShellCompletion --cmd opencode \
+      --bash <($out/bin/opencode completion) \
+      --zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
   '';
 
-  dontFixup = true;
+  nativeInstallCheckInputs = [
+    versionCheckHook
+    writableTmpDirAsHomeHook
+  ];
+  doInstallCheck = true;
+  versionCheckKeepEnvironment = [ "HOME" ];
+  versionCheckProgramArg = "--version";
+
+  passthru = {
+    jsonschema = "${placeholder "out"}/share/opencode/schema.json";
+  };
 
   meta = {
-    description = "AI coding agent built for the terminal";
-    longDescription = ''
-      OpenCode is a terminal-based agent that can build anything.
-      It combines a TypeScript/JavaScript core with a Go-based TUI
-      to provide an interactive AI coding experience.
-    '';
-    homepage = "https://github.com/anomalyco/opencode";
+    description = "The open source coding agent";
+    homepage = "https://opencode.ai/";
     license = lib.licenses.mit;
+    mainProgram = "opencode";
     platforms = [
       "aarch64-linux"
       "x86_64-linux"
       "aarch64-darwin"
       "x86_64-darwin"
     ];
-    mainProgram = "opencode";
   };
 })

+ 0 - 120
nix/scripts/bun-build.ts

@@ -1,120 +0,0 @@
-import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
-import path from "path"
-import fs from "fs"
-
-const version = "@VERSION@"
-const pkg = path.join(process.cwd(), "packages/opencode")
-const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"))
-const worker = "./src/cli/cmd/tui/worker.ts"
-const target = process.env["BUN_COMPILE_TARGET"]
-
-if (!target) {
-  throw new Error("BUN_COMPILE_TARGET not set")
-}
-
-process.chdir(pkg)
-
-const manifestName = "opencode-assets.manifest"
-const manifestPath = path.join(pkg, manifestName)
-
-const readTrackedAssets = () => {
-  if (!fs.existsSync(manifestPath)) return []
-  return fs
-    .readFileSync(manifestPath, "utf8")
-    .split("\n")
-    .map((line) => line.trim())
-    .filter((line) => line.length > 0)
-}
-
-const removeTrackedAssets = () => {
-  for (const file of readTrackedAssets()) {
-    const filePath = path.join(pkg, file)
-    if (fs.existsSync(filePath)) {
-      fs.rmSync(filePath, { force: true })
-    }
-  }
-}
-
-const assets = new Set<string>()
-
-const addAsset = async (p: string) => {
-  const file = path.basename(p)
-  const dest = path.join(pkg, file)
-  await Bun.write(dest, Bun.file(p))
-  assets.add(file)
-}
-
-removeTrackedAssets()
-
-const result = await Bun.build({
-  conditions: ["browser"],
-  tsconfig: "./tsconfig.json",
-  plugins: [solidPlugin],
-  sourcemap: "external",
-  entrypoints: ["./src/index.ts", parser, worker],
-  define: {
-    OPENCODE_VERSION: `'@VERSION@'`,
-    OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"),
-    OPENCODE_CHANNEL: "'latest'",
-  },
-  compile: {
-    target,
-    outfile: "opencode",
-    autoloadBunfig: false,
-    autoloadDotenv: false,
-    //@ts-ignore (bun types aren't up to date)
-    autoloadTsconfig: true,
-    autoloadPackageJson: true,
-    execArgv: ["--user-agent=opencode/" + version, "--use-system-ca", "--"],
-    windows: {},
-  },
-})
-
-if (!result.success) {
-  console.error("Build failed!")
-  for (const log of result.logs) {
-    console.error(log)
-  }
-  throw new Error("Compilation failed")
-}
-
-const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? []
-for (const x of assetOutputs) {
-  await addAsset(x.path)
-}
-
-const bundle = await Bun.build({
-  entrypoints: [worker],
-  tsconfig: "./tsconfig.json",
-  plugins: [solidPlugin],
-  target: "bun",
-  outdir: "./.opencode-worker",
-  sourcemap: "none",
-})
-
-if (!bundle.success) {
-  console.error("Worker build failed!")
-  for (const log of bundle.logs) {
-    console.error(log)
-  }
-  throw new Error("Worker compilation failed")
-}
-
-const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? []
-for (const x of workerAssets) {
-  await addAsset(x.path)
-}
-
-const output = bundle.outputs.find((x) => x.kind === "entry-point")
-if (!output) {
-  throw new Error("Worker build produced no entry-point output")
-}
-
-const dest = path.join(pkg, "opencode-worker.js")
-await Bun.write(dest, Bun.file(output.path))
-fs.rmSync(path.dirname(output.path), { recursive: true, force: true })
-
-const list = Array.from(assets)
-await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "")
-
-console.log("Build successful!")

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

@@ -1,43 +0,0 @@
-#!/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/...")
-const nixStorePrefix = process.env.NIX_STORE || "/nix/store"
-next = next.replace(/(\.\/)+/g, "./")
-next = next.replace(
-  new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
-  "/$2",
-)
-next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
-next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
-
-if (next !== content) fs.writeFileSync(file, next)

+ 5 - 0
packages/opencode/script/build.ts

@@ -90,6 +90,11 @@ const targets = singleFlag
         return baselineFlag
       }
 
+      // also skip abi-specific builds for the same reason
+      if (item.abi !== undefined) {
+        return false
+      }
+
       return true
     })
   : allTargets