From b2ee706ea71f784833ee46988b83ebe5a7af25be Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 08:14:02 +1100 Subject: [PATCH 01/72] nix flake: initial flake and patches for ts --- flake.nix | 58 +++++++++++++ packages/opencode/src/cli/cmd/tui/thread.ts | 81 +++++++++++++------ .../opencode/src/provider/models-macro.ts | 7 ++ packages/sdk/js/src/gen/client/utils.gen.ts | 7 +- 4 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 flake.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..04e81a923a3 --- /dev/null +++ b/flake.nix @@ -0,0 +1,58 @@ +{ + description = "OpenCode development flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = + { + nixpkgs, + ... + }: + let + systems = [ + "aarch64-linux" + "x86_64-linux" + ]; + forEachSystem = nixpkgs.lib.genAttrs systems; + in + { + devShells = forEachSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + bun + nodejs_20 + pkg-config + openssl + git + ]; + }; + } + ); + + apps = forEachSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + opencode-dev = { + type = "app"; + program = pkgs.writeShellApplication { + name = "opencode-dev"; + runtimeInputs = [ pkgs.bun ]; + text = '' + exec bun run dev "$@" + ''; + }; + }; + } + ); + }; +} diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index c4b96e6da78..7cdf06dda98 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -6,6 +6,8 @@ import { Session } from "@/session" import { bootstrap } from "@/cli/bootstrap" import path from "path" import { UI } from "@/cli/ui" +import { Server } from "@/server/server" +import { Instance } from "@/project/instance" declare global { const OPENCODE_WORKER_PATH: string @@ -64,11 +66,9 @@ 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() - let workerPath: string | URL = new URL("./worker.ts", import.meta.url) - - if (typeof OPENCODE_WORKER_PATH !== "undefined") { - workerPath = OPENCODE_WORKER_PATH - } + const defaultWorker = new URL("./worker.ts", import.meta.url) + const workerPath = + typeof OPENCODE_WORKER_PATH !== "undefined" ? OPENCODE_WORKER_PATH : defaultWorker try { process.chdir(cwd) } catch (e) { @@ -97,31 +97,64 @@ export const TuiThreadCommand = cmd({ return undefined })() - const worker = new Worker(workerPath, { - env: Object.fromEntries( - Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), - ), - }) - worker.onerror = console.error - const client = Rpc.client(worker) - process.on("uncaughtException", (e) => { - console.error(e) - }) - process.on("unhandledRejection", (e) => { - console.error(e) - }) - const server = await client.call("server", { - port: args.port, - hostname: args.hostname, - }) + const setup = await (async () => { + const env = Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined, + ), + ) + const worker = await Promise.resolve() + .then( + () => + new Worker(workerPath, { + env, + }), + ) + .catch((error) => { + console.debug("opencode tui worker disabled", error) + return undefined + }) + if (worker) { + worker.onerror = console.error + const client = Rpc.client(worker) + process.on("uncaughtException", (error) => { + console.error(error) + }) + process.on("unhandledRejection", (error) => { + console.error(error) + }) + const remote = await client.call("server", { + port: args.port, + hostname: args.hostname, + }) + return { + url: remote.url, + stop: async () => { + await client.call("shutdown", undefined) + worker.terminate() + }, + } + } + const inline = Server.listen({ + port: args.port ?? 0, + hostname: args.hostname ?? "127.0.0.1", + }) + return { + url: inline.url.toString(), + stop: async () => { + await Instance.disposeAll() + await inline.stop(true) + }, + } + })() await tui({ - url: server.url, + url: setup.url, sessionID, model: args.model, agent: args.agent, prompt, onExit: async () => { - await client.call("shutdown", undefined) + await setup.stop() }, }) }) diff --git a/packages/opencode/src/provider/models-macro.ts b/packages/opencode/src/provider/models-macro.ts index 91a0348e8fb..6c8492a7017 100644 --- a/packages/opencode/src/provider/models-macro.ts +++ b/packages/opencode/src/provider/models-macro.ts @@ -1,4 +1,11 @@ export async function data() { + const path = Bun.env.MODELS_DEV_API_JSON + if (path) { + const file = Bun.file(path) + if (await file.exists()) { + return await file.text() + } + } const json = await fetch("https://models.dev/api.json").then((x) => x.text()) return json } diff --git a/packages/sdk/js/src/gen/client/utils.gen.ts b/packages/sdk/js/src/gen/client/utils.gen.ts index 209bfbe8e62..c159584cce4 100644 --- a/packages/sdk/js/src/gen/client/utils.gen.ts +++ b/packages/sdk/js/src/gen/client/utils.gen.ts @@ -155,8 +155,11 @@ export const buildUrl: Client["buildUrl"] = (options) => export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b } - if (config.baseUrl?.endsWith("/")) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) + if (config.baseUrl) { + const base = + typeof config.baseUrl === "string" ? config.baseUrl : String(config.baseUrl) + const trimmed = base.endsWith("/") ? base.substring(0, base.length - 1) : base + config.baseUrl = trimmed } config.headers = mergeHeaders(a.headers, b.headers) return config From 1b698ae84eaaeafa3299fb978b91526c158159c0 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 10:26:23 +1100 Subject: [PATCH 02/72] nix flake: bundle TUI worker for nix builds --- .gitignore | 1 + flake.lock | 27 ++ flake.nix | 258 ++++++++++++++++++++ packages/opencode/src/cli/cmd/tui/thread.ts | 11 +- 4 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 flake.lock diff --git a/.gitignore b/.gitignore index f69a7079669..95229bba489 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dist .turbo **/.serena .serena/ +/result \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..c9a945db53a --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1762156382, + "narHash": "sha256-Yg7Ag7ov5+36jEFC1DaZh/12SEXo6OO3/8rqADRxiqs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7241bcbb4f099a66aafca120d37c65e8dda32717", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 04e81a923a3..dbb37dfb164 100644 --- a/flake.nix +++ b/flake.nix @@ -36,6 +36,264 @@ } ); + packages = forEachSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + bun-target = { + "aarch64-linux" = "bun-linux-arm64"; + "x86_64-linux" = "bun-linux-x64"; + }; + + models-dev = pkgs.stdenvNoCC.mkDerivation { + pname = "models-dev"; + version = "unstable"; + + src = pkgs.fetchurl { + url = "https://models.dev/api.json"; + hash = "sha256-xQ1FjLTz8g4YbgZZ97j8FrYeuZd9aDUtLB67I23RQDQ="; + }; + + dontUnpack = true; + dontBuild = true; + + installPhase = '' + mkdir -p $out/dist + cp $src $out/dist/_api.json + ''; + }; + in + { + default = pkgs.callPackage ( + { + lib, + stdenv, + stdenvNoCC, + bun, + makeBinaryWrapper, + }: + stdenvNoCC.mkDerivation (finalAttrs: { + pname = "opencode"; + version = "1.0.23"; + + src = ./.; + + node_modules = stdenvNoCC.mkDerivation { + pname = "opencode-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 \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress + 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) + runHook postInstall + ''; + + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = "sha256-S77NbdzNuHALDapU3Qr/lGPwvHCvyGxr+nyVEO9zeBg="; + }; + + nativeBuildInputs = [ + bun + makeBinaryWrapper + ]; + + configurePhase = '' + runHook preConfigure + cp -R ${finalAttrs.node_modules}/. . + runHook postConfigure + ''; + + env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; + + buildPhase = '' + runHook preBuild + + cat > tsconfig.build.json <<'EOF' + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", + "allowImportingTsExtensions": true, + "baseUrl": ".", + "paths": { + "@/*": ["./packages/opencode/src/*"], + "@tui/*": ["./packages/opencode/src/cli/cmd/tui/*"] + } + } + } + EOF + + cat > bun-build.ts <<'EOF' + import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin" + import path from "path" + import fs from "fs" + + const version = "@VERSION@" + const channel = "@CHANNEL@" + const repoRoot = process.cwd() + const packageDir = path.join(repoRoot, "packages/opencode") + + const parserWorker = fs.realpathSync( + path.join(packageDir, "./node_modules/@opentui/core/parser.worker.js"), + ) + const dir = packageDir + const workerPath = "./src/cli/cmd/tui/worker.ts" + const target = process.env["BUN_COMPILE_TARGET"] + + if (!target) { + throw new Error("BUN_COMPILE_TARGET not set") + } + + // Change to package directory like the original build script does + process.chdir(packageDir) + + const result = await Bun.build({ + conditions: ["browser"], + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + sourcemap: "external", + entrypoints: ["./src/index.ts", parserWorker, workerPath], + define: { + OPENCODE_VERSION: `'@VERSION@'`, + OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replace(/\\/g, "/"), + OPENCODE_CHANNEL: `'@CHANNEL@'`, + }, + compile: { + target, + outfile: "opencode", + execArgv: ["--user-agent=opencode/" + version, "--env-file=\"\"", "--"], + windows: {}, + }, + }) + + if (!result.success) { + console.error("Build failed!") + for (const log of result.logs) { + console.error(log) + } + throw new Error("Compilation failed") + } + + // Nix packaging needs a real file for Worker() lookups, so emit a JS bundle alongside the binary. + const workerBundle = await Bun.build({ + entrypoints: [workerPath], + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + target: "bun", + outdir: "./.opencode-worker", + sourcemap: "none", + }) + + if (!workerBundle.success) { + console.error("Worker build failed!") + for (const log of workerBundle.logs) { + console.error(log) + } + throw new Error("Worker compilation failed") + } + + const workerOutput = workerBundle.outputs.find((output) => output.kind === "entry-point") + if (!workerOutput) { + throw new Error("Worker build produced no entry-point output") + } + + const workerTarget = path.join(packageDir, "opencode-worker.js") + const workerSource = workerOutput.path + await Bun.write(workerTarget, Bun.file(workerSource)) + fs.rmSync(path.dirname(workerSource), { recursive: true, force: true }) + + console.log("Build successful!") + EOF + + substituteInPlace bun-build.ts \ + --replace '@VERSION@' "${finalAttrs.version}" \ + --replace '@CHANNEL@' "latest" + + export BUN_COMPILE_TARGET=${bun-target.${stdenvNoCC.hostPlatform.system}} + bun --bun bun-build.ts + + runHook postBuild + ''; + + dontStrip = true; + + installPhase = '' + runHook preInstall + + # The binary is created in the package directory after chdir + cd packages/opencode + if [ ! -f opencode ]; then + echo "ERROR: opencode binary not found in $(pwd)" + ls -la + exit 1 + fi + if [ ! -f opencode-worker.js ]; then + echo "ERROR: opencode worker bundle not found in $(pwd)" + ls -la + exit 1 + fi + + install -Dm755 opencode $out/bin/opencode + install -Dm644 opencode-worker.js $out/bin/opencode-worker.js + runHook postInstall + ''; + + postFixup = lib.optionalString stdenvNoCC.hostPlatform.isLinux '' + wrapProgram $out/bin/opencode \ + --set LD_LIBRARY_PATH "${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}" + ''; + + 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/sst/opencode"; + license = lib.licenses.mit; + platforms = [ + "aarch64-linux" + "x86_64-linux" + ]; + mainProgram = "opencode"; + }; + }) + ) { }; + } + ); + apps = forEachSystem ( system: let diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7cdf06dda98..201fd90f0ca 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -67,8 +67,15 @@ export const TuiThreadCommand = cmd({ 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) - const workerPath = - typeof OPENCODE_WORKER_PATH !== "undefined" ? OPENCODE_WORKER_PATH : defaultWorker + // Nix build creates a bundled worker next to the binary; prefer it when present. + const execDir = path.dirname(process.execPath) + const bundledWorker = path.join(execDir, "opencode-worker.js") + const hasBundledWorker = await Bun.file(bundledWorker).exists() + const workerPath = (() => { + if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH + if (hasBundledWorker) return bundledWorker + return defaultWorker + })() try { process.chdir(cwd) } catch (e) { From c47e8bf0e306a63b2a53d4ec2051462ba41d6eab Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 12:06:15 +1100 Subject: [PATCH 03/72] nix flake: get the version from package.json instead of hardcoded --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index dbb37dfb164..0ff7fa9ddff 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,7 @@ system: let pkgs = import nixpkgs { inherit system; }; + packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json); bun-target = { "aarch64-linux" = "bun-linux-arm64"; "x86_64-linux" = "bun-linux-x64"; @@ -74,7 +75,7 @@ }: stdenvNoCC.mkDerivation (finalAttrs: { pname = "opencode"; - version = "1.0.23"; + version = packageJson.version; src = ./.; From aeacf232fdfa4550e21472e8bd87a620688a1edf Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 13:11:41 +1100 Subject: [PATCH 04/72] nix flake: add github workflow for automatically updating hashes --- .github/workflows/update-nix-flake.yml | 153 +++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 .github/workflows/update-nix-flake.yml diff --git a/.github/workflows/update-nix-flake.yml b/.github/workflows/update-nix-flake.yml new file mode 100644 index 00000000000..bb6a1055d7a --- /dev/null +++ b/.github/workflows/update-nix-flake.yml @@ -0,0 +1,153 @@ +name: Update Nix Flake + +on: + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update node_modules Output Hash + run: | + set -euo pipefail + + FILE="flake.nix" + DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + export DUMMY + + echo "Setting dummy node_modules outputHash..." + perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/s' "$FILE" + + if ! grep -q "$DUMMY" "$FILE"; then + echo "Failed to set dummy hash placeholder" + exit 1 + fi + + echo "Refreshing models-dev hash..." + MODELS_URL="https://models.dev/api.json" + MODELS_HASH=$(nix store prefetch-file "$MODELS_URL" --json | jq -r '.hash // empty') + if [ -z "$MODELS_HASH" ]; then + echo "Failed to prefetch models-dev hash" + exit 1 + fi + current_models_hash="$(perl -0ne 'print $1 if /models-dev\s*=\s*pkgs\.stdenvNoCC\.mkDerivation\s*\{.*?hash = "(sha256-[^"]+)"/s' "$FILE")" + if [ "$MODELS_HASH" != "$current_models_hash" ]; then + export MODELS_HASH + perl -0pi -e 's/(models-dev\s*=\s*pkgs\.stdenvNoCC\.mkDerivation\s*\{.*?hash = ")sha256-[^"]+(";\n)/${1}$ENV{MODELS_HASH}${2}/s' "$FILE" + if ! grep -q "$MODELS_HASH" "$FILE"; then + echo "Failed to update models-dev hash" + exit 1 + fi + echo "models-dev hash updated: $MODELS_HASH" + else + echo "models-dev hash already up to date: $MODELS_HASH" + fi + + SYSTEM="x86_64-linux" + TARGET="packages.${SYSTEM}.default" + + echo "Building ${TARGET} to discover correct node_modules outputHash..." + LOG=$(mktemp) + nix build ".#${TARGET}" --system "$SYSTEM" --no-link -L >"$LOG" 2>&1 || true + + CORRECT_HASH="" + FALLBACK="" + while IFS= read -r line; do + if [[ $line =~ \"actualHash\":\"(sha256-[A-Za-z0-9+/=]+)\" ]]; then + value="${BASH_REMATCH[1]}" + if [ "$value" != "$DUMMY" ]; then + CORRECT_HASH="$value" + break + fi + fi + if [[ $line =~ got:\s*(sha256-[A-Za-z0-9+/=]+) ]]; then + value="${BASH_REMATCH[1]}" + if [ "$value" != "$DUMMY" ]; then + CORRECT_HASH="$value" + break + fi + fi + if [[ -z "$FALLBACK" && $line =~ (sha256-[A-Za-z0-9+/=]+) ]]; then + value="${BASH_REMATCH[1]}" + if [ "$value" != "$DUMMY" ]; then + FALLBACK="$value" + fi + fi + done < "$LOG" + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$FALLBACK" + fi + + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to extract node_modules hash" + tail -100 "$LOG" + rm -f "$LOG" + exit 1 + fi + + rm -f "$LOG" + + export CORRECT_HASH="$CORRECT_HASH" + + perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/s' "$FILE" + + if ! grep -q "$CORRECT_HASH" "$FILE"; then + echo "Failed to update flake with new hash" + exit 1 + fi + + echo "node_modules hash updated: $CORRECT_HASH" + + - name: Verification Build (x86_64-linux) + run: | + echo "Running final verification build for x86_64-linux..." + nix build .#packages.x86_64-linux.default --system x86_64-linux -L + echo "x86_64-linux build successful." + + - name: Verification Build (aarch64-linux) + run: | + echo "Running final verification build for aarch64-linux..." + nix build .#packages.aarch64-linux.default --system aarch64-linux -L + echo "aarch64-linux build successful." + + - name: Commit and Push Changes + run: | + set -euo pipefail + + echo "Checking for changes..." + if git diff --quiet flake.nix; then + echo "No changes to flake.nix. Hash is already up to date." + exit 0 + fi + + VERSION=$(jq -r '.version' packages/opencode/package.json) + git add flake.nix + git commit -m "nix: update node_modules hash for v$VERSION" + + git pull --rebase origin dev + git push origin HEAD:dev + + - name: Summary + run: | + echo "✓ Nix flake updated and verified" + echo " - node_modules hash: Updated" + echo " - x86_64-linux: Build successful" + echo " - aarch64-linux: Build successful" From bd8a5bdb414806071c3148486aefb67aef25acc5 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 13:33:51 +1100 Subject: [PATCH 05/72] nix flake: support arm64 builds --- .github/workflows/update-nix-flake.yml | 3 +- flake.nix | 97 ++++++++++++++++++++------ 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/.github/workflows/update-nix-flake.yml b/.github/workflows/update-nix-flake.yml index bb6a1055d7a..df81d309058 100644 --- a/.github/workflows/update-nix-flake.yml +++ b/.github/workflows/update-nix-flake.yml @@ -56,7 +56,8 @@ jobs: exit 1 fi echo "models-dev hash updated: $MODELS_HASH" - else + fi + if [ "$MODELS_HASH" = "$current_models_hash" ]; then echo "models-dev hash already up to date: $MODELS_HASH" fi diff --git a/flake.nix b/flake.nix index 0ff7fa9ddff..e12c1aab099 100644 --- a/flake.nix +++ b/flake.nix @@ -90,7 +90,7 @@ "SOCKS_SERVER" ]; - nativeBuildInputs = [ bun ]; + nativeBuildInputs = [ bun pkgs.cacert pkgs.curl pkgs.python3 ]; dontConfigure = true; @@ -102,6 +102,80 @@ --frozen-lockfile \ --ignore-scripts \ --no-progress + + cat > optional-packages.txt <<'EOF' +@parcel/watcher-linux-arm64-glibc +@opentui/core-linux-arm64 +EOF + + python3 <<'PY' > optional-metadata.txt +import pathlib +import re +import sys + +lock = pathlib.Path("bun.lock").read_text() +targets = [line.strip() for line in pathlib.Path("optional-packages.txt").read_text().splitlines() if line.strip()] + +for name in targets: + version_pattern = rf'"{re.escape(name)}": "([^"]+)"' + version_match = re.search(version_pattern, lock) + if not version_match: + print(f"missing-version\t{name}") + sys.exit(1) + version = version_match.group(1) + sha_pattern = rf'"{re.escape(name)}@{re.escape(version)}"[^\n]*"(sha512-[^"]+)"' + sha_match = re.search(sha_pattern, lock) + if not sha_match: + print(f"missing-sha\t{name}\t{version}") + sys.exit(1) + print(f"{name}\t{version}\t{sha_match.group(1)}") +PY + + while IFS=$'\t' read -r name version sha; do + [ -z "$name" ] && continue + scope="''${name%%/*}" + remainder="''${name#*/}" + if [ "$scope" = "$name" ]; then + scope="" + remainder="$name" + fi + + base="''${remainder##*/}" + encoded_scope="''${scope//@/%40}" + + url="https://registry.npmjs.org/''${remainder}/-/''${base}-''${version}.tgz" + dest="node_modules/''${remainder}" + if [ -n "$scope" ]; then + url="https://registry.npmjs.org/''${encoded_scope}/''${remainder}/-/''${base}-''${version}.tgz" + dest="node_modules/''${scope}/''${remainder}" + fi + + tmp=$(mktemp) + curl --fail --location --silent --show-error --tlsv1.2 "$url" -o "$tmp" + python3 - "$tmp" "$sha" <<'PY' +import base64 +import hashlib +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +expected = sys.argv[2] +data = path.read_bytes() +digest = base64.b64encode(hashlib.sha512(data).digest()).decode() +if expected.startswith('sha512-'): + expected = expected.split('-', 1)[1] +if digest != expected: + print(f"hash mismatch: expected {expected}, got {digest}", file=sys.stderr) + sys.exit(1) +PY + + mkdir -p "$dest" + tar -xzf "$tmp" -C "$dest" --strip-components=1 package + rm -f "$tmp" + done < optional-metadata.txt + + rm -f optional-packages.txt optional-metadata.txt + runHook postBuild ''; @@ -140,28 +214,12 @@ buildPhase = '' runHook preBuild - cat > tsconfig.build.json <<'EOF' - { - "compilerOptions": { - "jsx": "preserve", - "jsxImportSource": "@opentui/solid", - "allowImportingTsExtensions": true, - "baseUrl": ".", - "paths": { - "@/*": ["./packages/opencode/src/*"], - "@tui/*": ["./packages/opencode/src/cli/cmd/tui/*"] - } - } - } - EOF - cat > bun-build.ts <<'EOF' import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin" import path from "path" import fs from "fs" const version = "@VERSION@" - const channel = "@CHANNEL@" const repoRoot = process.cwd() const packageDir = path.join(repoRoot, "packages/opencode") @@ -188,7 +246,7 @@ define: { OPENCODE_VERSION: `'@VERSION@'`, OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replace(/\\/g, "/"), - OPENCODE_CHANNEL: `'@CHANNEL@'`, + OPENCODE_CHANNEL: "'latest'", }, compile: { target, @@ -238,8 +296,7 @@ EOF substituteInPlace bun-build.ts \ - --replace '@VERSION@' "${finalAttrs.version}" \ - --replace '@CHANNEL@' "latest" + --replace '@VERSION@' "${finalAttrs.version}" export BUN_COMPILE_TARGET=${bun-target.${stdenvNoCC.hostPlatform.system}} bun --bun bun-build.ts From ddac67f7fe0bcef7d17499a240ae930b58a6cd3c Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 13:52:54 +1100 Subject: [PATCH 06/72] nix flake: workflow cleanup and tweaks --- .github/workflows/update-nix-flake.yml | 154 ------------------------ .github/workflows/update-nix-hashes.yml | 61 ++++++++++ flake.nix | 66 +++++----- script/update-nix-hashes | 92 ++++++++++++++ 4 files changed, 189 insertions(+), 184 deletions(-) delete mode 100644 .github/workflows/update-nix-flake.yml create mode 100644 .github/workflows/update-nix-hashes.yml create mode 100755 script/update-nix-hashes diff --git a/.github/workflows/update-nix-flake.yml b/.github/workflows/update-nix-flake.yml deleted file mode 100644 index df81d309058..00000000000 --- a/.github/workflows/update-nix-flake.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: Update Nix Flake - -on: - workflow_dispatch: - -jobs: - verify: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Update node_modules Output Hash - run: | - set -euo pipefail - - FILE="flake.nix" - DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - export DUMMY - - echo "Setting dummy node_modules outputHash..." - perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/s' "$FILE" - - if ! grep -q "$DUMMY" "$FILE"; then - echo "Failed to set dummy hash placeholder" - exit 1 - fi - - echo "Refreshing models-dev hash..." - MODELS_URL="https://models.dev/api.json" - MODELS_HASH=$(nix store prefetch-file "$MODELS_URL" --json | jq -r '.hash // empty') - if [ -z "$MODELS_HASH" ]; then - echo "Failed to prefetch models-dev hash" - exit 1 - fi - current_models_hash="$(perl -0ne 'print $1 if /models-dev\s*=\s*pkgs\.stdenvNoCC\.mkDerivation\s*\{.*?hash = "(sha256-[^"]+)"/s' "$FILE")" - if [ "$MODELS_HASH" != "$current_models_hash" ]; then - export MODELS_HASH - perl -0pi -e 's/(models-dev\s*=\s*pkgs\.stdenvNoCC\.mkDerivation\s*\{.*?hash = ")sha256-[^"]+(";\n)/${1}$ENV{MODELS_HASH}${2}/s' "$FILE" - if ! grep -q "$MODELS_HASH" "$FILE"; then - echo "Failed to update models-dev hash" - exit 1 - fi - echo "models-dev hash updated: $MODELS_HASH" - fi - if [ "$MODELS_HASH" = "$current_models_hash" ]; then - echo "models-dev hash already up to date: $MODELS_HASH" - fi - - SYSTEM="x86_64-linux" - TARGET="packages.${SYSTEM}.default" - - echo "Building ${TARGET} to discover correct node_modules outputHash..." - LOG=$(mktemp) - nix build ".#${TARGET}" --system "$SYSTEM" --no-link -L >"$LOG" 2>&1 || true - - CORRECT_HASH="" - FALLBACK="" - while IFS= read -r line; do - if [[ $line =~ \"actualHash\":\"(sha256-[A-Za-z0-9+/=]+)\" ]]; then - value="${BASH_REMATCH[1]}" - if [ "$value" != "$DUMMY" ]; then - CORRECT_HASH="$value" - break - fi - fi - if [[ $line =~ got:\s*(sha256-[A-Za-z0-9+/=]+) ]]; then - value="${BASH_REMATCH[1]}" - if [ "$value" != "$DUMMY" ]; then - CORRECT_HASH="$value" - break - fi - fi - if [[ -z "$FALLBACK" && $line =~ (sha256-[A-Za-z0-9+/=]+) ]]; then - value="${BASH_REMATCH[1]}" - if [ "$value" != "$DUMMY" ]; then - FALLBACK="$value" - fi - fi - done < "$LOG" - - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$FALLBACK" - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to extract node_modules hash" - tail -100 "$LOG" - rm -f "$LOG" - exit 1 - fi - - rm -f "$LOG" - - export CORRECT_HASH="$CORRECT_HASH" - - perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/s' "$FILE" - - if ! grep -q "$CORRECT_HASH" "$FILE"; then - echo "Failed to update flake with new hash" - exit 1 - fi - - echo "node_modules hash updated: $CORRECT_HASH" - - - name: Verification Build (x86_64-linux) - run: | - echo "Running final verification build for x86_64-linux..." - nix build .#packages.x86_64-linux.default --system x86_64-linux -L - echo "x86_64-linux build successful." - - - name: Verification Build (aarch64-linux) - run: | - echo "Running final verification build for aarch64-linux..." - nix build .#packages.aarch64-linux.default --system aarch64-linux -L - echo "aarch64-linux build successful." - - - name: Commit and Push Changes - run: | - set -euo pipefail - - echo "Checking for changes..." - if git diff --quiet flake.nix; then - echo "No changes to flake.nix. Hash is already up to date." - exit 0 - fi - - VERSION=$(jq -r '.version' packages/opencode/package.json) - git add flake.nix - git commit -m "nix: update node_modules hash for v$VERSION" - - git pull --rebase origin dev - git push origin HEAD:dev - - - name: Summary - run: | - echo "✓ Nix flake updated and verified" - echo " - node_modules hash: Updated" - echo " - x86_64-linux: Build successful" - echo " - aarch64-linux: Build successful" diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml new file mode 100644 index 00000000000..1f34b00aa38 --- /dev/null +++ b/.github/workflows/update-nix-hashes.yml @@ -0,0 +1,61 @@ +name: update nix hashes + +permissions: + contents: write + +on: + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Nix + uses: DeterminateSystems/nix-installer-action@v20 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Configure git + run: | + git config --global user.email "opencode@sst.dev" + git config --global user.name "opencode" + + - name: Update build output hashes + run: | + set -euo pipefail + script/update-nix-hashes + + - name: Verification build (x86_64-linux) + run: | + echo "Running final verification build for x86_64-linux..." + nix build .#packages.x86_64-linux.default --system x86_64-linux -L + echo "x86_64-linux build successful." + + - name: Verification build (aarch64-linux) + run: | + echo "Running final verification build for aarch64-linux..." + nix build .#packages.aarch64-linux.default --system aarch64-linux -L + echo "aarch64-linux build successful." + + - name: Commit flake.nix changes + run: | + set -euo pipefail + + if git diff --quiet flake.nix; then + echo "No changes to flake.nix. Hash is already up to date." + exit 0 + fi + + VERSION=$(jq -r '.version' packages/opencode/package.json) + git add flake.nix + git commit -m "nix: update output hashes for v$VERSION" + + git pull --rebase origin dev + git push origin HEAD:dev diff --git a/flake.nix b/flake.nix index e12c1aab099..2d58936a466 100644 --- a/flake.nix +++ b/flake.nix @@ -15,13 +15,43 @@ "aarch64-linux" "x86_64-linux" ]; - forEachSystem = nixpkgs.lib.genAttrs systems; + lib = 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"; + }; + modelsDev = forEachSystem ( + system: + let + pkgs = pkgsFor system; + in + pkgs.stdenvNoCC.mkDerivation { + pname = "models-dev"; + version = "unstable"; + + src = pkgs.fetchurl { + url = "https://models.dev/api.json"; + hash = "sha256-xQ1FjLTz8g4YbgZZ97j8FrYeuZd9aDUtLB67I23RQDQ="; + }; + + dontUnpack = true; + dontBuild = true; + + installPhase = '' + mkdir -p $out/dist + cp $src $out/dist/_api.json + ''; + } + ); in { devShells = forEachSystem ( system: let - pkgs = import nixpkgs { inherit system; }; + pkgs = pkgsFor system; in { default = pkgs.mkShell { @@ -39,30 +69,7 @@ packages = forEachSystem ( system: let - pkgs = import nixpkgs { inherit system; }; - packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json); - bun-target = { - "aarch64-linux" = "bun-linux-arm64"; - "x86_64-linux" = "bun-linux-x64"; - }; - - models-dev = pkgs.stdenvNoCC.mkDerivation { - pname = "models-dev"; - version = "unstable"; - - src = pkgs.fetchurl { - url = "https://models.dev/api.json"; - hash = "sha256-xQ1FjLTz8g4YbgZZ97j8FrYeuZd9aDUtLB67I23RQDQ="; - }; - - dontUnpack = true; - dontBuild = true; - - installPhase = '' - mkdir -p $out/dist - cp $src $out/dist/_api.json - ''; - }; + pkgs = pkgsFor system; in { default = pkgs.callPackage ( @@ -209,7 +216,7 @@ PY runHook postConfigure ''; - env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; + env.MODELS_DEV_API_JSON = "${modelsDev.${system}}/dist/_api.json"; buildPhase = '' runHook preBuild @@ -234,7 +241,6 @@ PY throw new Error("BUN_COMPILE_TARGET not set") } - // Change to package directory like the original build script does process.chdir(packageDir) const result = await Bun.build({ @@ -298,7 +304,7 @@ PY substituteInPlace bun-build.ts \ --replace '@VERSION@' "${finalAttrs.version}" - export BUN_COMPILE_TARGET=${bun-target.${stdenvNoCC.hostPlatform.system}} + export BUN_COMPILE_TARGET=${bunTarget.${stdenvNoCC.hostPlatform.system}} bun --bun bun-build.ts runHook postBuild @@ -355,7 +361,7 @@ PY apps = forEachSystem ( system: let - pkgs = import nixpkgs { inherit system; }; + pkgs = pkgsFor system; in { opencode-dev = { diff --git a/script/update-nix-hashes b/script/update-nix-hashes new file mode 100755 index 00000000000..ca6795c481c --- /dev/null +++ b/script/update-nix-hashes @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -euo pipefail + +FILE=${FILE:-flake.nix} +SYSTEM=${SYSTEM:-x86_64-linux} +TARGET="packages.${SYSTEM}.default" +DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +MODELS_URL=${MODELS_URL:-https://models.dev/api.json} +export DUMMY + +cleanup() { + if [ -n "${JSON_OUTPUT:-}" ] && [ -f "$JSON_OUTPUT" ]; then + rm -f "$JSON_OUTPUT" + fi + if [ -n "${BUILD_LOG:-}" ] && [ -f "$BUILD_LOG" ]; then + rm -f "$BUILD_LOG" + fi +} + +trap cleanup EXIT + +echo "Setting dummy node_modules outputHash..." +perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/s' "$FILE" + +if ! grep -q "$DUMMY" "$FILE"; then + echo "Failed to set dummy hash placeholder" + exit 1 +fi + +echo "Refreshing models-dev hash..." +MODELS_HASH=$(nix store prefetch-file "$MODELS_URL" --json | jq -r '.hash // empty') + +if [ -z "$MODELS_HASH" ]; then + echo "Failed to prefetch models-dev hash" + exit 1 +fi + +current_models_hash="$(perl -0ne 'print $1 if /modelsDev\s*=\s*forEachSystem\s*\(.*?hash\s*=\s*"(sha256-[^"]+)"/s' "$FILE")" + +if [ "$MODELS_HASH" != "$current_models_hash" ]; then + export MODELS_HASH + perl -0pi -e 's/(modelsDev\s*=\s*forEachSystem\s*\(.*?hash\s*=\s*")sha256-[^"]+(")/${1}$ENV{MODELS_HASH}${2}/s' "$FILE" + if ! grep -q "$MODELS_HASH" "$FILE"; then + echo "Failed to update models-dev hash" + exit 1 + fi + echo "models-dev hash updated: $MODELS_HASH" +fi + +if [ "$MODELS_HASH" = "$current_models_hash" ]; then + echo "models-dev hash already up to date: $MODELS_HASH" +fi + +JSON_OUTPUT=$(mktemp) +BUILD_LOG=$(mktemp) + +echo "Building ${TARGET} to discover correct node_modules outputHash..." +if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --json >"$JSON_OUTPUT" 2>"$BUILD_LOG"; then + echo "Build succeeded while dummy hash was set; aborting" + exit 1 +fi + +CORRECT_HASH="$(jq -r ' + map( + select(.error.actualHash? != null) + | .error.actualHash + ) + | map(select(. != env.DUMMY)) + | .[0] // "" +' "$JSON_OUTPUT")" + +if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -o 'sha256-[A-Za-z0-9+/=]\+' "$BUILD_LOG" | grep -v "$DUMMY" | head -n1 || true)" +fi + +if [ -z "$CORRECT_HASH" ]; then + echo "Failed to extract node_modules hash" + tail -100 "$BUILD_LOG" || true + exit 1 +fi + +export CORRECT_HASH + +perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/s' "$FILE" + +if ! grep -q "$CORRECT_HASH" "$FILE"; then + echo "Failed to update flake with new hash" + exit 1 +fi + +echo "node_modules hash updated: $CORRECT_HASH" From 93040056457fb35af94c0fa8da08798b67a7a759 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 15:37:03 +1100 Subject: [PATCH 07/72] nix flake: canonicalize node modules for reproducibility nix: improve flake tooling and workflow robustness - Add fetch-depth: 0 to workflow checkout to enable git rebase - Run canonicalizer directly from source instead of copying - Support dynamic branch names in update workflow using GITHUB_REF_NAME The workflow previously failed on rebase because shallow clones lack history. Running the canonicalizer in-place keeps the build tree cleaner and avoids drift. Dynamic branch support allows the workflow to run on any branch instead of hardcoding 'dev'. nix: add commented-out workspace symlink rewriting option Current .bun/node_modules rewriting provides sufficient stability. Added optional extension to rewrite all workspace-level symlinks through the canonical tree if needed in the future. --- .github/workflows/update-nix-hashes.yml | 7 +- flake.nix | 12 +- script/nix/canonicalize-node-modules.ts | 124 ++++++++++++++++++ .../{update-nix-hashes => nix/update-hashes} | 4 +- 4 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 script/nix/canonicalize-node-modules.ts rename script/{update-nix-hashes => nix/update-hashes} (88%) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 1f34b00aa38..247060f7cf9 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -15,6 +15,7 @@ jobs: uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 - name: Setup Nix uses: DeterminateSystems/nix-installer-action@v20 @@ -30,7 +31,7 @@ jobs: - name: Update build output hashes run: | set -euo pipefail - script/update-nix-hashes + script/nix/update-hashes - name: Verification build (x86_64-linux) run: | @@ -57,5 +58,5 @@ jobs: git add flake.nix git commit -m "nix: update output hashes for v$VERSION" - git pull --rebase origin dev - git push origin HEAD:dev + git pull --rebase origin ${GITHUB_REF_NAME} + git push origin HEAD:${GITHUB_REF_NAME} diff --git a/flake.nix b/flake.nix index 2d58936a466..9934c349370 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ src = pkgs.fetchurl { url = "https://models.dev/api.json"; - hash = "sha256-xQ1FjLTz8g4YbgZZ97j8FrYeuZd9aDUtLB67I23RQDQ="; + hash = "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg="; }; dontUnpack = true; @@ -86,7 +86,11 @@ src = ./.; - node_modules = stdenvNoCC.mkDerivation { + node_modules = + let + canonicalizeScript = ./script/nix/canonicalize-node-modules.ts; + in + stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; inherit (finalAttrs) version src; @@ -183,6 +187,8 @@ PY rm -f optional-packages.txt optional-metadata.txt + bun --bun ${canonicalizeScript} + runHook postBuild ''; @@ -202,7 +208,7 @@ PY outputHashAlgo = "sha256"; outputHashMode = "recursive"; - outputHash = "sha256-S77NbdzNuHALDapU3Qr/lGPwvHCvyGxr+nyVEO9zeBg="; + outputHash = "sha256-s/UTz8BTYDOZpF9P6nZr0b7fNOS7Nv7hUfpihJgsSqE="; }; nativeBuildInputs = [ diff --git a/script/nix/canonicalize-node-modules.ts b/script/nix/canonicalize-node-modules.ts new file mode 100644 index 00000000000..4d06cec952e --- /dev/null +++ b/script/nix/canonicalize-node-modules.ts @@ -0,0 +1,124 @@ +import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" +import { join, relative } from "path" + +type SemverLike = { + valid: (value: string) => string | null + rcompare: (left: string, right: string) => number +} + +type Entry = { + dir: string + version: string + label: string +} + +const root = process.cwd() +const bunRoot = join(root, "node_modules/.bun") +const linkRoot = join(bunRoot, "node_modules") +const directories = (await readdir(bunRoot)).sort() +const versions = new Map() + +for (const entry of directories) { + const full = join(bunRoot, entry) + const info = await lstat(full) + if (!info.isDirectory()) { + continue + } + const marker = entry.lastIndexOf("@") + if (marker <= 0) { + 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 semverModule = (await import(join(bunRoot, "node_modules/semver"))) as SemverLike | { + default: SemverLike +} +const semver = "default" in semverModule ? semverModule.default : semverModule +const selections = new Map() + +for (const [slug, list] of versions) { + list.sort((a, b) => { + const left = semver.valid(a.version) + const right = semver.valid(b.version) + if (left && right) { + const delta = semver.rcompare(left, right) + if (delta !== 0) { + return delta + } + } + if (left && !right) { + return -1 + } + if (!left && right) { + return 1 + } + return b.version.localeCompare(a.version) + }) + selections.set(slug, list[0]) +} + +await rm(linkRoot, { recursive: true, force: true }) +await mkdir(linkRoot, { recursive: true }) + +const rewrites: string[] = [] + +for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) { + const parts = slug.split("/") + const leaf = parts.pop() + if (!leaf) { + continue + } + const parent = join(linkRoot, ...parts) + await mkdir(parent, { recursive: true }) + const linkPath = join(parent, leaf) + const desired = join(entry.dir, "node_modules", slug) + const relativeTarget = relative(parent, desired) + const resolved = relativeTarget.length === 0 ? "." : relativeTarget + await rm(linkPath, { recursive: true, force: true }) + await symlink(resolved, linkPath) + rewrites.push(slug + " -> " + resolved) +} + +rewrites.sort() +console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links") +for (const line of rewrites.slice(0, 20)) { + console.log(" ", line) +} +if (rewrites.length > 20) { + console.log(" ...") +} + +// NOTE: Current .bun/node_modules rewriting seems to be working fine for now. +// Could uncomment below in case of emergencies to force ALL workspace +// node_modules to point through .bun/node_modules (untested). +/* +async function rewriteWorkspaceLinks() { + const workspaces = await readdir(root) + for (const ws of workspaces) { + const wsModules = join(root, ws, "node_modules") + try { + const stat = await lstat(wsModules) + if (!stat.isDirectory()) continue + const entries = await readdir(wsModules) + for (const entry of entries) { + const linkPath = join(wsModules, entry) + const linkStat = await lstat(linkPath) + if (!linkStat.isSymbolicLink()) continue + const canonical = join(linkRoot, entry) + try { + await lstat(canonical) + const relTarget = relative(join(wsModules), canonical) + await rm(linkPath, { recursive: true, force: true }) + await symlink(relTarget, linkPath) + } catch {} + } + } catch {} + } +} +await rewriteWorkspaceLinks() +*/ diff --git a/script/update-nix-hashes b/script/nix/update-hashes similarity index 88% rename from script/update-nix-hashes rename to script/nix/update-hashes index ca6795c481c..52f34edb0dc 100755 --- a/script/update-nix-hashes +++ b/script/nix/update-hashes @@ -21,7 +21,7 @@ cleanup() { trap cleanup EXIT echo "Setting dummy node_modules outputHash..." -perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/s' "$FILE" +perl -0pi -e 's/(node_modules\s*=\s*(?:let.*?in\s*)?stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/s' "$FILE" if ! grep -q "$DUMMY" "$FILE"; then echo "Failed to set dummy hash placeholder" @@ -82,7 +82,7 @@ fi export CORRECT_HASH -perl -0pi -e 's/(node_modules = stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/s' "$FILE" +perl -0pi -e 's/(node_modules\s*=\s*(?:let.*?in\s*)?stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/s' "$FILE" if ! grep -q "$CORRECT_HASH" "$FILE"; then echo "Failed to update flake with new hash" From 7e64c3624daae4224ebe3c58f6cc73fdcc5eed84 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 22:57:37 +1100 Subject: [PATCH 08/72] nix flake: refactor/split bun-build script into script/nix dir --- flake.nix | 80 +---------------------------------------- script/nix/bun-build.ts | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 79 deletions(-) create mode 100644 script/nix/bun-build.ts diff --git a/flake.nix b/flake.nix index 9934c349370..ef590f48949 100644 --- a/flake.nix +++ b/flake.nix @@ -227,85 +227,7 @@ PY buildPhase = '' runHook preBuild - cat > bun-build.ts <<'EOF' - import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin" - import path from "path" - import fs from "fs" - - const version = "@VERSION@" - const repoRoot = process.cwd() - const packageDir = path.join(repoRoot, "packages/opencode") - - const parserWorker = fs.realpathSync( - path.join(packageDir, "./node_modules/@opentui/core/parser.worker.js"), - ) - const dir = packageDir - const workerPath = "./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(packageDir) - - const result = await Bun.build({ - conditions: ["browser"], - tsconfig: "./tsconfig.json", - plugins: [solidPlugin], - sourcemap: "external", - entrypoints: ["./src/index.ts", parserWorker, workerPath], - define: { - OPENCODE_VERSION: `'@VERSION@'`, - OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replace(/\\/g, "/"), - OPENCODE_CHANNEL: "'latest'", - }, - compile: { - target, - outfile: "opencode", - execArgv: ["--user-agent=opencode/" + version, "--env-file=\"\"", "--"], - windows: {}, - }, - }) - - if (!result.success) { - console.error("Build failed!") - for (const log of result.logs) { - console.error(log) - } - throw new Error("Compilation failed") - } - - // Nix packaging needs a real file for Worker() lookups, so emit a JS bundle alongside the binary. - const workerBundle = await Bun.build({ - entrypoints: [workerPath], - tsconfig: "./tsconfig.json", - plugins: [solidPlugin], - target: "bun", - outdir: "./.opencode-worker", - sourcemap: "none", - }) - - if (!workerBundle.success) { - console.error("Worker build failed!") - for (const log of workerBundle.logs) { - console.error(log) - } - throw new Error("Worker compilation failed") - } - - const workerOutput = workerBundle.outputs.find((output) => output.kind === "entry-point") - if (!workerOutput) { - throw new Error("Worker build produced no entry-point output") - } - - const workerTarget = path.join(packageDir, "opencode-worker.js") - const workerSource = workerOutput.path - await Bun.write(workerTarget, Bun.file(workerSource)) - fs.rmSync(path.dirname(workerSource), { recursive: true, force: true }) - - console.log("Build successful!") - EOF + cp ${./script/nix/bun-build.ts} bun-build.ts substituteInPlace bun-build.ts \ --replace '@VERSION@' "${finalAttrs.version}" diff --git a/script/nix/bun-build.ts b/script/nix/bun-build.ts new file mode 100644 index 00000000000..de275c87d69 --- /dev/null +++ b/script/nix/bun-build.ts @@ -0,0 +1,75 @@ +import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin" +import path from "path" +import fs from "fs" + +const version = "@VERSION@" +const repo = process.cwd() +const pkg = path.join(repo, "packages/opencode") +const parser = fs.realpathSync( + path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"), +) +const dir = pkg +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 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(dir, parser).replace(/\\/g, "/"), + OPENCODE_CHANNEL: "'latest'", + }, + compile: { + target, + outfile: "opencode", + execArgv: ["--user-agent=opencode/" + version, "--env-file=\"\"", "--"], + windows: {}, + }, +}) + +if (!result.success) { + console.error("Build failed!") + for (const log of result.logs) { + console.error(log) + } + throw new Error("Compilation failed") +} + +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 output = bundle.outputs.find((item) => item.kind === "entry-point") +if (!output) { + throw new Error("Worker build produced no entry-point output") +} + +const dest = path.join(pkg, "opencode-worker.js") +const src = output.path +await Bun.write(dest, Bun.file(src)) +fs.rmSync(path.dirname(src), { recursive: true, force: true }) + +console.log("Build successful!") From 0a57a27c1429654bfa7a254332bec33d4c51fd16 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 23:07:59 +1100 Subject: [PATCH 09/72] nix flake: refactor/split bun-build script into script/nix dir --- flake.nix | 44 ++++----------------------------- script/nix/optional-metadata.ts | 40 ++++++++++++++++++++++++++++++ script/nix/verify-sha.ts | 19 ++++++++++++++ 3 files changed, 64 insertions(+), 39 deletions(-) create mode 100644 script/nix/optional-metadata.ts create mode 100644 script/nix/verify-sha.ts diff --git a/flake.nix b/flake.nix index ef590f48949..2f7e740243e 100644 --- a/flake.nix +++ b/flake.nix @@ -89,6 +89,8 @@ node_modules = let canonicalizeScript = ./script/nix/canonicalize-node-modules.ts; + optionalMetadataScript = ./script/nix/optional-metadata.ts; + verifyShaScript = ./script/nix/verify-sha.ts; in stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; @@ -101,7 +103,7 @@ "SOCKS_SERVER" ]; - nativeBuildInputs = [ bun pkgs.cacert pkgs.curl pkgs.python3 ]; + nativeBuildInputs = [ bun pkgs.cacert pkgs.curl ]; dontConfigure = true; @@ -119,28 +121,7 @@ @opentui/core-linux-arm64 EOF - python3 <<'PY' > optional-metadata.txt -import pathlib -import re -import sys - -lock = pathlib.Path("bun.lock").read_text() -targets = [line.strip() for line in pathlib.Path("optional-packages.txt").read_text().splitlines() if line.strip()] - -for name in targets: - version_pattern = rf'"{re.escape(name)}": "([^"]+)"' - version_match = re.search(version_pattern, lock) - if not version_match: - print(f"missing-version\t{name}") - sys.exit(1) - version = version_match.group(1) - sha_pattern = rf'"{re.escape(name)}@{re.escape(version)}"[^\n]*"(sha512-[^"]+)"' - sha_match = re.search(sha_pattern, lock) - if not sha_match: - print(f"missing-sha\t{name}\t{version}") - sys.exit(1) - print(f"{name}\t{version}\t{sha_match.group(1)}") -PY + bun --bun ${optionalMetadataScript} optional-packages.txt > optional-metadata.txt while IFS=$'\t' read -r name version sha; do [ -z "$name" ] && continue @@ -163,22 +144,7 @@ PY tmp=$(mktemp) curl --fail --location --silent --show-error --tlsv1.2 "$url" -o "$tmp" - python3 - "$tmp" "$sha" <<'PY' -import base64 -import hashlib -import pathlib -import sys - -path = pathlib.Path(sys.argv[1]) -expected = sys.argv[2] -data = path.read_bytes() -digest = base64.b64encode(hashlib.sha512(data).digest()).decode() -if expected.startswith('sha512-'): - expected = expected.split('-', 1)[1] -if digest != expected: - print(f"hash mismatch: expected {expected}, got {digest}", file=sys.stderr) - sys.exit(1) -PY + bun --bun ${verifyShaScript} "$tmp" "$sha" mkdir -p "$dest" tar -xzf "$tmp" -C "$dest" --strip-components=1 package diff --git a/script/nix/optional-metadata.ts b/script/nix/optional-metadata.ts new file mode 100644 index 00000000000..c34d0b59528 --- /dev/null +++ b/script/nix/optional-metadata.ts @@ -0,0 +1,40 @@ +import path from "path" + +const argv = Bun.argv.slice(2) +const arg = argv[0] ?? "optional-packages.txt" +const root = process.cwd() +const lock = path.join(root, "bun.lock") +const file = path.isAbsolute(arg) ? arg : path.join(root, arg) + +const mask = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + +const doc = await Bun.file(lock).text() +const text = await Bun.file(file).text() + +const names = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + +if (names.length === 0) { + process.exit(0) +} + +const lines = names.map((name) => { + const safe = mask(name) + const verMatch = doc.match(new RegExp(`"${safe}": "([^"]+)"`)) + if (!verMatch) { + console.error(`missing-version\t${name}`) + process.exit(1) + } + const ver = verMatch[1] + const verSafe = mask(ver) + const shaHit = doc.match(new RegExp(`"${safe}@${verSafe}"[^\\n]*"(sha512-[^"]+)"`)) + if (!shaHit) { + console.error(`missing-sha\t${name}\t${ver}`) + process.exit(1) + } + return `${name}\t${ver}\t${shaHit[1]}` +}) + +await Bun.write(Bun.stdout, lines.join("\n")) diff --git a/script/nix/verify-sha.ts b/script/nix/verify-sha.ts new file mode 100644 index 00000000000..a8d46dfae3d --- /dev/null +++ b/script/nix/verify-sha.ts @@ -0,0 +1,19 @@ +const argv = Bun.argv.slice(2) +if (argv.length < 2) { + console.error("usage: verify-sha ") + process.exit(1) +} + +const file = argv[0] +const raw = argv[1] +const want = raw.startsWith("sha512-") ? raw.slice(7) : raw + +const data = await Bun.file(file).arrayBuffer() +const sha = new Bun.SHA512() +sha.update(data) +const digest = sha.digest("base64") + +if (digest !== want) { + console.error(`hash mismatch: expected ${want}, got ${digest}`) + process.exit(1) +} From ad6ff434f3a39202a1bd97590ce267d23095b76c Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 5 Nov 2025 23:16:14 +1100 Subject: [PATCH 10/72] nix flake: completed modularization refactor --- .github/workflows/update-nix-hashes.yml | 12 +- flake.nix | 206 +----------------- nix/models-dev.nix | 18 ++ nix/node-modules.nix | 103 +++++++++ nix/opencode.nix | 96 ++++++++ {script/nix => nix/scripts}/bun-build.ts | 0 .../scripts}/canonicalize-node-modules.ts | 0 .../nix => nix/scripts}/optional-metadata.ts | 4 +- {script/nix => nix/scripts}/update-hashes | 17 +- {script/nix => nix/scripts}/verify-sha.ts | 0 10 files changed, 247 insertions(+), 209 deletions(-) create mode 100644 nix/models-dev.nix create mode 100644 nix/node-modules.nix create mode 100644 nix/opencode.nix rename {script/nix => nix/scripts}/bun-build.ts (100%) rename {script/nix => nix/scripts}/canonicalize-node-modules.ts (100%) rename {script/nix => nix/scripts}/optional-metadata.ts (92%) rename {script/nix => nix/scripts}/update-hashes (73%) rename {script/nix => nix/scripts}/verify-sha.ts (100%) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 247060f7cf9..db4ea5d92be 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -31,7 +31,7 @@ jobs: - name: Update build output hashes run: | set -euo pipefail - script/nix/update-hashes + nix/scripts/update-hashes - name: Verification build (x86_64-linux) run: | @@ -45,18 +45,18 @@ jobs: nix build .#packages.aarch64-linux.default --system aarch64-linux -L echo "aarch64-linux build successful." - - name: Commit flake.nix changes + - name: Commit Nix hash changes run: | set -euo pipefail - if git diff --quiet flake.nix; then - echo "No changes to flake.nix. Hash is already up to date." + if git diff --quiet flake.nix nix/node-modules.nix nix/models-dev.nix; then + echo "No changes to tracked Nix files. Hashes are already up to date." exit 0 fi VERSION=$(jq -r '.version' packages/opencode/package.json) - git add flake.nix - git commit -m "nix: update output hashes for v$VERSION" + git add flake.nix nix/node-modules.nix nix/models-dev.nix + git commit -m "nix: update hashes for v$VERSION" git pull --rebase origin ${GITHUB_REF_NAME} git push origin HEAD:${GITHUB_REF_NAME} diff --git a/flake.nix b/flake.nix index 2f7e740243e..8877813e51c 100644 --- a/flake.nix +++ b/flake.nix @@ -23,28 +23,13 @@ "aarch64-linux" = "bun-linux-arm64"; "x86_64-linux" = "bun-linux-x64"; }; + scripts = ./nix/scripts; modelsDev = forEachSystem ( system: let pkgs = pkgsFor system; in - pkgs.stdenvNoCC.mkDerivation { - pname = "models-dev"; - version = "unstable"; - - src = pkgs.fetchurl { - url = "https://models.dev/api.json"; - hash = "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg="; - }; - - dontUnpack = true; - dontBuild = true; - - installPhase = '' - mkdir -p $out/dist - cp $src $out/dist/_api.json - ''; - } + pkgs.callPackage ./nix/models-dev.nix { } ); in { @@ -70,185 +55,18 @@ system: let pkgs = pkgsFor system; + mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { }; + mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in { - default = pkgs.callPackage ( - { - lib, - stdenv, - stdenvNoCC, - bun, - makeBinaryWrapper, - }: - stdenvNoCC.mkDerivation (finalAttrs: { - pname = "opencode"; - version = packageJson.version; - - src = ./.; - - node_modules = - let - canonicalizeScript = ./script/nix/canonicalize-node-modules.ts; - optionalMetadataScript = ./script/nix/optional-metadata.ts; - verifyShaScript = ./script/nix/verify-sha.ts; - in - stdenvNoCC.mkDerivation { - pname = "opencode-node_modules"; - inherit (finalAttrs) version src; - - impureEnvVars = - lib.fetchers.proxyImpureEnvVars - ++ [ - "GIT_PROXY_COMMAND" - "SOCKS_SERVER" - ]; - - nativeBuildInputs = [ bun pkgs.cacert pkgs.curl ]; - - dontConfigure = true; - - buildPhase = '' - runHook preBuild - export HOME=$(mktemp -d) - export BUN_INSTALL_CACHE_DIR=$(mktemp -d) - bun install \ - --frozen-lockfile \ - --ignore-scripts \ - --no-progress - - cat > optional-packages.txt <<'EOF' -@parcel/watcher-linux-arm64-glibc -@opentui/core-linux-arm64 -EOF - - bun --bun ${optionalMetadataScript} optional-packages.txt > optional-metadata.txt - - while IFS=$'\t' read -r name version sha; do - [ -z "$name" ] && continue - scope="''${name%%/*}" - remainder="''${name#*/}" - if [ "$scope" = "$name" ]; then - scope="" - remainder="$name" - fi - - base="''${remainder##*/}" - encoded_scope="''${scope//@/%40}" - - url="https://registry.npmjs.org/''${remainder}/-/''${base}-''${version}.tgz" - dest="node_modules/''${remainder}" - if [ -n "$scope" ]; then - url="https://registry.npmjs.org/''${encoded_scope}/''${remainder}/-/''${base}-''${version}.tgz" - dest="node_modules/''${scope}/''${remainder}" - fi - - tmp=$(mktemp) - curl --fail --location --silent --show-error --tlsv1.2 "$url" -o "$tmp" - bun --bun ${verifyShaScript} "$tmp" "$sha" - - mkdir -p "$dest" - tar -xzf "$tmp" -C "$dest" --strip-components=1 package - rm -f "$tmp" - done < optional-metadata.txt - - rm -f optional-packages.txt optional-metadata.txt - - bun --bun ${canonicalizeScript} - - 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) - runHook postInstall - ''; - - dontFixup = true; - - outputHashAlgo = "sha256"; - outputHashMode = "recursive"; - outputHash = "sha256-s/UTz8BTYDOZpF9P6nZr0b7fNOS7Nv7hUfpihJgsSqE="; - }; - - nativeBuildInputs = [ - bun - makeBinaryWrapper - ]; - - configurePhase = '' - runHook preConfigure - cp -R ${finalAttrs.node_modules}/. . - runHook postConfigure - ''; - - env.MODELS_DEV_API_JSON = "${modelsDev.${system}}/dist/_api.json"; - - buildPhase = '' - runHook preBuild - - cp ${./script/nix/bun-build.ts} bun-build.ts - - substituteInPlace bun-build.ts \ - --replace '@VERSION@' "${finalAttrs.version}" - - export BUN_COMPILE_TARGET=${bunTarget.${stdenvNoCC.hostPlatform.system}} - bun --bun bun-build.ts - - runHook postBuild - ''; - - dontStrip = true; - - installPhase = '' - runHook preInstall - - # The binary is created in the package directory after chdir - cd packages/opencode - if [ ! -f opencode ]; then - echo "ERROR: opencode binary not found in $(pwd)" - ls -la - exit 1 - fi - if [ ! -f opencode-worker.js ]; then - echo "ERROR: opencode worker bundle not found in $(pwd)" - ls -la - exit 1 - fi - - install -Dm755 opencode $out/bin/opencode - install -Dm644 opencode-worker.js $out/bin/opencode-worker.js - runHook postInstall - ''; - - postFixup = lib.optionalString stdenvNoCC.hostPlatform.isLinux '' - wrapProgram $out/bin/opencode \ - --set LD_LIBRARY_PATH "${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}" - ''; - - 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/sst/opencode"; - license = lib.licenses.mit; - platforms = [ - "aarch64-linux" - "x86_64-linux" - ]; - mainProgram = "opencode"; - }; - }) - ) { }; + default = mkPackage { + version = packageJson.version; + src = ./.; + scripts = scripts; + target = bunTarget.${system}; + modelsDev = "${modelsDev.${system}}/dist/_api.json"; + mkNodeModules = mkNodeModules; + }; } ); diff --git a/nix/models-dev.nix b/nix/models-dev.nix new file mode 100644 index 00000000000..451f8dbe026 --- /dev/null +++ b/nix/models-dev.nix @@ -0,0 +1,18 @@ +{ stdenvNoCC, fetchurl }: +stdenvNoCC.mkDerivation { + pname = "models-dev"; + version = "unstable"; + + src = fetchurl { + url = "https://models.dev/api.json"; + hash = "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg="; + }; + + dontUnpack = true; + dontBuild = true; + + installPhase = '' + mkdir -p $out/dist + cp $src $out/dist/_api.json + ''; +} diff --git a/nix/node-modules.nix b/nix/node-modules.nix new file mode 100644 index 00000000000..3bbe58aa1aa --- /dev/null +++ b/nix/node-modules.nix @@ -0,0 +1,103 @@ +{ lib, stdenvNoCC, bun, cacert, curl }: +args: +stdenvNoCC.mkDerivation { + pname = "opencode-node_modules"; + version = args.version; + src = args.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 \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress + + cat > optional-packages.txt <<'EOF' +@parcel/watcher-linux-arm64-glibc +@opentui/core-linux-arm64 +EOF + + bun --bun ${args.optionalMetadataScript} optional-packages.txt > optional-metadata.txt + + echo "Optional package metadata:" + cat optional-metadata.txt + while IFS=$'\t' read -r name version sha; do + [ -z "$name" ] && continue + scope="''${name%%/*}" + remainder="''${name#*/}" + if [ "$scope" = "$name" ]; then + scope="" + remainder="$name" + fi + + base="''${remainder##*/}" + encoded_scope="''${scope//@/%40}" + + url="https://registry.npmjs.org/''${remainder}/-/''${base}-''${version}.tgz" + dest="node_modules/''${remainder}" + if [ -n "$scope" ]; then + url="https://registry.npmjs.org/''${encoded_scope}/''${remainder}/-/''${base}-''${version}.tgz" + dest="node_modules/''${scope}/''${remainder}" + fi + + tmp=$(mktemp) + curl --fail --location --silent --show-error --tlsv1.2 "$url" -o "$tmp" + bun --bun ${args.verifyShaScript} "$tmp" "$sha" + + mkdir -p "$dest" + tar -xzf "$tmp" -C "$dest" --strip-components=1 package + rm -f "$tmp" + + echo "Installed optional package $name -> $dest" + + for ws in packages/*; do + [ -d "$ws/node_modules" ] || continue + ws_dest="$ws/node_modules/$remainder" + if [ -n "$scope" ]; then + ws_dest="$ws/node_modules/$scope/$remainder" + fi + mkdir -p "$(dirname "$ws_dest")" + rm -rf "$ws_dest" + target="$(realpath --relative-to="$(dirname "$ws_dest")" "$dest")" + ln -s "$target" "$ws_dest" + done + done < optional-metadata.txt + + rm -f optional-packages.txt optional-metadata.txt + + bun --bun ${args.canonicalizeScript} + + 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) + runHook postInstall + ''; + + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = "sha256-eIHqmHsGAlBMrjbitwDQ6tMT+CZZIC9TGwNinQBksx8="; +} diff --git a/nix/opencode.nix b/nix/opencode.nix new file mode 100644 index 00000000000..b6148c0b3d7 --- /dev/null +++ b/nix/opencode.nix @@ -0,0 +1,96 @@ +{ lib, stdenv, stdenvNoCC, bun, makeBinaryWrapper }: +args: +let + scripts = args.scripts; + mkModules = + attrs: + args.mkNodeModules ( + attrs + // { + canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; + optionalMetadataScript = scripts + "/optional-metadata.ts"; + verifyShaScript = scripts + "/verify-sha.ts"; + } + ); +in +stdenvNoCC.mkDerivation (finalAttrs: { + pname = "opencode"; + version = args.version; + + src = args.src; + + node_modules = mkModules { + version = finalAttrs.version; + src = finalAttrs.src; + }; + + nativeBuildInputs = [ + bun + makeBinaryWrapper + ]; + + configurePhase = '' + runHook preConfigure + cp -R ${finalAttrs.node_modules}/. . + runHook postConfigure + ''; + + env.MODELS_DEV_API_JSON = args.modelsDev; + + buildPhase = '' + runHook preBuild + + cp ${scripts + "/bun-build.ts"} bun-build.ts + + substituteInPlace bun-build.ts \ + --replace '@VERSION@' "${finalAttrs.version}" + + export BUN_COMPILE_TARGET=${args.target} + bun --bun bun-build.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 + exit 1 + fi + if [ ! -f opencode-worker.js ]; then + echo "ERROR: opencode worker bundle not found in $(pwd)" + ls -la + exit 1 + fi + + install -Dm755 opencode $out/bin/opencode + install -Dm644 opencode-worker.js $out/bin/opencode-worker.js + runHook postInstall + ''; + + postFixup = lib.optionalString stdenvNoCC.hostPlatform.isLinux '' + wrapProgram $out/bin/opencode \ + --set LD_LIBRARY_PATH "${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}" + ''; + + 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/sst/opencode"; + license = lib.licenses.mit; + platforms = [ + "aarch64-linux" + "x86_64-linux" + ]; + mainProgram = "opencode"; + }; +}) diff --git a/script/nix/bun-build.ts b/nix/scripts/bun-build.ts similarity index 100% rename from script/nix/bun-build.ts rename to nix/scripts/bun-build.ts diff --git a/script/nix/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts similarity index 100% rename from script/nix/canonicalize-node-modules.ts rename to nix/scripts/canonicalize-node-modules.ts diff --git a/script/nix/optional-metadata.ts b/nix/scripts/optional-metadata.ts similarity index 92% rename from script/nix/optional-metadata.ts rename to nix/scripts/optional-metadata.ts index c34d0b59528..67a5bc47986 100644 --- a/script/nix/optional-metadata.ts +++ b/nix/scripts/optional-metadata.ts @@ -37,4 +37,6 @@ const lines = names.map((name) => { return `${name}\t${ver}\t${shaHit[1]}` }) -await Bun.write(Bun.stdout, lines.join("\n")) +if (lines.length > 0) { + await Bun.write(Bun.stdout, lines.join("\n") + "\n") +} diff --git a/script/nix/update-hashes b/nix/scripts/update-hashes similarity index 73% rename from script/nix/update-hashes rename to nix/scripts/update-hashes index 52f34edb0dc..3be019fb097 100755 --- a/script/nix/update-hashes +++ b/nix/scripts/update-hashes @@ -2,11 +2,12 @@ set -euo pipefail -FILE=${FILE:-flake.nix} SYSTEM=${SYSTEM:-x86_64-linux} TARGET="packages.${SYSTEM}.default" DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" MODELS_URL=${MODELS_URL:-https://models.dev/api.json} +MODULES_FILE=${MODULES_FILE:-nix/node-modules.nix} +MODELS_FILE=${MODELS_FILE:-nix/models-dev.nix} export DUMMY cleanup() { @@ -21,9 +22,9 @@ cleanup() { trap cleanup EXIT echo "Setting dummy node_modules outputHash..." -perl -0pi -e 's/(node_modules\s*=\s*(?:let.*?in\s*)?stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/s' "$FILE" +perl -0pi -e 's/(outputHash\s*=\s*")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/' "$MODULES_FILE" -if ! grep -q "$DUMMY" "$FILE"; then +if ! grep -q "$DUMMY" "$MODULES_FILE"; then echo "Failed to set dummy hash placeholder" exit 1 fi @@ -36,12 +37,12 @@ if [ -z "$MODELS_HASH" ]; then exit 1 fi -current_models_hash="$(perl -0ne 'print $1 if /modelsDev\s*=\s*forEachSystem\s*\(.*?hash\s*=\s*"(sha256-[^"]+)"/s' "$FILE")" +current_models_hash="$(perl -0ne 'print $1 if /hash\s*=\s*"(sha256-[^"]+)";/' "$MODELS_FILE")" if [ "$MODELS_HASH" != "$current_models_hash" ]; then export MODELS_HASH - perl -0pi -e 's/(modelsDev\s*=\s*forEachSystem\s*\(.*?hash\s*=\s*")sha256-[^"]+(")/${1}$ENV{MODELS_HASH}${2}/s' "$FILE" - if ! grep -q "$MODELS_HASH" "$FILE"; then + perl -0pi -e 's/(hash\s*=\s*")sha256-[^"]+(")/${1}$ENV{MODELS_HASH}${2}/' "$MODELS_FILE" + if ! grep -q "$MODELS_HASH" "$MODELS_FILE"; then echo "Failed to update models-dev hash" exit 1 fi @@ -82,9 +83,9 @@ fi export CORRECT_HASH -perl -0pi -e 's/(node_modules\s*=\s*(?:let.*?in\s*)?stdenvNoCC\.mkDerivation\s*\{.*?outputHash = ")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/s' "$FILE" +perl -0pi -e 's/(outputHash\s*=\s*")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/' "$MODULES_FILE" -if ! grep -q "$CORRECT_HASH" "$FILE"; then +if ! grep -q "$CORRECT_HASH" "$MODULES_FILE"; then echo "Failed to update flake with new hash" exit 1 fi diff --git a/script/nix/verify-sha.ts b/nix/scripts/verify-sha.ts similarity index 100% rename from script/nix/verify-sha.ts rename to nix/scripts/verify-sha.ts From e7d548532cf88536cb42a9195b6159f54012ef24 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 10:37:13 +1100 Subject: [PATCH 11/72] nix flake: fix/support aarch64 --- .github/workflows/update-nix-hashes.yml | 6 +- flake.nix | 12 ++- nix/node-modules-hashes.json | 4 + nix/node-modules.nix | 4 +- nix/scripts/update-hashes | 117 ++++++++++++++---------- 5 files changed, 86 insertions(+), 57 deletions(-) create mode 100644 nix/node-modules-hashes.json diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index db4ea5d92be..9b9efeddb8f 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -29,6 +29,8 @@ jobs: git config --global user.name "opencode" - name: Update build output hashes + env: + SYSTEMS: "aarch64-linux x86_64-linux" run: | set -euo pipefail nix/scripts/update-hashes @@ -49,13 +51,13 @@ jobs: run: | set -euo pipefail - if git diff --quiet flake.nix nix/node-modules.nix nix/models-dev.nix; then + if git diff --quiet flake.nix nix/node-modules.nix nix/node-modules-hashes.json nix/models-dev.nix; then echo "No changes to tracked Nix files. Hashes are already up to date." exit 0 fi VERSION=$(jq -r '.version' packages/opencode/package.json) - git add flake.nix nix/node-modules.nix nix/models-dev.nix + git add flake.nix nix/node-modules.nix nix/node-modules-hashes.json nix/models-dev.nix git commit -m "nix: update hashes for v$VERSION" git pull --rebase origin ${GITHUB_REF_NAME} diff --git a/flake.nix b/flake.nix index 8877813e51c..07b84fc2765 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,7 @@ "x86_64-linux" = "bun-linux-x64"; }; scripts = ./nix/scripts; + nodeModulesHashes = builtins.fromJSON (builtins.readFile ./nix/node-modules-hashes.json); modelsDev = forEachSystem ( system: let @@ -55,7 +56,7 @@ system: let pkgs = pkgsFor system; - mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { }; + mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hashes = nodeModulesHashes; }; mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in { @@ -78,13 +79,16 @@ { opencode-dev = { type = "app"; - program = pkgs.writeShellApplication { + meta = { + description = "Nix devshell shell for OpenCode"; + runtimeInputs = [ pkgs.bun ]; + }; + program = "${pkgs.writeShellApplication { name = "opencode-dev"; - runtimeInputs = [ pkgs.bun ]; text = '' exec bun run dev "$@" ''; - }; + }}/bin/opencode-dev"; }; } ); diff --git a/nix/node-modules-hashes.json b/nix/node-modules-hashes.json new file mode 100644 index 00000000000..11842b7d30d --- /dev/null +++ b/nix/node-modules-hashes.json @@ -0,0 +1,4 @@ +{ + "aarch64-linux": "sha256-eIHqmHsGAlBMrjbitwDQ6tMT+CZZIC9TGwNinQBksx8=", + "x86_64-linux": "sha256-eIHqmHsGAlBMrjbitwDQ6tMT+CZZIC9TGwNinQBksx8=" +} diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 3bbe58aa1aa..eaeae3aea6d 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,4 +1,4 @@ -{ lib, stdenvNoCC, bun, cacert, curl }: +{ hashes, lib, stdenvNoCC, bun, cacert, curl }: args: stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; @@ -99,5 +99,5 @@ EOF outputHashAlgo = "sha256"; outputHashMode = "recursive"; - outputHash = "sha256-eIHqmHsGAlBMrjbitwDQ6tMT+CZZIC9TGwNinQBksx8="; + outputHash = hashes.${stdenvNoCC.hostPlatform.system}; } diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes index 3be019fb097..4cce927c876 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes @@ -2,33 +2,25 @@ set -euo pipefail -SYSTEM=${SYSTEM:-x86_64-linux} -TARGET="packages.${SYSTEM}.default" DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" MODELS_URL=${MODELS_URL:-https://models.dev/api.json} -MODULES_FILE=${MODULES_FILE:-nix/node-modules.nix} MODELS_FILE=${MODELS_FILE:-nix/models-dev.nix} +HASH_FILE=${MODULES_HASH_FILE:-nix/node-modules-hashes.json} export DUMMY -cleanup() { - if [ -n "${JSON_OUTPUT:-}" ] && [ -f "$JSON_OUTPUT" ]; then - rm -f "$JSON_OUTPUT" - fi - if [ -n "${BUILD_LOG:-}" ] && [ -f "$BUILD_LOG" ]; then - rm -f "$BUILD_LOG" - fi -} - -trap cleanup EXIT - -echo "Setting dummy node_modules outputHash..." -perl -0pi -e 's/(outputHash\s*=\s*")sha256-[^"]+(";\n)/${1}$ENV{DUMMY}${2}/' "$MODULES_FILE" +if [ -z "${SYSTEMS:-}" ]; then + SYSTEMS="$(nix eval --json .#packages | jq -r 'keys[]')" +fi -if ! grep -q "$DUMMY" "$MODULES_FILE"; then - echo "Failed to set dummy hash placeholder" +if [ -z "$SYSTEMS" ]; then + echo "No target systems detected for hash update" exit 1 fi +if [ ! -f "$HASH_FILE" ]; then + echo "{}" >"$HASH_FILE" +fi + echo "Refreshing models-dev hash..." MODELS_HASH=$(nix store prefetch-file "$MODELS_URL" --json | jq -r '.hash // empty') @@ -53,41 +45,68 @@ if [ "$MODELS_HASH" = "$current_models_hash" ]; then echo "models-dev hash already up to date: $MODELS_HASH" fi -JSON_OUTPUT=$(mktemp) -BUILD_LOG=$(mktemp) +cleanup() { + if [ -n "${JSON_OUTPUT:-}" ] && [ -f "$JSON_OUTPUT" ]; then + rm -f "$JSON_OUTPUT" + fi + if [ -n "${BUILD_LOG:-}" ] && [ -f "$BUILD_LOG" ]; then + rm -f "$BUILD_LOG" + fi +} -echo "Building ${TARGET} to discover correct node_modules outputHash..." -if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --json >"$JSON_OUTPUT" 2>"$BUILD_LOG"; then - echo "Build succeeded while dummy hash was set; aborting" - exit 1 -fi +trap cleanup EXIT -CORRECT_HASH="$(jq -r ' - map( - select(.error.actualHash? != null) - | .error.actualHash - ) - | map(select(. != env.DUMMY)) - | .[0] // "" -' "$JSON_OUTPUT")" - -if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -o 'sha256-[A-Za-z0-9+/=]\+' "$BUILD_LOG" | grep -v "$DUMMY" | head -n1 || true)" -fi +write_hash() { + local value="$1" + local temp + temp=$(mktemp) + jq --arg system "$SYSTEM" --arg value "$value" '.[$system] = $value' "$HASH_FILE" >"$temp" + mv "$temp" "$HASH_FILE" +} -if [ -z "$CORRECT_HASH" ]; then - echo "Failed to extract node_modules hash" - tail -100 "$BUILD_LOG" || true - exit 1 -fi +for SYSTEM in $SYSTEMS; do + TARGET="packages.${SYSTEM}.default" -export CORRECT_HASH + echo "Setting dummy node_modules outputHash for ${SYSTEM}..." + write_hash "$DUMMY" -perl -0pi -e 's/(outputHash\s*=\s*")sha256-[^"]+(";\n)/${1}$ENV{CORRECT_HASH}${2}/' "$MODULES_FILE" + JSON_OUTPUT=$(mktemp) + BUILD_LOG=$(mktemp) -if ! grep -q "$CORRECT_HASH" "$MODULES_FILE"; then - echo "Failed to update flake with new hash" - exit 1 -fi + echo "Building ${TARGET} to discover correct node_modules outputHash..." + if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --json >"$JSON_OUTPUT" 2>"$BUILD_LOG"; then + echo "Build succeeded while dummy hash was set; aborting" + exit 1 + fi + + CORRECT_HASH="$(jq -r ' + map( + select(.error.actualHash? != null) + | .error.actualHash + ) + | map(select(. != env.DUMMY)) + | .[0] // "" + ' "$JSON_OUTPUT")" + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -o 'sha256-[A-Za-z0-9+/=]\+' "$BUILD_LOG" | grep -v "$DUMMY" | head -n1 || true)" + fi + + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to extract node_modules hash for ${SYSTEM}" + tail -100 "$BUILD_LOG" || true + exit 1 + fi + + write_hash "$CORRECT_HASH" + + if ! jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.[ $system ] == $hash' "$HASH_FILE" >/dev/null; then + echo "Failed to persist node_modules hash for ${SYSTEM}" + exit 1 + fi + + echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" -echo "node_modules hash updated: $CORRECT_HASH" + rm -f "$JSON_OUTPUT" "$BUILD_LOG" + unset JSON_OUTPUT BUILD_LOG +done From ca804a1bc7f7f6987626195dc33ed0305e9c2b76 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 11:33:19 +1100 Subject: [PATCH 12/72] nix flake: refactor hash collection to unified json file --- .github/workflows/update-nix-hashes.yml | 4 +-- flake.nix | 6 ++--- nix/hashes.json | 9 +++++++ nix/models-dev.nix | 4 +-- nix/node-modules-hashes.json | 4 --- nix/node-modules.nix | 4 +-- nix/scripts/update-hashes | 36 ++++++++++++------------- 7 files changed, 36 insertions(+), 31 deletions(-) create mode 100644 nix/hashes.json delete mode 100644 nix/node-modules-hashes.json diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 9b9efeddb8f..ee096b25527 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -51,13 +51,13 @@ jobs: run: | set -euo pipefail - if git diff --quiet flake.nix nix/node-modules.nix nix/node-modules-hashes.json nix/models-dev.nix; then + if git diff --quiet flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix; then echo "No changes to tracked Nix files. Hashes are already up to date." exit 0 fi VERSION=$(jq -r '.version' packages/opencode/package.json) - git add flake.nix nix/node-modules.nix nix/node-modules-hashes.json nix/models-dev.nix + git add flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix git commit -m "nix: update hashes for v$VERSION" git pull --rebase origin ${GITHUB_REF_NAME} diff --git a/flake.nix b/flake.nix index 07b84fc2765..7b0792a3836 100644 --- a/flake.nix +++ b/flake.nix @@ -24,13 +24,13 @@ "x86_64-linux" = "bun-linux-x64"; }; scripts = ./nix/scripts; - nodeModulesHashes = builtins.fromJSON (builtins.readFile ./nix/node-modules-hashes.json); + hashes = builtins.fromJSON (builtins.readFile ./nix/hashes.json); modelsDev = forEachSystem ( system: let pkgs = pkgsFor system; in - pkgs.callPackage ./nix/models-dev.nix { } + pkgs.callPackage ./nix/models-dev.nix { hash = hashes.models.dev; } ); in { @@ -56,7 +56,7 @@ system: let pkgs = pkgsFor system; - mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hashes = nodeModulesHashes; }; + mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hash = hashes.nodeModules.${system}; }; mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in { diff --git a/nix/hashes.json b/nix/hashes.json new file mode 100644 index 00000000000..1e0d2113bb0 --- /dev/null +++ b/nix/hashes.json @@ -0,0 +1,9 @@ +{ + "models": { + "dev": "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg=" + }, + "nodeModules": { + "aarch64-linux": "sha256-e4NmkdKlx8Rlwi1u+GYrzzh93xvZ4jbHdzAl4OLnHtQ=", + "x86_64-linux": "sha256-8goN/fR8mjiJj/ZzfgP4ES2sW2Ljoyl+SNthqNj6Dr8=" + } +} diff --git a/nix/models-dev.nix b/nix/models-dev.nix index 451f8dbe026..baacb05e29f 100644 --- a/nix/models-dev.nix +++ b/nix/models-dev.nix @@ -1,11 +1,11 @@ -{ stdenvNoCC, fetchurl }: +{ hash, stdenvNoCC, fetchurl }: stdenvNoCC.mkDerivation { pname = "models-dev"; version = "unstable"; src = fetchurl { url = "https://models.dev/api.json"; - hash = "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg="; + hash = hash; }; dontUnpack = true; diff --git a/nix/node-modules-hashes.json b/nix/node-modules-hashes.json deleted file mode 100644 index 11842b7d30d..00000000000 --- a/nix/node-modules-hashes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "aarch64-linux": "sha256-eIHqmHsGAlBMrjbitwDQ6tMT+CZZIC9TGwNinQBksx8=", - "x86_64-linux": "sha256-eIHqmHsGAlBMrjbitwDQ6tMT+CZZIC9TGwNinQBksx8=" -} diff --git a/nix/node-modules.nix b/nix/node-modules.nix index eaeae3aea6d..2a48a122855 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,4 +1,4 @@ -{ hashes, lib, stdenvNoCC, bun, cacert, curl }: +{ hash, lib, stdenvNoCC, bun, cacert, curl }: args: stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; @@ -99,5 +99,5 @@ EOF outputHashAlgo = "sha256"; outputHashMode = "recursive"; - outputHash = hashes.${stdenvNoCC.hostPlatform.system}; + outputHash = hash; } diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes index 4cce927c876..5763626c1bd 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes @@ -4,8 +4,8 @@ set -euo pipefail DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" MODELS_URL=${MODELS_URL:-https://models.dev/api.json} -MODELS_FILE=${MODELS_FILE:-nix/models-dev.nix} -HASH_FILE=${MODULES_HASH_FILE:-nix/node-modules-hashes.json} +DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} +HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} export DUMMY if [ -z "${SYSTEMS:-}" ]; then @@ -18,7 +18,12 @@ if [ -z "$SYSTEMS" ]; then fi if [ ! -f "$HASH_FILE" ]; then - echo "{}" >"$HASH_FILE" + cat <<'EOF' >"$HASH_FILE" +{ + "models": {}, + "nodeModules": {} +} +EOF fi echo "Refreshing models-dev hash..." @@ -29,19 +34,14 @@ if [ -z "$MODELS_HASH" ]; then exit 1 fi -current_models_hash="$(perl -0ne 'print $1 if /hash\s*=\s*"(sha256-[^"]+)";/' "$MODELS_FILE")" +current_models_hash="$(jq -r '.models.dev // empty' "$HASH_FILE")" if [ "$MODELS_HASH" != "$current_models_hash" ]; then - export MODELS_HASH - perl -0pi -e 's/(hash\s*=\s*")sha256-[^"]+(")/${1}$ENV{MODELS_HASH}${2}/' "$MODELS_FILE" - if ! grep -q "$MODELS_HASH" "$MODELS_FILE"; then - echo "Failed to update models-dev hash" - exit 1 - fi + tmp=$(mktemp) + jq --arg value "$MODELS_HASH" '.models.dev = $value' "$HASH_FILE" >"$tmp" + mv "$tmp" "$HASH_FILE" echo "models-dev hash updated: $MODELS_HASH" -fi - -if [ "$MODELS_HASH" = "$current_models_hash" ]; then +else echo "models-dev hash already up to date: $MODELS_HASH" fi @@ -56,11 +56,11 @@ cleanup() { trap cleanup EXIT -write_hash() { +write_node_modules_hash() { local value="$1" local temp temp=$(mktemp) - jq --arg system "$SYSTEM" --arg value "$value" '.[$system] = $value' "$HASH_FILE" >"$temp" + jq --arg system "$SYSTEM" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp" mv "$temp" "$HASH_FILE" } @@ -68,7 +68,7 @@ for SYSTEM in $SYSTEMS; do TARGET="packages.${SYSTEM}.default" echo "Setting dummy node_modules outputHash for ${SYSTEM}..." - write_hash "$DUMMY" + write_node_modules_hash "$DUMMY" JSON_OUTPUT=$(mktemp) BUILD_LOG=$(mktemp) @@ -98,9 +98,9 @@ for SYSTEM in $SYSTEMS; do exit 1 fi - write_hash "$CORRECT_HASH" + write_node_modules_hash "$CORRECT_HASH" - if ! jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.[ $system ] == $hash' "$HASH_FILE" >/dev/null; then + if ! jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null; then echo "Failed to persist node_modules hash for ${SYSTEM}" exit 1 fi From ae21ae3291fb21fdbff2b14334b79ed9c8ff4eb9 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 12:30:42 +1100 Subject: [PATCH 13/72] nix flake: ensure optional packages list is up-to-date --- nix/hashes.json | 10 ++++++++++ nix/scripts/optional-metadata.ts | 34 ++++++++++++++++++++++++++------ nix/scripts/update-hashes | 1 + 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 1e0d2113bb0..0b4aed0e003 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -5,5 +5,15 @@ "nodeModules": { "aarch64-linux": "sha256-e4NmkdKlx8Rlwi1u+GYrzzh93xvZ4jbHdzAl4OLnHtQ=", "x86_64-linux": "sha256-8goN/fR8mjiJj/ZzfgP4ES2sW2Ljoyl+SNthqNj6Dr8=" + }, + "optional": { + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "sha512": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" + }, + "@opentui/core-linux-arm64": { + "version": "0.1.33", + "sha512": "sha512-bBc1EdkVxsLBtqGjXM2BYpBJLa57ogcrSADSZbc5cQkPu0muSGzUwBbVnVZJUjWEfk6n5jcd4dDmLezVoQga0A==" + } } } diff --git a/nix/scripts/optional-metadata.ts b/nix/scripts/optional-metadata.ts index 67a5bc47986..16ce491bb47 100644 --- a/nix/scripts/optional-metadata.ts +++ b/nix/scripts/optional-metadata.ts @@ -10,6 +10,20 @@ const mask = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") const doc = await Bun.file(lock).text() const text = await Bun.file(file).text() +const hashesPath = path.join(root, "nix/hashes.json") +let optional: Record = {} + +try { + // Pre-seeded metadata keeps optional bundles reproducible without network calls. + const data = await Bun.file(hashesPath).text() + const parsed = JSON.parse(data ?? "{}") + if (parsed && typeof parsed.optional === "object" && parsed.optional !== null) { + optional = parsed.optional as typeof optional + } +} catch (error) { + console.error(`missing-hashes\t${hashesPath}\t${(error as Error).message}`) + process.exit(1) +} const names = text .split(/\r?\n/) @@ -20,22 +34,30 @@ if (names.length === 0) { process.exit(0) } -const lines = names.map((name) => { +const lines: string[] = [] + +for (const name of names) { const safe = mask(name) const verMatch = doc.match(new RegExp(`"${safe}": "([^"]+)"`)) - if (!verMatch) { + const stored = optional[name] ?? {} + const ver = stored.version ?? verMatch?.[1] + if (!ver) { console.error(`missing-version\t${name}`) process.exit(1) } - const ver = verMatch[1] + if (stored.version && verMatch && stored.version !== verMatch[1]) { + console.error(`version-mismatch\t${name}\t${stored.version}\t${verMatch[1]}`) + process.exit(1) + } const verSafe = mask(ver) const shaHit = doc.match(new RegExp(`"${safe}@${verSafe}"[^\\n]*"(sha512-[^"]+)"`)) - if (!shaHit) { + const sha = stored.sha512 ?? stored.sha ?? shaHit?.[1] + if (!sha) { console.error(`missing-sha\t${name}\t${ver}`) process.exit(1) } - return `${name}\t${ver}\t${shaHit[1]}` -}) + lines.push(`${name}\t${ver}\t${sha}`) +} if (lines.length > 0) { await Bun.write(Bun.stdout, lines.join("\n") + "\n") diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes index 5763626c1bd..f298f0f45d2 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes @@ -18,6 +18,7 @@ if [ -z "$SYSTEMS" ]; then fi if [ ! -f "$HASH_FILE" ]; then + # Keep structure stable so optional metadata survives repeated runs. cat <<'EOF' >"$HASH_FILE" { "models": {}, From 1bc9ef928962b6339b0b9dabce24f023b5a1aace Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 13:12:07 +1100 Subject: [PATCH 14/72] nix flake: rewrite of workflow to hopefully be more robust --- .github/workflows/update-nix-hashes.yml | 53 +++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index ee096b25527..6808bf5ec0f 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -1,4 +1,4 @@ -name: update nix hashes +name: Update Nix Hashes permissions: contents: write @@ -28,6 +28,37 @@ jobs: git config --global user.email "opencode@sst.dev" git config --global user.name "opencode" + - name: Preflight build (x86_64-linux) + id: preflight_x86 + continue-on-error: true + run: | + echo "Verifying existing hashes for x86_64-linux..." + nix build .#packages.x86_64-linux.default --system x86_64-linux -L + echo "x86_64-linux preflight build successful." + + - name: Preflight build (aarch64-linux) + id: preflight_aarch64 + continue-on-error: true + run: | + echo "Verifying existing hashes for aarch64-linux..." + nix build .#packages.aarch64-linux.default --system aarch64-linux -L + echo "aarch64-linux preflight build successful." + + - name: Summarize preflight status + id: preflight_status + run: | + set -e + if [ "${PRE_X86}" = "failure" ] || [ "${PRE_ARM}" = "failure" ]; then + echo "Preflight detected stale hashes." + echo "failed=true" >> "$GITHUB_OUTPUT" + else + echo "Preflight builds passed." + echo "failed=false" >> "$GITHUB_OUTPUT" + fi + env: + PRE_X86: ${{ steps.preflight_x86.outcome }} + PRE_ARM: ${{ steps.preflight_aarch64.outcome }} + - name: Update build output hashes env: SYSTEMS: "aarch64-linux x86_64-linux" @@ -35,15 +66,31 @@ jobs: set -euo pipefail nix/scripts/update-hashes + - name: Detect hash changes + id: hash_change + run: | + set -euo pipefail + if git diff --quiet nix/hashes.json; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No hash changes detected." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Hashes updated; running post-update verification." + fi + - name: Verification build (x86_64-linux) + if: steps.hash_change.outputs.changed == 'true' + || steps.preflight_status.outputs.failed == 'true' run: | - echo "Running final verification build for x86_64-linux..." + echo "Running post-update verification build for x86_64-linux..." nix build .#packages.x86_64-linux.default --system x86_64-linux -L echo "x86_64-linux build successful." - name: Verification build (aarch64-linux) + if: steps.hash_change.outputs.changed == 'true' + || steps.preflight_status.outputs.failed == 'true' run: | - echo "Running final verification build for aarch64-linux..." + echo "Running post-update verification build for aarch64-linux..." nix build .#packages.aarch64-linux.default --system aarch64-linux -L echo "aarch64-linux build successful." From 04cb545dddc5a81d0d9afc9ff353cca92700013b Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 13:31:46 +1100 Subject: [PATCH 15/72] nix flake: handle version changes of package deps --- flake.nix | 5 ++- nix/node-modules.nix | 7 ++-- nix/optional-packages.txt | 2 ++ nix/scripts/optional-metadata.ts | 11 +++--- nix/scripts/update-hashes | 59 +++++++++++++++++++++++++++----- 5 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 nix/optional-packages.txt diff --git a/flake.nix b/flake.nix index 7b0792a3836..df618ab7af9 100644 --- a/flake.nix +++ b/flake.nix @@ -56,7 +56,10 @@ system: let pkgs = pkgsFor system; - mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hash = hashes.nodeModules.${system}; }; + mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { + hash = hashes.nodeModules.${system}; + optionalPackagesFile = ./nix/optional-packages.txt; + }; mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in { diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 2a48a122855..79c32041f85 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,4 +1,4 @@ -{ hash, lib, stdenvNoCC, bun, cacert, curl }: +{ optionalPackagesFile, hash, lib, stdenvNoCC, bun, cacert, curl }: args: stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; @@ -25,10 +25,7 @@ stdenvNoCC.mkDerivation { --ignore-scripts \ --no-progress - cat > optional-packages.txt <<'EOF' -@parcel/watcher-linux-arm64-glibc -@opentui/core-linux-arm64 -EOF + cp ${optionalPackagesFile} optional-packages.txt bun --bun ${args.optionalMetadataScript} optional-packages.txt > optional-metadata.txt diff --git a/nix/optional-packages.txt b/nix/optional-packages.txt new file mode 100644 index 00000000000..f1940174875 --- /dev/null +++ b/nix/optional-packages.txt @@ -0,0 +1,2 @@ +@parcel/watcher-linux-arm64-glibc +@opentui/core-linux-arm64 diff --git a/nix/scripts/optional-metadata.ts b/nix/scripts/optional-metadata.ts index 16ce491bb47..a419feabcb6 100644 --- a/nix/scripts/optional-metadata.ts +++ b/nix/scripts/optional-metadata.ts @@ -39,19 +39,18 @@ const lines: string[] = [] for (const name of names) { const safe = mask(name) const verMatch = doc.match(new RegExp(`"${safe}": "([^"]+)"`)) - const stored = optional[name] ?? {} - const ver = stored.version ?? verMatch?.[1] + const store = optional[name] ?? {} + const ver = verMatch?.[1] ?? store.version if (!ver) { console.error(`missing-version\t${name}`) process.exit(1) } - if (stored.version && verMatch && stored.version !== verMatch[1]) { - console.error(`version-mismatch\t${name}\t${stored.version}\t${verMatch[1]}`) - process.exit(1) + if (store.version && verMatch && store.version !== verMatch[1]) { + console.warn(`version-mismatch\t${name}\t${store.version}\t${verMatch[1]}`) } const verSafe = mask(ver) const shaHit = doc.match(new RegExp(`"${safe}@${verSafe}"[^\\n]*"(sha512-[^"]+)"`)) - const sha = stored.sha512 ?? stored.sha ?? shaHit?.[1] + const sha = shaHit?.[1] ?? store.sha512 ?? store.sha if (!sha) { console.error(`missing-sha\t${name}\t${ver}`) process.exit(1) diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes index f298f0f45d2..2f2014b6fbf 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes @@ -6,7 +6,10 @@ DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" MODELS_URL=${MODELS_URL:-https://models.dev/api.json} DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} +OPTIONAL_FILE=${OPTIONAL_PACKAGES_FILE:-nix/optional-packages.txt} export DUMMY +export NIX_KEEP_OUTPUTS=1 +export NIX_KEEP_DERIVATIONS=1 if [ -z "${SYSTEMS:-}" ]; then SYSTEMS="$(nix eval --json .#packages | jq -r 'keys[]')" @@ -18,11 +21,11 @@ if [ -z "$SYSTEMS" ]; then fi if [ ! -f "$HASH_FILE" ]; then - # Keep structure stable so optional metadata survives repeated runs. cat <<'EOF' >"$HASH_FILE" { "models": {}, - "nodeModules": {} + "nodeModules": {}, + "optional": {} } EOF fi @@ -57,6 +60,39 @@ cleanup() { trap cleanup EXIT +update_optional() { + if [ ! -f "$OPTIONAL_FILE" ]; then + echo "optional package list missing at $OPTIONAL_FILE" >&2 + return 1 + fi + local meta tmp name ver sha + if command -v bun >/dev/null 2>&1; then + meta="$(bun --bun nix/scripts/optional-metadata.ts "$OPTIONAL_FILE")" || return 1 + fi + if [ -z "${meta:-}" ] && command -v nix >/dev/null 2>&1; then + meta="$(nix shell --quiet nixpkgs#bun -c bun --bun nix/scripts/optional-metadata.ts "$OPTIONAL_FILE")" || return 1 + fi + if [ -z "${meta:-}" ]; then + echo "bun unavailable; skipping optional metadata refresh" >&2 + return 0 + fi + while IFS=$'\t' read -r name ver sha; do + [ -n "$name" ] || continue + tmp=$(mktemp) + jq \ + --arg name "$name" \ + --arg ver "$ver" \ + --arg sha "$sha" \ + '.optional[$name] = { version: $ver, sha512: $sha }' \ + "$HASH_FILE" >"$tmp" + mv "$tmp" "$HASH_FILE" + done <"$JSON_OUTPUT" 2>"$BUILD_LOG"; then + if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --json --rebuild >"$JSON_OUTPUT" 2>"$BUILD_LOG"; then echo "Build succeeded while dummy hash was set; aborting" exit 1 fi @@ -89,14 +125,21 @@ for SYSTEM in $SYSTEMS; do | .[0] // "" ' "$JSON_OUTPUT")" - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -o 'sha256-[A-Za-z0-9+/=]\+' "$BUILD_LOG" | grep -v "$DUMMY" | head -n1 || true)" - fi - if [ -z "$CORRECT_HASH" ]; then echo "Failed to extract node_modules hash for ${SYSTEM}" tail -100 "$BUILD_LOG" || true - exit 1 + STORE_PATH="$(jq -r ' + map(select(.outputs.node_modules? != null)) + | map(select(.outputs.node_modules != env.DUMMY)) + | map(select(.outputs.node_modules != "")) + | .[0].outputs.node_modules // "" + ' "$JSON_OUTPUT")" + if [ -n "$STORE_PATH" ] && [ -e "$STORE_PATH" ]; then + CORRECT_HASH="$(nix hash-path --sri "$STORE_PATH" || true)" + fi + if [ -z "$CORRECT_HASH" ]; then + exit 1 + fi fi write_node_modules_hash "$CORRECT_HASH" From 5fe4051bc937b9b6e7eb6f0452ce790c76ebec30 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 15:24:25 +1100 Subject: [PATCH 16/72] nix flake: extra fallback to allow script to get actual hash using `nix-store --realise` --- nix/hashes.json | 6 +- nix/scripts/update-hashes | 124 ++++++++++++++++++++++++++++++-------- 2 files changed, 103 insertions(+), 27 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 0b4aed0e003..25538a46ac1 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -3,7 +3,7 @@ "dev": "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg=" }, "nodeModules": { - "aarch64-linux": "sha256-e4NmkdKlx8Rlwi1u+GYrzzh93xvZ4jbHdzAl4OLnHtQ=", + "aarch64-linux": "sha256-h/XhGcla9THbbqP3tTjyaN4jw/SdaGJ3vUXRy0jlo8E=", "x86_64-linux": "sha256-8goN/fR8mjiJj/ZzfgP4ES2sW2Ljoyl+SNthqNj6Dr8=" }, "optional": { @@ -12,8 +12,8 @@ "sha512": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" }, "@opentui/core-linux-arm64": { - "version": "0.1.33", - "sha512": "sha512-bBc1EdkVxsLBtqGjXM2BYpBJLa57ogcrSADSZbc5cQkPu0muSGzUwBbVnVZJUjWEfk6n5jcd4dDmLezVoQga0A==" + "version": "0.1.36", + "sha512": "sha512-ATR+vdtraZEC/gHR1mQa/NYPlqFNBpsnnJAGepQmcxm85VceLYM701QaaIgNAwyYXiP6RQN1ZCv06MD1Ph1m4w==" } } } diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes index 2f2014b6fbf..2d97e45df85 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes @@ -56,6 +56,9 @@ cleanup() { if [ -n "${BUILD_LOG:-}" ] && [ -f "$BUILD_LOG" ]; then rm -f "$BUILD_LOG" fi + if [ -n "${TMP_EXPR:-}" ] && [ -f "$TMP_EXPR" ]; then + rm -f "$TMP_EXPR" + fi } trap cleanup EXIT @@ -103,43 +106,116 @@ write_node_modules_hash() { for SYSTEM in $SYSTEMS; do TARGET="packages.${SYSTEM}.default" + MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" + + echo "Removing cached node_modules output for ${SYSTEM} (if present)..." + PREV_PATH="$(nix path-info "$MODULES_ATTR" --system "$SYSTEM" 2>/dev/null || true)" + if [ -n "$PREV_PATH" ]; then + nix store delete --ignore-liveness "$PREV_PATH" >/dev/null 2>&1 || true + fi + + DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" echo "Setting dummy node_modules outputHash for ${SYSTEM}..." write_node_modules_hash "$DUMMY" - JSON_OUTPUT=$(mktemp) BUILD_LOG=$(mktemp) + JSON_OUTPUT=$(mktemp) echo "Building ${TARGET} to discover correct node_modules outputHash..." - if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --json --rebuild >"$JSON_OUTPUT" 2>"$BUILD_LOG"; then + if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --rebuild -L --log-format raw --keep-failed --json 2>&1 | tee "$BUILD_LOG" >"$JSON_OUTPUT"; then echo "Build succeeded while dummy hash was set; aborting" exit 1 fi - CORRECT_HASH="$(jq -r ' - map( - select(.error.actualHash? != null) - | .error.actualHash - ) - | map(select(. != env.DUMMY)) - | .[0] // "" - ' "$JSON_OUTPUT")" + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + + if [ -z "$CORRECT_HASH" ]; then + echo "Attempting to read nix log for ${DRV_PATH}..." + CORRECT_HASH="$(nix log "$DRV_PATH" 2>/dev/null | grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' | awk '{print $2}' | head -n1 || true)" + fi + + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to find 'got:' line in build log; attempting alternative hash extraction..." + + # When a FOD has the wrong hash, modern Nix refuses to build at all with: + # "some outputs ... are not valid, so checking is not possible" + # + # Solution: Use nix-build (legacy) with the derivation path and --check disabled, + # which will actually build it and produce a "hash mismatch" error. + + echo "Using nix-build to force derivation build and capture hash mismatch..." + TMP_MODULES_LOG=$(mktemp) + + # Use nix-build with the derivation path directly + # --no-build-output suppresses some output, -K keeps failed builds + BUILD_OUT=$(nix-build "$DRV_PATH" --no-out-link --keep-failed --system "$SYSTEM" 2>&1 | tee "$TMP_MODULES_LOG" || true) + + # Check if build succeeded (hash was correct) + if echo "$BUILD_OUT" | grep -q "^/nix/store/"; then + # Build succeeded - hash the output + BUILD_PATH=$(echo "$BUILD_OUT" | grep "^/nix/store/" | head -n1) + if [ -d "$BUILD_PATH" ]; then + echo "Build succeeded with current hash, verifying output at: $BUILD_PATH" + CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) + if [ -n "$CORRECT_HASH" ]; then + echo "Hash verified from successful build: $CORRECT_HASH" + fi + fi + else + # Build failed - extract hash from error message + # Format: "error: hash mismatch in fixed-output derivation '/nix/store/...': + # specified: sha256-... + # got: sha256-..." + CORRECT_HASH="$(grep -A1 'hash mismatch' "$TMP_MODULES_LOG" | grep 'got:' | awk '{print $2}' | sed 's/:/-/' || true)" + + if [ -z "$CORRECT_HASH" ]; then + # Try alternate format without the 'hash mismatch' context + CORRECT_HASH="$(grep 'got:' "$TMP_MODULES_LOG" | awk '{print $2}' | head -n1 || true)" + fi + + if [ -z "$CORRECT_HASH" ]; then + # Try to find kept output + # Look for "note: keeping build directory '/...'" or similar + KEPT_LINE=$(grep -E "(keeping|kept).*build" "$TMP_MODULES_LOG" | head -n1 || true) + if [ -n "$KEPT_LINE" ]; then + # Extract path - format might be "note: keeping build directory '/tmp/nix-build-...'" + KEPT_DIR=$(echo "$KEPT_LINE" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) + + if [ -z "$KEPT_DIR" ]; then + # Try without quotes + KEPT_DIR=$(echo "$KEPT_LINE" | grep -oE '/tmp/nix-build-[^ ]+|/nix/var/nix/builds/[^ ]+' | head -n1) + fi + + if [ -d "$KEPT_DIR" ]; then + # Look for the output subdirectory + if [ -d "$KEPT_DIR/output" ]; then + KEPT_OUT="$KEPT_DIR/output" + elif [ -d "$KEPT_DIR" ]; then + KEPT_OUT="$KEPT_DIR" + fi + + if [ -n "$KEPT_OUT" ]; then + echo "Found kept build at: $KEPT_OUT" + CORRECT_HASH=$(nix hash path --sri "$KEPT_OUT" 2>/dev/null || true) + + if [ -n "$CORRECT_HASH" ]; then + echo "Extracted hash from kept output: $CORRECT_HASH" + fi + fi + fi + fi + fi + fi + + rm -f "$TMP_MODULES_LOG" + fi if [ -z "$CORRECT_HASH" ]; then echo "Failed to extract node_modules hash for ${SYSTEM}" + echo "Build log (last 100 lines):" tail -100 "$BUILD_LOG" || true - STORE_PATH="$(jq -r ' - map(select(.outputs.node_modules? != null)) - | map(select(.outputs.node_modules != env.DUMMY)) - | map(select(.outputs.node_modules != "")) - | .[0].outputs.node_modules // "" - ' "$JSON_OUTPUT")" - if [ -n "$STORE_PATH" ] && [ -e "$STORE_PATH" ]; then - CORRECT_HASH="$(nix hash-path --sri "$STORE_PATH" || true)" - fi - if [ -z "$CORRECT_HASH" ]; then - exit 1 - fi + exit 1 fi write_node_modules_hash "$CORRECT_HASH" @@ -151,6 +227,6 @@ for SYSTEM in $SYSTEMS; do echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" - rm -f "$JSON_OUTPUT" "$BUILD_LOG" - unset JSON_OUTPUT BUILD_LOG + rm -f "$BUILD_LOG" + unset BUILD_LOG done From 431873cd9b4ef1aba72ffa401ba6dce710f6d83e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 6 Nov 2025 16:33:28 +1100 Subject: [PATCH 17/72] nix flake: trigger workflow on package/bun.lock changes nix flake: cleanup --- .github/workflows/update-nix-hashes.yml | 28 ++++++++++++++++++++++--- nix/scripts/update-hashes | 21 +++---------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 6808bf5ec0f..3808bfe057b 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -5,6 +5,16 @@ permissions: on: workflow_dispatch: + push: + paths: + - 'bun.lock' + - 'package.json' + - 'packages/*/package.json' + pull_request: + paths: + - 'bun.lock' + - 'package.json' + - 'packages/*/package.json' jobs: verify: @@ -103,9 +113,21 @@ jobs: exit 0 fi + # Prevent infinite loop: check if the last commit was a hash update + LAST_COMMIT_MSG=$(git log -1 --pretty=%B) + if echo "$LAST_COMMIT_MSG" | grep -q "^nix: update hashes"; then + echo "Last commit was already a hash update. Skipping to prevent loop." + exit 0 + fi + VERSION=$(jq -r '.version' packages/opencode/package.json) git add flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix - git commit -m "nix: update hashes for v$VERSION" + git commit -m "nix: update hashes for v$VERSION" -m "[skip ci]" - git pull --rebase origin ${GITHUB_REF_NAME} - git push origin HEAD:${GITHUB_REF_NAME} + # For PRs, push to the head branch; for push events, push to current branch + if [ "${{ github.event_name }}" = "pull_request" ]; then + git push origin HEAD:${{ github.head_ref }} + else + git pull --rebase origin ${GITHUB_REF_NAME} + git push origin HEAD:${GITHUB_REF_NAME} + fi diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes index 2d97e45df85..d4c20be9a13 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes @@ -139,19 +139,14 @@ for SYSTEM in $SYSTEMS; do echo "Failed to find 'got:' line in build log; attempting alternative hash extraction..." # When a FOD has the wrong hash, modern Nix refuses to build at all with: - # "some outputs ... are not valid, so checking is not possible" - # - # Solution: Use nix-build (legacy) with the derivation path and --check disabled, - # which will actually build it and produce a "hash mismatch" error. + # "some outputs ... are not valid, so checking is not possible", so we use nix-build (legacy) + # with the derivation path and --check disabled, which will actually build and produce a hash mismatch. echo "Using nix-build to force derivation build and capture hash mismatch..." TMP_MODULES_LOG=$(mktemp) - # Use nix-build with the derivation path directly - # --no-build-output suppresses some output, -K keeps failed builds BUILD_OUT=$(nix-build "$DRV_PATH" --no-out-link --keep-failed --system "$SYSTEM" 2>&1 | tee "$TMP_MODULES_LOG" || true) - # Check if build succeeded (hash was correct) if echo "$BUILD_OUT" | grep -q "^/nix/store/"; then # Build succeeded - hash the output BUILD_PATH=$(echo "$BUILD_OUT" | grep "^/nix/store/" | head -n1) @@ -164,22 +159,13 @@ for SYSTEM in $SYSTEMS; do fi else # Build failed - extract hash from error message - # Format: "error: hash mismatch in fixed-output derivation '/nix/store/...': - # specified: sha256-... - # got: sha256-..." + # Not sure if there's a way around this other than parsing the human-readable log: CORRECT_HASH="$(grep -A1 'hash mismatch' "$TMP_MODULES_LOG" | grep 'got:' | awk '{print $2}' | sed 's/:/-/' || true)" - if [ -z "$CORRECT_HASH" ]; then - # Try alternate format without the 'hash mismatch' context - CORRECT_HASH="$(grep 'got:' "$TMP_MODULES_LOG" | awk '{print $2}' | head -n1 || true)" - fi - if [ -z "$CORRECT_HASH" ]; then # Try to find kept output - # Look for "note: keeping build directory '/...'" or similar KEPT_LINE=$(grep -E "(keeping|kept).*build" "$TMP_MODULES_LOG" | head -n1 || true) if [ -n "$KEPT_LINE" ]; then - # Extract path - format might be "note: keeping build directory '/tmp/nix-build-...'" KEPT_DIR=$(echo "$KEPT_LINE" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) if [ -z "$KEPT_DIR" ]; then @@ -188,7 +174,6 @@ for SYSTEM in $SYSTEMS; do fi if [ -d "$KEPT_DIR" ]; then - # Look for the output subdirectory if [ -d "$KEPT_DIR/output" ]; then KEPT_OUT="$KEPT_DIR/output" elif [ -d "$KEPT_DIR" ]; then From 3c04fe659b19308621c1cc5be9e6346e52f6e745 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 09:08:43 +1100 Subject: [PATCH 18/72] nix flake: initial darwin build support --- flake.nix | 4 ++++ nix/opencode.nix | 2 ++ 2 files changed, 6 insertions(+) diff --git a/flake.nix b/flake.nix index df618ab7af9..4c49ea64abb 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,8 @@ systems = [ "aarch64-linux" "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" ]; lib = nixpkgs.lib; forEachSystem = lib.genAttrs systems; @@ -22,6 +24,8 @@ bunTarget = { "aarch64-linux" = "bun-linux-arm64"; "x86_64-linux" = "bun-linux-x64"; + "aarch64-darwin" = "bun-darwin-arm64"; + "x86_64-darwin" = "bun-darwin-x64"; }; scripts = ./nix/scripts; hashes = builtins.fromJSON (builtins.readFile ./nix/hashes.json); diff --git a/nix/opencode.nix b/nix/opencode.nix index b6148c0b3d7..e835fb4d75f 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -90,6 +90,8 @@ stdenvNoCC.mkDerivation (finalAttrs: { platforms = [ "aarch64-linux" "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" ]; mainProgram = "opencode"; }; From 48a3c3115aaba2e095976966428ba836a09c937a Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 09:13:59 +1100 Subject: [PATCH 19/72] nix flake: workflow: use 4 native system runners --- .github/workflows/update-nix-hashes.yml | 116 +++++++++++++----- .../{update-hashes => update-hashes.sh} | 103 ++++++---------- 2 files changed, 121 insertions(+), 98 deletions(-) rename nix/scripts/{update-hashes => update-hashes.sh} (56%) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 3808bfe057b..2ddd347a04c 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -18,7 +18,23 @@ on: jobs: verify: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + system: x86_64-linux + platform: linux + - runner: ubuntu-24.04-arm + system: aarch64-linux + platform: linux + - runner: macos-latest + system: aarch64-darwin + platform: darwin + - runner: macos-15-intel + system: x86_64-darwin + platform: darwin + runs-on: ${{ matrix.runner }} steps: - name: Checkout repository @@ -30,51 +46,39 @@ jobs: - name: Setup Nix uses: DeterminateSystems/nix-installer-action@v20 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Configure git run: | git config --global user.email "opencode@sst.dev" git config --global user.name "opencode" - - name: Preflight build (x86_64-linux) - id: preflight_x86 + - name: Preflight build + id: preflight continue-on-error: true run: | - echo "Verifying existing hashes for x86_64-linux..." - nix build .#packages.x86_64-linux.default --system x86_64-linux -L - echo "x86_64-linux preflight build successful." - - - name: Preflight build (aarch64-linux) - id: preflight_aarch64 - continue-on-error: true - run: | - echo "Verifying existing hashes for aarch64-linux..." - nix build .#packages.aarch64-linux.default --system aarch64-linux -L - echo "aarch64-linux preflight build successful." + echo "Verifying existing hashes for ${{ matrix.system }}..." + nix build .#packages.${{ matrix.system }}.default --system ${{ matrix.system }} -L + echo "${{ matrix.system }} preflight build successful." - name: Summarize preflight status id: preflight_status run: | set -e - if [ "${PRE_X86}" = "failure" ] || [ "${PRE_ARM}" = "failure" ]; then + if [ "${PREFLIGHT_OUTCOME}" = "failure" ]; then echo "Preflight detected stale hashes." echo "failed=true" >> "$GITHUB_OUTPUT" else - echo "Preflight builds passed." + echo "Preflight build passed." echo "failed=false" >> "$GITHUB_OUTPUT" fi env: - PRE_X86: ${{ steps.preflight_x86.outcome }} - PRE_ARM: ${{ steps.preflight_aarch64.outcome }} + PREFLIGHT_OUTCOME: ${{ steps.preflight.outcome }} - name: Update build output hashes env: - SYSTEMS: "aarch64-linux x86_64-linux" + SYSTEMS: ${{ matrix.system }} run: | set -euo pipefail - nix/scripts/update-hashes + nix/scripts/update-hashes.sh - name: Detect hash changes id: hash_change @@ -88,21 +92,65 @@ jobs: echo "Hashes updated; running post-update verification." fi - - name: Verification build (x86_64-linux) + - name: Verification build if: steps.hash_change.outputs.changed == 'true' || steps.preflight_status.outputs.failed == 'true' run: | - echo "Running post-update verification build for x86_64-linux..." - nix build .#packages.x86_64-linux.default --system x86_64-linux -L - echo "x86_64-linux build successful." + echo "Running post-update verification build for ${{ matrix.system }}..." + nix build .#packages.${{ matrix.system }}.default --system ${{ matrix.system }} -L + echo "${{ matrix.system }} build successful." - - name: Verification build (aarch64-linux) - if: steps.hash_change.outputs.changed == 'true' - || steps.preflight_status.outputs.failed == 'true' + - name: Upload hash changes + uses: actions/upload-artifact@v4 + with: + name: hashes-${{ matrix.system }} + path: nix/hashes.json + retention-days: 1 + + commit-hashes: + needs: verify + if: always() + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Configure git run: | - echo "Running post-update verification build for aarch64-linux..." - nix build .#packages.aarch64-linux.default --system aarch64-linux -L - echo "aarch64-linux build successful." + git config --global user.email "opencode@sst.dev" + git config --global user.name "opencode" + + - name: Download all hash files + uses: actions/download-artifact@v4 + with: + path: hash-artifacts + + - name: Merge hash files + run: | + set -euo pipefail + + BASE_HASH="nix/hashes.json" + for system in x86_64-linux aarch64-linux aarch64-darwin x86_64-darwin; do + if [ -f "hash-artifacts/hashes-${system}/hashes.json" ]; then + echo "Merging ${system} hashes..." + # Deep merge: preserve all keys in nested objects + jq --arg sys "$system" \ + --slurpfile new "hash-artifacts/hashes-${system}/hashes.json" \ + '.models = ($new[0].models // .models) | + .nodeModules[$sys] = $new[0].nodeModules[$sys] | + .optional = (.optional + $new[0].optional)' \ + "$BASE_HASH" > /tmp/merged.json + mv /tmp/merged.json "$BASE_HASH" + else + echo "Warning: No hash artifact found for ${system}" + fi + done + + echo "Final merged hashes.json:" + cat "$BASE_HASH" - name: Commit Nix hash changes run: | @@ -122,7 +170,7 @@ jobs: VERSION=$(jq -r '.version' packages/opencode/package.json) git add flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix - git commit -m "nix: update hashes for v$VERSION" -m "[skip ci]" + git commit -m "nix: update hashes for v$VERSION" # For PRs, push to the head branch; for push events, push to current branch if [ "${{ github.event_name }}" = "pull_request" ]; then diff --git a/nix/scripts/update-hashes b/nix/scripts/update-hashes.sh similarity index 56% rename from nix/scripts/update-hashes rename to nix/scripts/update-hashes.sh index d4c20be9a13..1105307d96b 100755 --- a/nix/scripts/update-hashes +++ b/nix/scripts/update-hashes.sh @@ -122,78 +122,53 @@ for SYSTEM in $SYSTEMS; do BUILD_LOG=$(mktemp) JSON_OUTPUT=$(mktemp) - echo "Building ${TARGET} to discover correct node_modules outputHash..." - if nix build ".#${TARGET}" --system "$SYSTEM" --no-link --rebuild -L --log-format raw --keep-failed --json 2>&1 | tee "$BUILD_LOG" >"$JSON_OUTPUT"; then - echo "Build succeeded while dummy hash was set; aborting" - exit 1 - fi + echo "Building node_modules for ${SYSTEM} to discover correct outputHash..." + # Try to use nix-store --realise to force the build even with wrong hash + echo "Attempting to realize derivation: ${DRV_PATH}" + REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) + + # Check if build succeeded (shouldn't with dummy hash) + if echo "$REALISE_OUT" | grep -q "^/nix/store/" && [ -d "$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1)" ]; then + BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1) + echo "Build succeeded unexpectedly, hashing output: $BUILD_PATH" + CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) + else + # Extract hash from error message + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + + if [ -z "$CORRECT_HASH" ]; then + # Try alternate format: "hash mismatch ... got: sha256:..." + CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" + fi - CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + if [ -z "$CORRECT_HASH" ]; then + # Try to find and hash the kept failed build + echo "Searching for kept failed build directory..." + KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) - if [ -z "$CORRECT_HASH" ]; then - echo "Attempting to read nix log for ${DRV_PATH}..." - CORRECT_HASH="$(nix log "$DRV_PATH" 2>/dev/null | grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' | awk '{print $2}' | head -n1 || true)" - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to find 'got:' line in build log; attempting alternative hash extraction..." - - # When a FOD has the wrong hash, modern Nix refuses to build at all with: - # "some outputs ... are not valid, so checking is not possible", so we use nix-build (legacy) - # with the derivation path and --check disabled, which will actually build and produce a hash mismatch. + if [ -z "$KEPT_DIR" ]; then + # Alternative pattern for kept build + KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1) + fi - echo "Using nix-build to force derivation build and capture hash mismatch..." - TMP_MODULES_LOG=$(mktemp) + if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then + echo "Found kept build directory: $KEPT_DIR" + # The output should be in a subdirectory + if [ -d "$KEPT_DIR/build" ]; then + HASH_PATH="$KEPT_DIR/build" + else + HASH_PATH="$KEPT_DIR" + fi - BUILD_OUT=$(nix-build "$DRV_PATH" --no-out-link --keep-failed --system "$SYSTEM" 2>&1 | tee "$TMP_MODULES_LOG" || true) + echo "Attempting to hash: $HASH_PATH" + ls -la "$HASH_PATH" || true - if echo "$BUILD_OUT" | grep -q "^/nix/store/"; then - # Build succeeded - hash the output - BUILD_PATH=$(echo "$BUILD_OUT" | grep "^/nix/store/" | head -n1) - if [ -d "$BUILD_PATH" ]; then - echo "Build succeeded with current hash, verifying output at: $BUILD_PATH" - CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) - if [ -n "$CORRECT_HASH" ]; then - echo "Hash verified from successful build: $CORRECT_HASH" - fi - fi - else - # Build failed - extract hash from error message - # Not sure if there's a way around this other than parsing the human-readable log: - CORRECT_HASH="$(grep -A1 'hash mismatch' "$TMP_MODULES_LOG" | grep 'got:' | awk '{print $2}' | sed 's/:/-/' || true)" - - if [ -z "$CORRECT_HASH" ]; then - # Try to find kept output - KEPT_LINE=$(grep -E "(keeping|kept).*build" "$TMP_MODULES_LOG" | head -n1 || true) - if [ -n "$KEPT_LINE" ]; then - KEPT_DIR=$(echo "$KEPT_LINE" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) - - if [ -z "$KEPT_DIR" ]; then - # Try without quotes - KEPT_DIR=$(echo "$KEPT_LINE" | grep -oE '/tmp/nix-build-[^ ]+|/nix/var/nix/builds/[^ ]+' | head -n1) - fi - - if [ -d "$KEPT_DIR" ]; then - if [ -d "$KEPT_DIR/output" ]; then - KEPT_OUT="$KEPT_DIR/output" - elif [ -d "$KEPT_DIR" ]; then - KEPT_OUT="$KEPT_DIR" - fi - - if [ -n "$KEPT_OUT" ]; then - echo "Found kept build at: $KEPT_OUT" - CORRECT_HASH=$(nix hash path --sri "$KEPT_OUT" 2>/dev/null || true) - - if [ -n "$CORRECT_HASH" ]; then - echo "Extracted hash from kept output: $CORRECT_HASH" - fi - fi - fi + if [ -d "$HASH_PATH/node_modules" ]; then + CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) + echo "Computed hash from kept build: $CORRECT_HASH" fi fi fi - - rm -f "$TMP_MODULES_LOG" fi if [ -z "$CORRECT_HASH" ]; then From fc367c428ac372c44e75ecc9ddcd6b67dcea856a Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 13:55:32 +1100 Subject: [PATCH 20/72] nix flake: pin node_modules metadata so derivation no longer drifts between CI and local --- nix/node-modules.nix | 6 +++++ nix/opencode.nix | 1 + nix/scripts/normalize-node-modules.ts | 38 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 nix/scripts/normalize-node-modules.ts diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 79c32041f85..da65af9056a 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,5 +1,9 @@ { optionalPackagesFile, hash, lib, stdenvNoCC, bun, cacert, curl }: args: +let + sourceDateEpoch = + if args ? sourceDateEpoch then args.sourceDateEpoch else 1; +in stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; version = args.version; @@ -20,6 +24,7 @@ stdenvNoCC.mkDerivation { runHook preBuild export HOME=$(mktemp -d) export BUN_INSTALL_CACHE_DIR=$(mktemp -d) + export SOURCE_DATE_EPOCH=${toString sourceDateEpoch} bun install \ --frozen-lockfile \ --ignore-scripts \ @@ -76,6 +81,7 @@ stdenvNoCC.mkDerivation { rm -f optional-packages.txt optional-metadata.txt bun --bun ${args.canonicalizeScript} + bun --bun ${args.normalizeScript} runHook postBuild ''; diff --git a/nix/opencode.nix b/nix/opencode.nix index e835fb4d75f..122c7427c77 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -10,6 +10,7 @@ let canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; optionalMetadataScript = scripts + "/optional-metadata.ts"; verifyShaScript = scripts + "/verify-sha.ts"; + normalizeScript = scripts + "/normalize-node-modules.ts"; } ); in diff --git a/nix/scripts/normalize-node-modules.ts b/nix/scripts/normalize-node-modules.ts new file mode 100644 index 00000000000..00c3ec7a8ad --- /dev/null +++ b/nix/scripts/normalize-node-modules.ts @@ -0,0 +1,38 @@ +import { join } from "path" +import { lstat, lutimes, readdir, utimes } from "fs/promises" + +const argv = Bun.argv.slice(2) +const root = process.cwd() +const base = argv[0] ?? "node_modules" +const target = join(root, base) +const epochRaw = Bun.env.SOURCE_DATE_EPOCH ?? "1" +const epoch = Number.parseInt(epochRaw, 10) + +if (!Number.isFinite(epoch)) { + console.error(`[normalize-node-modules] invalid SOURCE_DATE_EPOCH: ${epochRaw}`) + process.exit(1) +} + +const seen = new Set() +const stack = [target] + +while (stack.length > 0) { + const next = stack.pop() + if (!next) continue + if (seen.has(next)) continue + seen.add(next) + const info = await lstat(next) + if (info.isDirectory()) { + const entries = await readdir(next) + for (const entry of entries) { + stack.push(join(next, entry)) + } + } + if (info.isSymbolicLink()) { + await lutimes(next, epoch, epoch) + continue + } + await utimes(next, epoch, epoch) +} + +console.log("[normalize-node-modules] normalized timestamps for", seen.size, "paths") From 2a68819dab72329a9011443c1b7d7af660db3c9c Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 19:11:04 +1100 Subject: [PATCH 21/72] nix flake: pin platform specific optional packages --- flake.nix | 8 +++++- nix/hashes.json | 28 +++++++++++++++++-- nix/optional-packages/aarch64-darwin.txt | 2 ++ .../aarch64-linux.txt} | 0 nix/optional-packages/x86_64-darwin.txt | 2 ++ nix/optional-packages/x86_64-linux.txt | 2 ++ nix/scripts/update-hashes.sh | 21 +++++++++++++- 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 nix/optional-packages/aarch64-darwin.txt rename nix/{optional-packages.txt => optional-packages/aarch64-linux.txt} (100%) create mode 100644 nix/optional-packages/x86_64-darwin.txt create mode 100644 nix/optional-packages/x86_64-linux.txt diff --git a/flake.nix b/flake.nix index 4c49ea64abb..8c78714dc31 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,12 @@ }; scripts = ./nix/scripts; hashes = builtins.fromJSON (builtins.readFile ./nix/hashes.json); + optionalPackagesFiles = { + "aarch64-linux" = ./nix/optional-packages/aarch64-linux.txt; + "x86_64-linux" = ./nix/optional-packages/x86_64-linux.txt; + "aarch64-darwin" = ./nix/optional-packages/aarch64-darwin.txt; + "x86_64-darwin" = ./nix/optional-packages/x86_64-darwin.txt; + }; modelsDev = forEachSystem ( system: let @@ -62,7 +68,7 @@ pkgs = pkgsFor system; mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hash = hashes.nodeModules.${system}; - optionalPackagesFile = ./nix/optional-packages.txt; + optionalPackagesFile = optionalPackagesFiles.${system}; }; mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in diff --git a/nix/hashes.json b/nix/hashes.json index 25538a46ac1..90a35ebaf7e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -12,8 +12,32 @@ "sha512": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" }, "@opentui/core-linux-arm64": { - "version": "0.1.36", - "sha512": "sha512-ATR+vdtraZEC/gHR1mQa/NYPlqFNBpsnnJAGepQmcxm85VceLYM701QaaIgNAwyYXiP6RQN1ZCv06MD1Ph1m4w==" + "version": "0.0.0-20251106-788e97e4", + "sha512": "sha512-Zi1EzLCzooRfYoQnN/Dz8OxzrpRXByny8SJqhdO9ZP2mYX72yJ3AhUUW1Sl6YSzVi0H+QIKj7g+RX2KfsXIGFg==" + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "sha512": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==" + }, + "@opentui/core-linux-x64": { + "version": "0.0.0-20251106-788e97e4", + "sha512": "sha512-/E0XEBVzO4JEEhJGzfURF2tPxDE2oTODxlgNYYB1QbAuOsLcV69uSrwAjo1TxuIn4P78tBR+ZOlmONjroPqfbQ==" + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "sha512": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==" + }, + "@opentui/core-darwin-arm64": { + "version": "0.0.0-20251106-788e97e4", + "sha512": "sha512-EOO8SSIYJBIh+Sd9bgVTiQmt+TEJmfg65/oym54J4zfDtCYlAqSaLcRnDe4TzB+4hejV9of8etrG3ZZACBJT+A==" + }, + "@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "sha512": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==" + }, + "@opentui/core-darwin-x64": { + "version": "0.0.0-20251106-788e97e4", + "sha512": "sha512-MUTt7CDbzL2afNGK8gJ4jUZd+AHiOUJEO0eJGDSfWU8DUs0zv8XoLZfaI5PPbkUPEL/7CEBMARAAiwfRtoG/4A==" } } } diff --git a/nix/optional-packages/aarch64-darwin.txt b/nix/optional-packages/aarch64-darwin.txt new file mode 100644 index 00000000000..34a4ec964e9 --- /dev/null +++ b/nix/optional-packages/aarch64-darwin.txt @@ -0,0 +1,2 @@ +@parcel/watcher-darwin-arm64 +@opentui/core-darwin-arm64 diff --git a/nix/optional-packages.txt b/nix/optional-packages/aarch64-linux.txt similarity index 100% rename from nix/optional-packages.txt rename to nix/optional-packages/aarch64-linux.txt diff --git a/nix/optional-packages/x86_64-darwin.txt b/nix/optional-packages/x86_64-darwin.txt new file mode 100644 index 00000000000..cf3df4cd3f4 --- /dev/null +++ b/nix/optional-packages/x86_64-darwin.txt @@ -0,0 +1,2 @@ +@parcel/watcher-darwin-x64 +@opentui/core-darwin-x64 diff --git a/nix/optional-packages/x86_64-linux.txt b/nix/optional-packages/x86_64-linux.txt new file mode 100644 index 00000000000..5c147c8a65c --- /dev/null +++ b/nix/optional-packages/x86_64-linux.txt @@ -0,0 +1,2 @@ +@parcel/watcher-linux-x64-glibc +@opentui/core-linux-x64 diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 1105307d96b..198dd0a353c 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -6,7 +6,23 @@ DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" MODELS_URL=${MODELS_URL:-https://models.dev/api.json} DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} -OPTIONAL_FILE=${OPTIONAL_PACKAGES_FILE:-nix/optional-packages.txt} +OPTIONAL_PACKAGES_DIR=${OPTIONAL_PACKAGES_DIR:-nix/optional-packages} +OPTIONAL_FILE=${OPTIONAL_PACKAGES_FILE:-} +OPTIONAL_AGGREGATE="" + +if [ -z "$OPTIONAL_FILE" ] && [ -d "$OPTIONAL_PACKAGES_DIR" ]; then + OPTIONAL_AGGREGATE=$(mktemp) + find "$OPTIONAL_PACKAGES_DIR" -maxdepth 1 -type f -name '*.txt' | sort | while read -r file; do + [ -n "$file" ] || continue + cat "$file" + echo + done | sed 's/#.*$//' | sed '/^[[:space:]]*$/d' | sort -u >"$OPTIONAL_AGGREGATE" + OPTIONAL_FILE="$OPTIONAL_AGGREGATE" +fi + +if [ -z "$OPTIONAL_FILE" ]; then + OPTIONAL_FILE="nix/optional-packages.txt" +fi export DUMMY export NIX_KEEP_OUTPUTS=1 export NIX_KEEP_DERIVATIONS=1 @@ -59,6 +75,9 @@ cleanup() { if [ -n "${TMP_EXPR:-}" ] && [ -f "$TMP_EXPR" ]; then rm -f "$TMP_EXPR" fi + if [ -n "$OPTIONAL_AGGREGATE" ] && [ -f "$OPTIONAL_AGGREGATE" ]; then + rm -f "$OPTIONAL_AGGREGATE" + fi } trap cleanup EXIT From 902bc08a92cdbde28a89c423e796f59893ccdc22 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 19:39:55 +1100 Subject: [PATCH 22/72] nix flake: only npm pack when target bundle is absent --- nix/opencode.nix | 2 ++ packages/opencode/script/build.ts | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/nix/opencode.nix b/nix/opencode.nix index 122c7427c77..3f9c446f6ff 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -37,6 +37,8 @@ stdenvNoCC.mkDerivation (finalAttrs: { ''; env.MODELS_DEV_API_JSON = args.modelsDev; + env.OPENCODE_VERSION = args.version; + env.OPENCODE_CHANNEL = "stable"; buildPhase = '' runHook preBuild diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 29706c09cc5..ec811d708ed 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from "url" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") +const modulesDir = path.join(dir, "../../node_modules") process.chdir(dir) @@ -31,6 +32,23 @@ const targets = singleFlag ? allTargets.filter(([os, arch]) => os === process.platform && arch === process.arch) : allTargets +async function bundleReady(name: string) { + const marker = Bun.file(path.join(modulesDir, name, "package.json")) + return marker.exists() +} + +async function ensureBundle(name: string, spec?: string) { + if (await bundleReady(name)) { + return + } + const target = path.join(modulesDir, name) + const tweak = name.replace(/^@/, "").replace(/\//g, "-") + await $`mkdir -p ${target}` + const pack = spec ?? name + await $`npm pack ${pack}`.cwd(modulesDir) + await $`tar -xf ${tweak}-*.tgz -C ${target} --strip-components=1`.cwd(modulesDir) +} + await $`rm -rf dist` const binaries: Record = {} @@ -40,14 +58,10 @@ for (const [os, arch] of targets) { await $`mkdir -p dist/${name}/bin` const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}` - await $`mkdir -p ../../node_modules/${opentui}` - await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(path.join(dir, "../../node_modules")) - await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1` + await ensureBundle(opentui, `${opentui}@${pkg.dependencies["@opentui/core"]}`) const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}` - await $`mkdir -p ../../node_modules/${watcher}` - await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet() - await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1` + await ensureBundle(watcher) const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js")) const workerPath = "./src/cli/cmd/tui/worker.ts" From a038fc76c1883130c0b33dd8e7100826ee83272a Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 22:51:52 +1100 Subject: [PATCH 23/72] nix flake: add hash guard workflow and add better metadata to auto commits --- .github/workflows/guard-release-hashes.yml | 143 +++++++++++++++++++++ .github/workflows/update-nix-hashes.yml | 73 ++++++++++- nix/scripts/update-hashes.sh | 50 ++++++- 3 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/guard-release-hashes.yml diff --git a/.github/workflows/guard-release-hashes.yml b/.github/workflows/guard-release-hashes.yml new file mode 100644 index 00000000000..d6c43c6e8ea --- /dev/null +++ b/.github/workflows/guard-release-hashes.yml @@ -0,0 +1,143 @@ +name: Guard Release Hashes + +on: + release: + types: + - published + - prereleased + +permissions: + contents: read + actions: read + +jobs: + ensure-ready: + runs-on: ubuntu-latest + steps: + - name: Check pending hash workflow + id: pending + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET_BRANCH: ${{ github.event.release.target_commitish }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + branch="${TARGET_BRANCH:-}" + if [ -z "$branch" ]; then + branch="$DEFAULT_BRANCH" + fi + if printf '%s' "$branch" | grep -Eq '^[0-9a-f]{40}$'; then + branch="$DEFAULT_BRANCH" + fi + api="${{ github.api_url }}/repos/${{ github.repository }}/actions/workflows/update-nix-hashes.yml/runs?per_page=20&branch=${branch}" + data=$(curl -fsSL -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" "$api") + if [ -z "$data" ]; then + echo "Failed to read workflow runs" + exit 1 + fi + pending=$(printf '%s' "$data" | jq '[.workflow_runs[] | select(.status != "completed")]') + count=$(printf '%s' "$pending" | jq 'length') + if [ "$count" -gt 0 ]; then + urls=$(printf '%s' "$pending" | jq -r '.[].html_url') + { + echo "pending=true" + echo "branch=$branch" + echo "urls<> "$GITHUB_OUTPUT" + echo "Pending hash workflow detected on ${branch}" + exit 1 + fi + { + echo "pending=false" + echo "branch=$branch" + } >> "$GITHUB_OUTPUT" + + - name: Checkout release commit + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 0 + + - name: Validate dependency coverage + id: coverage + run: | + set -euo pipefail + if [ ! -f nix/hashes.json ]; then + echo "nix/hashes.json missing in release commit" + exit 1 + fi + source_commit=$(jq -r '.metadata.sourceCommit // empty' nix/hashes.json) + if [ -z "$source_commit" ]; then + echo "metadata.sourceCommit missing; rerun nix workflow" + exit 1 + fi + if ! git cat-file -e "${source_commit}^{commit}" >/dev/null 2>&1; then + echo "metadata.sourceCommit ${source_commit} not found in repo history" + exit 1 + fi + mapfile -t package_files < <(git ls-files 'packages/*/package.json') + paths=() + if git ls-files --error-unmatch bun.lock >/dev/null 2>&1; then + paths+=("bun.lock") + fi + if git ls-files --error-unmatch package.json >/dev/null 2>&1; then + paths+=("package.json") + fi + if [ "${#package_files[@]}" -gt 0 ]; then + paths+=("${package_files[@]}") + fi + release_commit=$(git rev-parse HEAD) + diff_out="" + if [ "${#paths[@]}" -gt 0 ]; then + diff_out=$(git diff --name-only "${source_commit}..${release_commit}" -- "${paths[@]}" || true) + fi + if [ -n "${diff_out:-}" ]; then + echo "Dependency files changed since last hash update:" + printf '%s\n' "$diff_out" + { + echo "outdated=true" + echo "diff<> "$GITHUB_OUTPUT" + exit 1 + fi + { + echo "outdated=false" + echo "source_commit=${source_commit}" + echo "release_commit=${release_commit}" + } >> "$GITHUB_OUTPUT" + + - name: Summarize guard status + if: always() + run: | + { + echo "### Release Hash Guard" + echo "" + echo "- tag: ${{ github.event.release.tag_name }}" + echo "- branch: ${{ steps.pending.outputs.branch }}" + echo "- pending-hash-run: ${{ steps.pending.outputs.pending }}" + echo "- dependency-changes: ${{ steps.coverage.outputs.outdated }}" + } >> "$GITHUB_STEP_SUMMARY" + if [ "${{ steps.coverage.outputs.outdated }}" = "true" ]; then + { + echo "- last-hash-commit: ${{ steps.coverage.outputs.source_commit }}" + echo "- release-commit: ${{ steps.coverage.outputs.release_commit }}" + echo "" + echo "#### Files requiring hash refresh" + echo "" + printf '%s\n' "${{ steps.coverage.outputs.diff }}" + } >> "$GITHUB_STEP_SUMMARY" + fi + if [ "${{ steps.pending.outputs.pending }}" = "true" ]; then + { + echo "" + echo "#### Pending Runs" + echo "" + printf '%s\n' "${{ steps.pending.outputs.urls }}" + } >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 2ddd347a04c..31b44c2046b 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -152,11 +152,78 @@ jobs: echo "Final merged hashes.json:" cat "$BASE_HASH" + - name: Derive commit metadata + id: commit_meta + run: | + set -euo pipefail + EVENT="$GITHUB_EVENT_NAME" + LABEL="$GITHUB_REF_NAME" + SUMMARY="Hashes refreshed for $LABEL" + TAG_NAME="" + PR_NUMBER="" + if [ -f "$GITHUB_EVENT_PATH" ]; then + TAG_NAME="$(jq -r '.release.tag_name // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true)" + PR_NUMBER="$(jq -r '.number // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true)" + fi + case "$EVENT" in + release) + LABEL="release:${GITHUB_REF_NAME}" + if [ -n "$TAG_NAME" ]; then + LABEL="release:${TAG_NAME}" + fi + SUMMARY="Hashes refreshed for ${LABEL}" + ;; + pull_request) + LABEL="pr:${GITHUB_REF_NAME}" + if [ -n "$PR_NUMBER" ]; then + LABEL="pr:${PR_NUMBER}" + fi + SUMMARY="Hashes refreshed for ${LABEL}" + ;; + workflow_dispatch) + LABEL="manual:${GITHUB_REF_NAME}" + SUMMARY="Manual hash refresh for ${LABEL}" + ;; + *) + LABEL="branch:${GITHUB_REF_NAME}" + SUMMARY="Hashes refreshed for ${LABEL}" + ;; + esac + MESSAGE="nix: update hashes (${LABEL})" + { + echo "message=$MESSAGE" + echo "label=$LABEL" + echo "summary=$SUMMARY" + } >> "$GITHUB_OUTPUT" + - name: Commit Nix hash changes + env: + COMMIT_MESSAGE: ${{ steps.commit_meta.outputs.message }} + UPDATE_LABEL: ${{ steps.commit_meta.outputs.label }} + UPDATE_SUMMARY: ${{ steps.commit_meta.outputs.summary }} run: | set -euo pipefail + summarize() { + local status="$1" + { + echo "### Nix Hash Update" + echo "" + echo "- context: ${UPDATE_LABEL}" + echo "- ref: ${GITHUB_REF_NAME}" + echo "- status: ${status}" + } >> "$GITHUB_STEP_SUMMARY" + if [ -n "${UPDATE_SUMMARY:-}" ]; then + echo "- note: ${UPDATE_SUMMARY}" >> "$GITHUB_STEP_SUMMARY" + fi + 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" + } + if git diff --quiet flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix; then + summarize "no changes" echo "No changes to tracked Nix files. Hashes are already up to date." exit 0 fi @@ -164,13 +231,13 @@ jobs: # Prevent infinite loop: check if the last commit was a hash update LAST_COMMIT_MSG=$(git log -1 --pretty=%B) if echo "$LAST_COMMIT_MSG" | grep -q "^nix: update hashes"; then + summarize "skipped (recent hash update)" echo "Last commit was already a hash update. Skipping to prevent loop." exit 0 fi - VERSION=$(jq -r '.version' packages/opencode/package.json) git add flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix - git commit -m "nix: update hashes for v$VERSION" + git commit -m "$COMMIT_MESSAGE" # For PRs, push to the head branch; for push events, push to current branch if [ "${{ github.event_name }}" = "pull_request" ]; then @@ -179,3 +246,5 @@ jobs: git pull --rebase origin ${GITHUB_REF_NAME} git push origin HEAD:${GITHUB_REF_NAME} fi + + summarize "committed $(git rev-parse --short HEAD)" diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 198dd0a353c..1d3117173a3 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -41,7 +41,8 @@ if [ ! -f "$HASH_FILE" ]; then { "models": {}, "nodeModules": {}, - "optional": {} + "optional": {}, + "metadata": {} } EOF fi @@ -209,3 +210,50 @@ for SYSTEM in $SYSTEMS; do rm -f "$BUILD_LOG" unset BUILD_LOG done + +write_metadata() { + local tmp ts ref commit run_url + ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + ref="${HASH_SOURCE_REF:-${GITHUB_REF_NAME:-}}" + if [ -z "$ref" ] && command -v git >/dev/null 2>&1; then + ref="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + if [ "$ref" = "HEAD" ]; then + ref="$(git rev-parse --short HEAD 2>/dev/null || true)" + fi + fi + if [ -z "$ref" ]; then + ref="unknown" + fi + commit="${HASH_SOURCE_COMMIT:-${GITHUB_SHA:-}}" + if [ -z "$commit" ] && command -v git >/dev/null 2>&1; then + commit="$(git rev-parse HEAD 2>/dev/null || true)" + fi + if [ -z "$commit" ]; then + commit="unknown" + fi + run_url="${HASH_SOURCE_RUN:-}" + if [ -z "$run_url" ] && [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + fi + tmp=$(mktemp) + jq \ + --arg ref "$ref" \ + --arg sha "$commit" \ + --arg ts "$ts" \ + ' + .metadata = (.metadata // {}) | + .metadata.updatedAt = $ts | + .metadata.sourceRef = $ref | + .metadata.sourceCommit = $sha + ' \ + "$HASH_FILE" >"$tmp" + mv "$tmp" "$HASH_FILE" + if [ -n "$run_url" ]; then + tmp=$(mktemp) + jq --arg run "$run_url" '.metadata.workflowRun = $run' "$HASH_FILE" >"$tmp" + mv "$tmp" "$HASH_FILE" + fi + echo "metadata updated: ref=${ref} commit=${commit}" +} + +write_metadata From a1ba48bee462abc61f735b6fb99e886f8fd414f2 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 7 Nov 2025 23:10:25 +1100 Subject: [PATCH 24/72] nix flake: expanded triggers for the workflow, use hash metadata with trigger/update commits, gate releases via a new guard workflow that verifies hashes are current --- .github/workflows/guard-release-hashes.yml | 29 +++++++++----- .github/workflows/update-nix-hashes.yml | 4 ++ nix/scripts/update-hashes.sh | 46 ++++++++++++---------- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/.github/workflows/guard-release-hashes.yml b/.github/workflows/guard-release-hashes.yml index d6c43c6e8ea..deadecc5d90 100644 --- a/.github/workflows/guard-release-hashes.yml +++ b/.github/workflows/guard-release-hashes.yml @@ -68,13 +68,21 @@ jobs: echo "nix/hashes.json missing in release commit" exit 1 fi - source_commit=$(jq -r '.metadata.sourceCommit // empty' nix/hashes.json) - if [ -z "$source_commit" ]; then - echo "metadata.sourceCommit missing; rerun nix workflow" + trigger_commit=$(jq -r '.metadata.trigger // empty' nix/hashes.json) + if [ -z "$trigger_commit" ]; then + echo "metadata.trigger missing; rerun nix workflow" exit 1 fi - if ! git cat-file -e "${source_commit}^{commit}" >/dev/null 2>&1; then - echo "metadata.sourceCommit ${source_commit} not found in repo history" + if ! git cat-file -e "${trigger_commit}^{commit}" >/dev/null 2>&1; then + echo "metadata trigger ${trigger_commit} not found in repo history" + exit 1 + fi + hash_commit=$(jq -r '.metadata.commit // empty' nix/hashes.json) + if [ -z "$hash_commit" ]; then + hash_commit="$trigger_commit" + fi + if ! git cat-file -e "${hash_commit}^{commit}" >/dev/null 2>&1; then + echo "metadata commit ${hash_commit} not found in repo history" exit 1 fi mapfile -t package_files < <(git ls-files 'packages/*/package.json') @@ -91,7 +99,7 @@ jobs: release_commit=$(git rev-parse HEAD) diff_out="" if [ "${#paths[@]}" -gt 0 ]; then - diff_out=$(git diff --name-only "${source_commit}..${release_commit}" -- "${paths[@]}" || true) + diff_out=$(git diff --name-only "${trigger_commit}..${release_commit}" -- "${paths[@]}" || true) fi if [ -n "${diff_out:-}" ]; then echo "Dependency files changed since last hash update:" @@ -101,14 +109,16 @@ jobs: echo "diff<> "$GITHUB_OUTPUT" exit 1 fi { echo "outdated=false" - echo "source_commit=${source_commit}" + echo "trigger_commit=${trigger_commit}" + echo "hash_commit=${hash_commit}" echo "release_commit=${release_commit}" } >> "$GITHUB_OUTPUT" @@ -122,10 +132,11 @@ jobs: echo "- branch: ${{ steps.pending.outputs.branch }}" echo "- pending-hash-run: ${{ steps.pending.outputs.pending }}" echo "- dependency-changes: ${{ steps.coverage.outputs.outdated }}" + echo "- hash-trigger: ${{ steps.coverage.outputs.trigger_commit }}" + echo "- hash-commit: ${{ steps.coverage.outputs.hash_commit }}" } >> "$GITHUB_STEP_SUMMARY" if [ "${{ steps.coverage.outputs.outdated }}" = "true" ]; then { - echo "- last-hash-commit: ${{ steps.coverage.outputs.source_commit }}" echo "- release-commit: ${{ steps.coverage.outputs.release_commit }}" echo "" echo "#### Files requiring hash refresh" diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 31b44c2046b..cec80d52d20 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -10,11 +10,15 @@ on: - 'bun.lock' - 'package.json' - 'packages/*/package.json' + - 'nix/optional-packages/**' + - 'nix/scripts/optional-metadata.ts' pull_request: paths: - 'bun.lock' - 'package.json' - 'packages/*/package.json' + - 'nix/optional-packages/**' + - 'nix/scripts/optional-metadata.ts' jobs: verify: diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 1d3117173a3..08a297674a1 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -143,37 +143,32 @@ for SYSTEM in $SYSTEMS; do JSON_OUTPUT=$(mktemp) echo "Building node_modules for ${SYSTEM} to discover correct outputHash..." - # Try to use nix-store --realise to force the build even with wrong hash echo "Attempting to realize derivation: ${DRV_PATH}" REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) - # Check if build succeeded (shouldn't with dummy hash) - if echo "$REALISE_OUT" | grep -q "^/nix/store/" && [ -d "$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1)" ]; then - BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1) - echo "Build succeeded unexpectedly, hashing output: $BUILD_PATH" + BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) + if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then + echo "Realized node_modules output: $BUILD_PATH" CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) - else - # Extract hash from error message + fi + + if [ -z "$CORRECT_HASH" ]; then CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" if [ -z "$CORRECT_HASH" ]; then - # Try alternate format: "hash mismatch ... got: sha256:..." CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" fi if [ -z "$CORRECT_HASH" ]; then - # Try to find and hash the kept failed build echo "Searching for kept failed build directory..." KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) if [ -z "$KEPT_DIR" ]; then - # Alternative pattern for kept build KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1) fi if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then echo "Found kept build directory: $KEPT_DIR" - # The output should be in a subdirectory if [ -d "$KEPT_DIR/build" ]; then HASH_PATH="$KEPT_DIR/build" else @@ -212,8 +207,8 @@ for SYSTEM in $SYSTEMS; do done write_metadata() { - local tmp ts ref commit run_url - ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + local tmp stamp ref trigger commit run_url + stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" ref="${HASH_SOURCE_REF:-${GITHUB_REF_NAME:-}}" if [ -z "$ref" ] && command -v git >/dev/null 2>&1; then ref="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" @@ -224,12 +219,19 @@ write_metadata() { if [ -z "$ref" ]; then ref="unknown" fi - commit="${HASH_SOURCE_COMMIT:-${GITHUB_SHA:-}}" + trigger="${HASH_TRIGGER_COMMIT:-${GITHUB_SHA:-}}" + if [ -z "$trigger" ] && command -v git >/dev/null 2>&1; then + trigger="$(git rev-parse HEAD 2>/dev/null || true)" + fi + if [ -z "$trigger" ]; then + trigger="unknown" + fi + commit="${HASH_UPDATE_COMMIT:-}" if [ -z "$commit" ] && command -v git >/dev/null 2>&1; then commit="$(git rev-parse HEAD 2>/dev/null || true)" fi if [ -z "$commit" ]; then - commit="unknown" + commit="$trigger" fi run_url="${HASH_SOURCE_RUN:-}" if [ -z "$run_url" ] && [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then @@ -238,13 +240,15 @@ write_metadata() { tmp=$(mktemp) jq \ --arg ref "$ref" \ - --arg sha "$commit" \ - --arg ts "$ts" \ + --arg trigger "$trigger" \ + --arg commit "$commit" \ ' - .metadata = (.metadata // {}) | - .metadata.updatedAt = $ts | - .metadata.sourceRef = $ref | - .metadata.sourceCommit = $sha + .metadata = { + stamp: $stamp, + ref: $ref, + trigger: $trigger, + commit: $commit + } ' \ "$HASH_FILE" >"$tmp" mv "$tmp" "$HASH_FILE" From 9b0411bf3016daec93be1de8c918a8b6b0ed35bf Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 11:48:59 +1100 Subject: [PATCH 25/72] nix flake: remove direct models.dev json fetching, replace with equivalent nixpkgs distribution --- .github/workflows/update-nix-hashes.yml | 4 ++-- flake.nix | 2 +- nix/hashes.json | 3 --- nix/models-dev.nix | 18 ------------------ nix/scripts/update-hashes.sh | 22 +--------------------- 5 files changed, 4 insertions(+), 45 deletions(-) delete mode 100644 nix/models-dev.nix diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index cec80d52d20..be9c65cac0e 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -226,7 +226,7 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" } - if git diff --quiet flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix; then + if git diff --quiet flake.nix nix/node-modules.nix nix/hashes.json; then summarize "no changes" echo "No changes to tracked Nix files. Hashes are already up to date." exit 0 @@ -240,7 +240,7 @@ jobs: exit 0 fi - git add flake.nix nix/node-modules.nix nix/hashes.json nix/models-dev.nix + git add flake.nix nix/node-modules.nix nix/hashes.json git commit -m "$COMMIT_MESSAGE" # For PRs, push to the head branch; for push events, push to current branch diff --git a/flake.nix b/flake.nix index 8c78714dc31..c1c4b94060b 100644 --- a/flake.nix +++ b/flake.nix @@ -40,7 +40,7 @@ let pkgs = pkgsFor system; in - pkgs.callPackage ./nix/models-dev.nix { hash = hashes.models.dev; } + pkgs."models-dev" ); in { diff --git a/nix/hashes.json b/nix/hashes.json index 90a35ebaf7e..58d802d25d3 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,7 +1,4 @@ { - "models": { - "dev": "sha256-Dff3OWJ7pD7LfVbZZ0Gf/QA65uw4ft14mdfBun0qDBg=" - }, "nodeModules": { "aarch64-linux": "sha256-h/XhGcla9THbbqP3tTjyaN4jw/SdaGJ3vUXRy0jlo8E=", "x86_64-linux": "sha256-8goN/fR8mjiJj/ZzfgP4ES2sW2Ljoyl+SNthqNj6Dr8=" diff --git a/nix/models-dev.nix b/nix/models-dev.nix deleted file mode 100644 index baacb05e29f..00000000000 --- a/nix/models-dev.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ hash, stdenvNoCC, fetchurl }: -stdenvNoCC.mkDerivation { - pname = "models-dev"; - version = "unstable"; - - src = fetchurl { - url = "https://models.dev/api.json"; - hash = hash; - }; - - dontUnpack = true; - dontBuild = true; - - installPhase = '' - mkdir -p $out/dist - cp $src $out/dist/_api.json - ''; -} diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 08a297674a1..3cbf7f1f0cf 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -3,7 +3,6 @@ set -euo pipefail DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" -MODELS_URL=${MODELS_URL:-https://models.dev/api.json} DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} OPTIONAL_PACKAGES_DIR=${OPTIONAL_PACKAGES_DIR:-nix/optional-packages} @@ -39,7 +38,6 @@ fi if [ ! -f "$HASH_FILE" ]; then cat <<'EOF' >"$HASH_FILE" { - "models": {}, "nodeModules": {}, "optional": {}, "metadata": {} @@ -47,25 +45,6 @@ if [ ! -f "$HASH_FILE" ]; then EOF fi -echo "Refreshing models-dev hash..." -MODELS_HASH=$(nix store prefetch-file "$MODELS_URL" --json | jq -r '.hash // empty') - -if [ -z "$MODELS_HASH" ]; then - echo "Failed to prefetch models-dev hash" - exit 1 -fi - -current_models_hash="$(jq -r '.models.dev // empty' "$HASH_FILE")" - -if [ "$MODELS_HASH" != "$current_models_hash" ]; then - tmp=$(mktemp) - jq --arg value "$MODELS_HASH" '.models.dev = $value' "$HASH_FILE" >"$tmp" - mv "$tmp" "$HASH_FILE" - echo "models-dev hash updated: $MODELS_HASH" -else - echo "models-dev hash already up to date: $MODELS_HASH" -fi - cleanup() { if [ -n "${JSON_OUTPUT:-}" ] && [ -f "$JSON_OUTPUT" ]; then rm -f "$JSON_OUTPUT" @@ -127,6 +106,7 @@ write_node_modules_hash() { for SYSTEM in $SYSTEMS; do TARGET="packages.${SYSTEM}.default" MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" + CORRECT_HASH="" echo "Removing cached node_modules output for ${SYSTEM} (if present)..." PREV_PATH="$(nix path-info "$MODULES_ATTR" --system "$SYSTEM" 2>/dev/null || true)" From 0fb86b45c1831378b2a2fdb29e807e5053890b7e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 12:04:07 +1100 Subject: [PATCH 26/72] nix flake: properly cancel previous workflow --- .github/workflows/update-nix-hashes.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index be9c65cac0e..311fa9f146f 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -21,7 +21,15 @@ on: - 'nix/scripts/optional-metadata.ts' jobs: + cancel-previous: + runs-on: ubuntu-latest + steps: + - uses: styfle/cancel-workflow-action@0.12.1 + with: + access_token: ${{ github.token }} + verify: + needs: cancel-previous strategy: fail-fast: false matrix: @@ -143,8 +151,7 @@ jobs: # Deep merge: preserve all keys in nested objects jq --arg sys "$system" \ --slurpfile new "hash-artifacts/hashes-${system}/hashes.json" \ - '.models = ($new[0].models // .models) | - .nodeModules[$sys] = $new[0].nodeModules[$sys] | + '.nodeModules[$sys] = $new[0].nodeModules[$sys] | .optional = (.optional + $new[0].optional)' \ "$BASE_HASH" > /tmp/merged.json mv /tmp/merged.json "$BASE_HASH" From f154c25388ee179f90ba9e3da3a7a580b27a932e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 12:51:52 +1100 Subject: [PATCH 27/72] nix flake: test with missing hashes.json --- .github/workflows/update-nix-hashes.yml | 50 ++++++++++++++++++++----- flake.nix | 16 +++++++- nix/hashes.json | 40 -------------------- nix/scripts/optional-metadata.ts | 16 ++++---- nix/scripts/update-hashes.sh | 28 +++++++++----- 5 files changed, 83 insertions(+), 67 deletions(-) delete mode 100644 nix/hashes.json diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 311fa9f146f..762694ffd20 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -96,7 +96,8 @@ jobs: id: hash_change run: | set -euo pipefail - if git diff --quiet nix/hashes.json; then + STATUS="$(git status --short -- nix/hashes.json || true)" + if [ -z "$STATUS" ]; then echo "changed=false" >> "$GITHUB_OUTPUT" echo "No hash changes detected." else @@ -140,11 +141,30 @@ jobs: with: path: hash-artifacts + - name: Sync branch state + env: + TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + run: | + set -euo pipefail + BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" + git fetch origin "$BRANCH" + if git show-ref --verify --quiet "refs/heads/$BRANCH"; then + git checkout "$BRANCH" + else + git checkout -b "$BRANCH" + fi + git reset --hard "origin/$BRANCH" + - name: Merge hash files + id: merge_hashes run: | set -euo pipefail BASE_HASH="nix/hashes.json" + if [ ! -f "$BASE_HASH" ]; then + mkdir -p "$(dirname "$BASE_HASH")" + jq -n '{nodeModules: {}, optional: {}, metadata: {}}' > "$BASE_HASH" + fi for system in x86_64-linux aarch64-linux aarch64-darwin x86_64-darwin; do if [ -f "hash-artifacts/hashes-${system}/hashes.json" ]; then echo "Merging ${system} hashes..." @@ -163,6 +183,15 @@ jobs: echo "Final merged hashes.json:" cat "$BASE_HASH" + DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + HAS_NODE=$(jq --arg dummy "$DUMMY" '[ (.nodeModules // {})[] | select(. != $dummy) ] | length' "$BASE_HASH") + HAS_OPTIONAL=$(jq '(.optional // {}) | length' "$BASE_HASH") + if [ "$HAS_NODE" -gt 0 ] || [ "$HAS_OPTIONAL" -gt 0 ]; then + echo "has_data=true" >> "$GITHUB_OUTPUT" + else + echo "has_data=false" >> "$GITHUB_OUTPUT" + fi + - name: Derive commit metadata id: commit_meta run: | @@ -201,17 +230,23 @@ jobs: ;; esac MESSAGE="nix: update hashes (${LABEL})" + if [ "${HAS_HASH_DATA}" != "true" ]; then + MESSAGE="nix: bootstrap hashes (${LABEL})" + fi { echo "message=$MESSAGE" echo "label=$LABEL" echo "summary=$SUMMARY" } >> "$GITHUB_OUTPUT" + env: + HAS_HASH_DATA: ${{ steps.merge_hashes.outputs.has_data }} - name: Commit Nix hash changes env: COMMIT_MESSAGE: ${{ steps.commit_meta.outputs.message }} UPDATE_LABEL: ${{ steps.commit_meta.outputs.label }} UPDATE_SUMMARY: ${{ steps.commit_meta.outputs.summary }} + TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} run: | set -euo pipefail @@ -233,7 +268,9 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" } - if git diff --quiet flake.nix nix/node-modules.nix nix/hashes.json; then + FILES=(flake.nix nix/node-modules.nix nix/hashes.json) + STATUS="$(git status --short -- "${FILES[@]}" || true)" + if [ -z "$STATUS" ]; then summarize "no changes" echo "No changes to tracked Nix files. Hashes are already up to date." exit 0 @@ -250,12 +287,7 @@ jobs: git add flake.nix nix/node-modules.nix nix/hashes.json git commit -m "$COMMIT_MESSAGE" - # For PRs, push to the head branch; for push events, push to current branch - if [ "${{ github.event_name }}" = "pull_request" ]; then - git push origin HEAD:${{ github.head_ref }} - else - git pull --rebase origin ${GITHUB_REF_NAME} - git push origin HEAD:${GITHUB_REF_NAME} - fi + BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" + git push origin HEAD:"$BRANCH" summarize "committed $(git rev-parse --short HEAD)" diff --git a/flake.nix b/flake.nix index c1c4b94060b..862dd542849 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,21 @@ "x86_64-darwin" = "bun-darwin-x64"; }; scripts = ./nix/scripts; - hashes = builtins.fromJSON (builtins.readFile ./nix/hashes.json); + dummyHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + defaultNodeModules = builtins.listToAttrs ( + map (system: { + name = system; + value = dummyHash; + }) systems + ); + hashesFile = "${./nix}/hashes.json"; + hashesData = + if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { }; + hashes = { + nodeModules = defaultNodeModules // (hashesData.nodeModules or { }); + optional = hashesData.optional or { }; + metadata = hashesData.metadata or { }; + }; optionalPackagesFiles = { "aarch64-linux" = ./nix/optional-packages/aarch64-linux.txt; "x86_64-linux" = ./nix/optional-packages/x86_64-linux.txt; diff --git a/nix/hashes.json b/nix/hashes.json deleted file mode 100644 index 58d802d25d3..00000000000 --- a/nix/hashes.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "nodeModules": { - "aarch64-linux": "sha256-h/XhGcla9THbbqP3tTjyaN4jw/SdaGJ3vUXRy0jlo8E=", - "x86_64-linux": "sha256-8goN/fR8mjiJj/ZzfgP4ES2sW2Ljoyl+SNthqNj6Dr8=" - }, - "optional": { - "@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "sha512": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" - }, - "@opentui/core-linux-arm64": { - "version": "0.0.0-20251106-788e97e4", - "sha512": "sha512-Zi1EzLCzooRfYoQnN/Dz8OxzrpRXByny8SJqhdO9ZP2mYX72yJ3AhUUW1Sl6YSzVi0H+QIKj7g+RX2KfsXIGFg==" - }, - "@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "sha512": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==" - }, - "@opentui/core-linux-x64": { - "version": "0.0.0-20251106-788e97e4", - "sha512": "sha512-/E0XEBVzO4JEEhJGzfURF2tPxDE2oTODxlgNYYB1QbAuOsLcV69uSrwAjo1TxuIn4P78tBR+ZOlmONjroPqfbQ==" - }, - "@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "sha512": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==" - }, - "@opentui/core-darwin-arm64": { - "version": "0.0.0-20251106-788e97e4", - "sha512": "sha512-EOO8SSIYJBIh+Sd9bgVTiQmt+TEJmfg65/oym54J4zfDtCYlAqSaLcRnDe4TzB+4hejV9of8etrG3ZZACBJT+A==" - }, - "@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "sha512": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==" - }, - "@opentui/core-darwin-x64": { - "version": "0.0.0-20251106-788e97e4", - "sha512": "sha512-MUTt7CDbzL2afNGK8gJ4jUZd+AHiOUJEO0eJGDSfWU8DUs0zv8XoLZfaI5PPbkUPEL/7CEBMARAAiwfRtoG/4A==" - } - } -} diff --git a/nix/scripts/optional-metadata.ts b/nix/scripts/optional-metadata.ts index a419feabcb6..f7ca12359c5 100644 --- a/nix/scripts/optional-metadata.ts +++ b/nix/scripts/optional-metadata.ts @@ -13,16 +13,18 @@ const text = await Bun.file(file).text() const hashesPath = path.join(root, "nix/hashes.json") let optional: Record = {} -try { - // Pre-seeded metadata keeps optional bundles reproducible without network calls. - const data = await Bun.file(hashesPath).text() - const parsed = JSON.parse(data ?? "{}") +const hashesData = await Bun.file(hashesPath) + .text() + .catch((error) => { + console.warn(`missing-hashes\t${hashesPath}\t${(error as Error).message}`) + return "" + }) + +if (hashesData?.trim()) { + const parsed = JSON.parse(hashesData) if (parsed && typeof parsed.optional === "object" && parsed.optional !== null) { optional = parsed.optional as typeof optional } -} catch (error) { - console.error(`missing-hashes\t${hashesPath}\t${(error as Error).message}`) - process.exit(1) } const names = text diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 3cbf7f1f0cf..31555be9891 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -22,6 +22,23 @@ fi if [ -z "$OPTIONAL_FILE" ]; then OPTIONAL_FILE="nix/optional-packages.txt" fi + +if [ ! -f "$HASH_FILE" ]; then + cat <<'EOF' >"$HASH_FILE" +{ + "nodeModules": {}, + "optional": {}, + "metadata": {} +} +EOF +fi + +if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then + git add -N "$HASH_FILE" >/dev/null 2>&1 || true + fi +fi + export DUMMY export NIX_KEEP_OUTPUTS=1 export NIX_KEEP_DERIVATIONS=1 @@ -35,16 +52,6 @@ if [ -z "$SYSTEMS" ]; then exit 1 fi -if [ ! -f "$HASH_FILE" ]; then - cat <<'EOF' >"$HASH_FILE" -{ - "nodeModules": {}, - "optional": {}, - "metadata": {} -} -EOF -fi - cleanup() { if [ -n "${JSON_OUTPUT:-}" ] && [ -f "$JSON_OUTPUT" ]; then rm -f "$JSON_OUTPUT" @@ -219,6 +226,7 @@ write_metadata() { fi tmp=$(mktemp) jq \ + --arg stamp "$stamp" \ --arg ref "$ref" \ --arg trigger "$trigger" \ --arg commit "$commit" \ From 9d1cba2697aec934c7488b58dda5e79315641673 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 16:27:23 +1100 Subject: [PATCH 28/72] nix flake: verify npm packages sri --- nix/optional-packages/win32-x64.txt | 2 + packages/opencode/script/build.ts | 34 +++++----- packages/opencode/script/bundle-utils.ts | 86 ++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 nix/optional-packages/win32-x64.txt create mode 100644 packages/opencode/script/bundle-utils.ts diff --git a/nix/optional-packages/win32-x64.txt b/nix/optional-packages/win32-x64.txt new file mode 100644 index 00000000000..2f304b369b1 --- /dev/null +++ b/nix/optional-packages/win32-x64.txt @@ -0,0 +1,2 @@ +@parcel/watcher-win32-x64 +@opentui/core-win32-x64 diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index ec811d708ed..47987111262 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -5,11 +5,15 @@ import path from "path" import fs from "fs" import { $ } from "bun" import { fileURLToPath } from "url" +import { loadBundleMeta, ensureBundle } from "./bundle-utils" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") const modulesDir = path.join(dir, "../../node_modules") +const repoRoot = path.join(dir, "..", "..") +const hashesPath = path.join(repoRoot, "nix/hashes.json") +const bundleMeta = await loadBundleMeta(hashesPath) process.chdir(dir) @@ -32,23 +36,6 @@ const targets = singleFlag ? allTargets.filter(([os, arch]) => os === process.platform && arch === process.arch) : allTargets -async function bundleReady(name: string) { - const marker = Bun.file(path.join(modulesDir, name, "package.json")) - return marker.exists() -} - -async function ensureBundle(name: string, spec?: string) { - if (await bundleReady(name)) { - return - } - const target = path.join(modulesDir, name) - const tweak = name.replace(/^@/, "").replace(/\//g, "-") - await $`mkdir -p ${target}` - const pack = spec ?? name - await $`npm pack ${pack}`.cwd(modulesDir) - await $`tar -xf ${tweak}-*.tgz -C ${target} --strip-components=1`.cwd(modulesDir) -} - await $`rm -rf dist` const binaries: Record = {} @@ -58,10 +45,19 @@ for (const [os, arch] of targets) { await $`mkdir -p dist/${name}/bin` const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}` - await ensureBundle(opentui, `${opentui}@${pkg.dependencies["@opentui/core"]}`) + await ensureBundle({ + name: opentui, + modulesDir, + metadata: bundleMeta, + spec: `${opentui}@${pkg.dependencies["@opentui/core"]}`, + }) const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}` - await ensureBundle(watcher) + await ensureBundle({ + name: watcher, + modulesDir, + metadata: bundleMeta, + }) const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js")) const workerPath = "./src/cli/cmd/tui/worker.ts" diff --git a/packages/opencode/script/bundle-utils.ts b/packages/opencode/script/bundle-utils.ts new file mode 100644 index 00000000000..b45d0aca896 --- /dev/null +++ b/packages/opencode/script/bundle-utils.ts @@ -0,0 +1,86 @@ +import path from "path" +import { $ } from "bun" +import { promises as fsp } from "fs" + +const SRI_PREFIX = "sha512-" + +type MetaEntry = { + version?: string + sha512?: string + sha?: string +} + +export type BundleMeta = Record + +export async function loadBundleMeta(hashesPath: string) { + const input = await Bun.file(hashesPath).text() + const parsed = JSON.parse(input ?? "{}") + const optional = parsed.optional ?? {} + return optional as BundleMeta +} + +export async function bundleReady(modulesDir: string, name: string) { + const marker = Bun.file(path.join(modulesDir, name, "package.json")) + return marker.exists() +} + +const parseVersion = (spec?: string) => { + if (!spec) return + const idx = spec.lastIndexOf("@") + if (idx === -1) return + return spec.slice(idx + 1) +} + +type EnsureArgs = { + name: string + modulesDir: string + metadata: BundleMeta + spec?: string +} + +export async function ensureBundle({ name, modulesDir, metadata, spec }: EnsureArgs) { + if (await bundleReady(modulesDir, name)) { + return + } + const info = metadata[name] ?? {} + const expectedVersion = info.version + const expectedSha = info.sha512 ?? info.sha + let resolved = spec + if (!resolved && expectedVersion) { + resolved = `${name}@${expectedVersion}` + } + if (!resolved) { + throw new Error(`Missing spec for ${name}; update nix/hashes.json`) + } + const specVersion = parseVersion(resolved) + if (expectedVersion && specVersion && specVersion !== expectedVersion) { + throw new Error( + `Version mismatch for ${name}: resolved ${specVersion}, expected ${expectedVersion}`, + ) + } + if (!expectedSha) { + throw new Error(`Missing hash for ${name}; run nix/scripts/update-hashes.sh`) + } + const stdout = await $`npm pack ${resolved}`.cwd(modulesDir).text() + const tarball = stdout.trim().split("\n").pop()?.trim() + if (!tarball) { + throw new Error(`npm pack produced no output for ${resolved}`) + } + const tarPath = path.join(modulesDir, tarball) + const file = Bun.file(tarPath) + if (!(await file.exists())) { + throw new Error(`Tarball ${tarPath} missing after npm pack`) + } + const hasher = new Bun.CryptoHasher("sha512") + hasher.update(new Uint8Array(await file.arrayBuffer())) + const digest = SRI_PREFIX + hasher.digest("base64") + if (digest !== expectedSha) { + throw new Error( + `Hash mismatch for ${resolved}: expected ${expectedSha}, got ${digest}. Re-run hash refresh if intentional.`, + ) + } + const target = path.join(modulesDir, name) + await $`mkdir -p ${target}` + await $`tar -xf ${tarball} -C ${target} --strip-components=1`.cwd(modulesDir) + await fsp.rm(tarPath, { force: true }) +} From 99d0b45dd5a3b9a08509416d00ba931325b18ed1 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 16:48:21 +1100 Subject: [PATCH 29/72] nix flake: remove infinite loop check block (unnecessary) --- .github/workflows/update-nix-hashes.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 762694ffd20..7b36aa4db17 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -276,14 +276,6 @@ jobs: exit 0 fi - # Prevent infinite loop: check if the last commit was a hash update - LAST_COMMIT_MSG=$(git log -1 --pretty=%B) - if echo "$LAST_COMMIT_MSG" | grep -q "^nix: update hashes"; then - summarize "skipped (recent hash update)" - echo "Last commit was already a hash update. Skipping to prevent loop." - exit 0 - fi - git add flake.nix nix/node-modules.nix nix/hashes.json git commit -m "$COMMIT_MESSAGE" From d5e802f2931d1d4b16f2d11f7b4a29bc681c5623 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 18:28:38 +1100 Subject: [PATCH 30/72] nix flake: minor cleanup --- nix/scripts/canonicalize-node-modules.ts | 32 +----------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index 4d06cec952e..791d96dfe02 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -91,34 +91,4 @@ for (const line of rewrites.slice(0, 20)) { } if (rewrites.length > 20) { console.log(" ...") -} - -// NOTE: Current .bun/node_modules rewriting seems to be working fine for now. -// Could uncomment below in case of emergencies to force ALL workspace -// node_modules to point through .bun/node_modules (untested). -/* -async function rewriteWorkspaceLinks() { - const workspaces = await readdir(root) - for (const ws of workspaces) { - const wsModules = join(root, ws, "node_modules") - try { - const stat = await lstat(wsModules) - if (!stat.isDirectory()) continue - const entries = await readdir(wsModules) - for (const entry of entries) { - const linkPath = join(wsModules, entry) - const linkStat = await lstat(linkPath) - if (!linkStat.isSymbolicLink()) continue - const canonical = join(linkRoot, entry) - try { - await lstat(canonical) - const relTarget = relative(join(wsModules), canonical) - await rm(linkPath, { recursive: true, force: true }) - await symlink(relTarget, linkPath) - } catch {} - } - } catch {} - } -} -await rewriteWorkspaceLinks() -*/ +} \ No newline at end of file From 08c03d9bd77e3af5703218c45ea5e87aa8d16ecc Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 22:22:02 +1100 Subject: [PATCH 31/72] nix flake: track every asset Bun emits during compile, install them durign the installPhase --- nix/opencode.nix | 10 ++++++++++ nix/scripts/bun-build.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/nix/opencode.nix b/nix/opencode.nix index 3f9c446f6ff..4ec26fa3d0e 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -73,6 +73,16 @@ stdenvNoCC.mkDerivation (finalAttrs: { 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 runHook postInstall ''; diff --git a/nix/scripts/bun-build.ts b/nix/scripts/bun-build.ts index de275c87d69..2e63e256e7b 100644 --- a/nix/scripts/bun-build.ts +++ b/nix/scripts/bun-build.ts @@ -18,6 +18,29 @@ if (!target) { 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 targetPath = path.join(pkg, file) + if (fs.existsSync(targetPath)) { + fs.rmSync(targetPath, { force: true }) + } + } +} + +removeTrackedAssets() + const result = await Bun.build({ conditions: ["browser"], tsconfig: "./tsconfig.json", @@ -45,6 +68,19 @@ if (!result.success) { throw new Error("Compilation failed") } +const assetOutputs = result.outputs?.filter((item) => item.kind === "asset") ?? [] +const trackedAssets: string[] = [] +for (const asset of assetOutputs) { + const file = path.basename(asset.path) + const dest = path.join(pkg, file) + await Bun.write(dest, Bun.file(asset.path)) + trackedAssets.push(file) +} +await Bun.write( + manifestPath, + trackedAssets.length > 0 ? trackedAssets.join("\n") + "\n" : "", +) + const bundle = await Bun.build({ entrypoints: [worker], tsconfig: "./tsconfig.json", From e80beb00ccceaeddc8f4b12ff19df71fbc719df0 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 8 Nov 2025 23:32:30 +1100 Subject: [PATCH 32/72] nix flake: resolve wasm assets to absolute filepaths nix flake: track every asset Bun emits during compile, install them durign the installPhase --- nix/hashes.json | 51 ++++++++++++++++++++++++++++++ nix/scripts/bun-build.ts | 27 ++++++++++------ packages/opencode/src/tool/bash.ts | 14 ++++++-- 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 nix/hashes.json diff --git a/nix/hashes.json b/nix/hashes.json new file mode 100644 index 00000000000..91bbc6f4e79 --- /dev/null +++ b/nix/hashes.json @@ -0,0 +1,51 @@ +{ + "nodeModules": { + "x86_64-linux": "sha256-gwFZjTKos3pr5GE3T6+wJT0lQ3cjSO5GahwiOfMArBc=", + "aarch64-linux": "sha256-2mytvbm1D1UkVTEfv0oJD+kmkuDVX87cdbe1azZSYqg=", + "aarch64-darwin": "sha256-pIDGFHTLSOt3svygdD+YgRqgy/PdFp+PaAPm5c03+cw=", + "x86_64-darwin": "sha256-Mic3Lle8XOc3cS0O2Ll7X1pcLbUuDjb9rcfnYI9ceSU=" + }, + "optional": { + "@opentui/core-darwin-arm64": { + "version": "0.1.39", + "sha512": "sha512-tDUdNdzGeylkDWTiDIy/CalM/9nIeDwMZGN0Q6FLqABnAplwBhdIH2w/gInAcMaTyagm7Qk88p398Wbnxa9uyg==" + }, + "@opentui/core-darwin-x64": { + "version": "0.1.39", + "sha512": "sha512-dWXXNUpdi3ndd+6WotQezsO7g54MLSc/6DmYcl0p7fZrQFct8fX0c9ny/S0xAusNHgBGVS5j5FWE75Mx79301Q==" + }, + "@opentui/core-linux-arm64": { + "version": "0.1.39", + "sha512": "sha512-ookQbxLjsg51iwGb6/KTxCfiVRtE9lSE2OVFLLYork8iVzxg81jX29Uoxe1knZ8FjOJ0+VqTzex2IqQH6mjJlw==" + }, + "@opentui/core-linux-x64": { + "version": "0.1.39", + "sha512": "sha512-CeXVNa3hB7gTYKYoZAuMtxWMIXn2rPhmXLkHKpEvXvDRjODFDk8wN1AIVnT5tfncXbWNa5z35BhmqewpGkl4oQ==" + }, + "@opentui/core-win32-x64": { + "version": "0.1.39", + "sha512": "sha512-lLXeQUBg6Wlenauwd+xaBD+0HT4YIcONeZUTHA+Gyd/rqVhxId97rhhzFikp3bBTvNJlYAscJI3yIF2JvRiFNQ==" + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "sha512": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==" + }, + "@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "sha512": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==" + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "sha512": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "sha512": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==" + }, + "@parcel/watcher-win32-x64": { + "version": "2.5.1", + "sha512": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==" + } + }, + "metadata": {} +} diff --git a/nix/scripts/bun-build.ts b/nix/scripts/bun-build.ts index 2e63e256e7b..3ecbf42a993 100644 --- a/nix/scripts/bun-build.ts +++ b/nix/scripts/bun-build.ts @@ -39,6 +39,15 @@ const removeTrackedAssets = () => { } } +const trackedAssets = new Set() + +const addAsset = async (assetPath: string) => { + const file = path.basename(assetPath) + const dest = path.join(pkg, file) + await Bun.write(dest, Bun.file(assetPath)) + trackedAssets.add(file) +} + removeTrackedAssets() const result = await Bun.build({ @@ -69,17 +78,9 @@ if (!result.success) { } const assetOutputs = result.outputs?.filter((item) => item.kind === "asset") ?? [] -const trackedAssets: string[] = [] for (const asset of assetOutputs) { - const file = path.basename(asset.path) - const dest = path.join(pkg, file) - await Bun.write(dest, Bun.file(asset.path)) - trackedAssets.push(file) + await addAsset(asset.path) } -await Bun.write( - manifestPath, - trackedAssets.length > 0 ? trackedAssets.join("\n") + "\n" : "", -) const bundle = await Bun.build({ entrypoints: [worker], @@ -98,6 +99,11 @@ if (!bundle.success) { throw new Error("Worker compilation failed") } +const workerAssetOutputs = bundle.outputs?.filter((item) => item.kind === "asset") ?? [] +for (const asset of workerAssetOutputs) { + await addAsset(asset.path) +} + const output = bundle.outputs.find((item) => item.kind === "entry-point") if (!output) { throw new Error("Worker build produced no entry-point output") @@ -108,4 +114,7 @@ const src = output.path await Bun.write(dest, Bun.file(src)) fs.rmSync(path.dirname(src), { recursive: true, force: true }) +const assetList = Array.from(trackedAssets) +await Bun.write(manifestPath, assetList.length > 0 ? assetList.join("\n") + "\n" : "") + console.log("Build successful!") diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index a3ccfc397f2..7c168626cc0 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -11,6 +11,7 @@ import { $ } from "bun" import { Filesystem } from "@/util/filesystem" import { Wildcard } from "@/util/wildcard" import { Permission } from "@/permission" +import { fileURLToPath } from "url" const MAX_OUTPUT_LENGTH = 30_000 const DEFAULT_TIMEOUT = 1 * 60 * 1000 @@ -19,20 +20,29 @@ const SIGKILL_TIMEOUT_MS = 200 export const log = Log.create({ service: "bash-tool" }) +const resolveWasm = (asset: string) => { + if (asset.startsWith("file://")) return fileURLToPath(asset) + if (asset.startsWith("/")) return asset + const url = new URL(asset, import.meta.url) + return fileURLToPath(url) +} + const parser = lazy(async () => { const { Parser } = await import("web-tree-sitter") const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" }, }) + const treePath = resolveWasm(treeWasm) await Parser.init({ locateFile() { - return treeWasm + return treePath }, }) const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { with: { type: "wasm" }, }) - const bashLanguage = await Language.load(bashWasm) + const bashPath = resolveWasm(bashWasm) + const bashLanguage = await Language.load(bashPath) const p = new Parser() p.setLanguage(bashLanguage) return p From 77ca8987e1252cbc50c0d57012ffedd7185597e1 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 9 Nov 2025 04:02:06 +1100 Subject: [PATCH 33/72] nix flake: remove (1) of the unneeded scripts --- nix/node-modules.nix | 1 - nix/opencode.nix | 1 - nix/scripts/normalize-node-modules.ts | 38 --------------------------- 3 files changed, 40 deletions(-) delete mode 100644 nix/scripts/normalize-node-modules.ts diff --git a/nix/node-modules.nix b/nix/node-modules.nix index da65af9056a..388948f58dd 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -81,7 +81,6 @@ stdenvNoCC.mkDerivation { rm -f optional-packages.txt optional-metadata.txt bun --bun ${args.canonicalizeScript} - bun --bun ${args.normalizeScript} runHook postBuild ''; diff --git a/nix/opencode.nix b/nix/opencode.nix index 4ec26fa3d0e..ec65d955bfc 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -10,7 +10,6 @@ let canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; optionalMetadataScript = scripts + "/optional-metadata.ts"; verifyShaScript = scripts + "/verify-sha.ts"; - normalizeScript = scripts + "/normalize-node-modules.ts"; } ); in diff --git a/nix/scripts/normalize-node-modules.ts b/nix/scripts/normalize-node-modules.ts deleted file mode 100644 index 00c3ec7a8ad..00000000000 --- a/nix/scripts/normalize-node-modules.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { join } from "path" -import { lstat, lutimes, readdir, utimes } from "fs/promises" - -const argv = Bun.argv.slice(2) -const root = process.cwd() -const base = argv[0] ?? "node_modules" -const target = join(root, base) -const epochRaw = Bun.env.SOURCE_DATE_EPOCH ?? "1" -const epoch = Number.parseInt(epochRaw, 10) - -if (!Number.isFinite(epoch)) { - console.error(`[normalize-node-modules] invalid SOURCE_DATE_EPOCH: ${epochRaw}`) - process.exit(1) -} - -const seen = new Set() -const stack = [target] - -while (stack.length > 0) { - const next = stack.pop() - if (!next) continue - if (seen.has(next)) continue - seen.add(next) - const info = await lstat(next) - if (info.isDirectory()) { - const entries = await readdir(next) - for (const entry of entries) { - stack.push(join(next, entry)) - } - } - if (info.isSymbolicLink()) { - await lutimes(next, epoch, epoch) - continue - } - await utimes(next, epoch, epoch) -} - -console.log("[normalize-node-modules] normalized timestamps for", seen.size, "paths") From 6305089b6dd047cdbe6179c1effee8bf3c0af5d1 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 9 Nov 2025 12:03:56 +1100 Subject: [PATCH 34/72] nix flake: add `[skip ci]` to auto commit message --- .github/workflows/update-nix-hashes.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 7b36aa4db17..498d23c703b 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -233,8 +233,9 @@ jobs: if [ "${HAS_HASH_DATA}" != "true" ]; then MESSAGE="nix: bootstrap hashes (${LABEL})" fi + BODY="[skip ci]" { - echo "message=$MESSAGE" + echo "message=$MESSAGE"$'\n'"$BODY" echo "label=$LABEL" echo "summary=$SUMMARY" } >> "$GITHUB_OUTPUT" From 7cc268fec1235cd097c27976cd796552ca94b8a9 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 9 Nov 2025 21:03:36 +1100 Subject: [PATCH 35/72] nix flake: fix commit metadata --- .github/workflows/update-nix-hashes.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 498d23c703b..37a5f0eead7 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -235,7 +235,8 @@ jobs: fi BODY="[skip ci]" { - echo "message=$MESSAGE"$'\n'"$BODY" + echo "message=$MESSAGE" + echo "body=$BODY" echo "label=$LABEL" echo "summary=$SUMMARY" } >> "$GITHUB_OUTPUT" From eb913954088abb95ef1f5bbe5cb2a5a78880767c Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 10 Nov 2025 11:08:19 +0000 Subject: [PATCH 36/72] nix: update hashes (branch:nix-support) --- nix/hashes.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 91bbc6f4e79..442ef4d65e1 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,30 +1,30 @@ { "nodeModules": { - "x86_64-linux": "sha256-gwFZjTKos3pr5GE3T6+wJT0lQ3cjSO5GahwiOfMArBc=", - "aarch64-linux": "sha256-2mytvbm1D1UkVTEfv0oJD+kmkuDVX87cdbe1azZSYqg=", - "aarch64-darwin": "sha256-pIDGFHTLSOt3svygdD+YgRqgy/PdFp+PaAPm5c03+cw=", - "x86_64-darwin": "sha256-Mic3Lle8XOc3cS0O2Ll7X1pcLbUuDjb9rcfnYI9ceSU=" + "x86_64-linux": "sha256-4ZNhLYNMoGu/3Fe60ctzjP3LpkRiO1tVhSbZTlzE5bk=", + "aarch64-linux": "sha256-DRQMrcOjyEJ/keccyixZT4pZ9nSzAYUPb7J1zHIzusk=", + "aarch64-darwin": "sha256-3JU0mt8L3olSVU014NlZ7gPDXEFsshUi6fwHtHKvMiM=", + "x86_64-darwin": "sha256-xPj370tbrauEJW8Q2ySPTNK+sTtHU23C8Uvd1O/VxVE=" }, "optional": { "@opentui/core-darwin-arm64": { - "version": "0.1.39", - "sha512": "sha512-tDUdNdzGeylkDWTiDIy/CalM/9nIeDwMZGN0Q6FLqABnAplwBhdIH2w/gInAcMaTyagm7Qk88p398Wbnxa9uyg==" + "version": "0.0.0-20251108-0c7899b1", + "sha512": "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g==" }, "@opentui/core-darwin-x64": { - "version": "0.1.39", - "sha512": "sha512-dWXXNUpdi3ndd+6WotQezsO7g54MLSc/6DmYcl0p7fZrQFct8fX0c9ny/S0xAusNHgBGVS5j5FWE75Mx79301Q==" + "version": "0.0.0-20251108-0c7899b1", + "sha512": "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg==" }, "@opentui/core-linux-arm64": { - "version": "0.1.39", - "sha512": "sha512-ookQbxLjsg51iwGb6/KTxCfiVRtE9lSE2OVFLLYork8iVzxg81jX29Uoxe1knZ8FjOJ0+VqTzex2IqQH6mjJlw==" + "version": "0.0.0-20251108-0c7899b1", + "sha512": "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA==" }, "@opentui/core-linux-x64": { - "version": "0.1.39", - "sha512": "sha512-CeXVNa3hB7gTYKYoZAuMtxWMIXn2rPhmXLkHKpEvXvDRjODFDk8wN1AIVnT5tfncXbWNa5z35BhmqewpGkl4oQ==" + "version": "0.0.0-20251108-0c7899b1", + "sha512": "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q==" }, "@opentui/core-win32-x64": { - "version": "0.1.39", - "sha512": "sha512-lLXeQUBg6Wlenauwd+xaBD+0HT4YIcONeZUTHA+Gyd/rqVhxId97rhhzFikp3bBTvNJlYAscJI3yIF2JvRiFNQ==" + "version": "0.0.0-20251108-0c7899b1", + "sha512": "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg==" }, "@parcel/watcher-darwin-arm64": { "version": "2.5.1", From c3b53cf867a3e62be4f8a16f66c197ffed777e1e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Mon, 10 Nov 2025 22:46:08 +1100 Subject: [PATCH 37/72] nix flake: add more explicit sorting --- nix/node-modules.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 388948f58dd..5d627210ad3 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -65,7 +65,7 @@ stdenvNoCC.mkDerivation { echo "Installed optional package $name -> $dest" - for ws in packages/*; do + for ws in $(find packages -maxdepth 1 -mindepth 1 -type d | sort); do [ -d "$ws/node_modules" ] || continue ws_dest="$ws/node_modules/$remainder" if [ -n "$scope" ]; then @@ -93,7 +93,7 @@ stdenvNoCC.mkDerivation { dest="$out/$rel" mkdir -p "$(dirname "$dest")" cp -R "$dir" "$dest" - done < <(find . -type d -name node_modules -prune) + done < <(find . -type d -name node_modules -prune | sort) runHook postInstall ''; From 3071a0351a2c5e9c648bbe4882a646293a0f0014 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 10 Nov 2025 11:57:30 +0000 Subject: [PATCH 38/72] nix: update hashes (branch:nix-support-dev) --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 442ef4d65e1..dde3e447c2b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-4ZNhLYNMoGu/3Fe60ctzjP3LpkRiO1tVhSbZTlzE5bk=", + "x86_64-linux": "sha256-mLjOOpjHChJwB0dlroub6eWGu4dnEstJ4LuLbIxUqsA=", "aarch64-linux": "sha256-DRQMrcOjyEJ/keccyixZT4pZ9nSzAYUPb7J1zHIzusk=", "aarch64-darwin": "sha256-3JU0mt8L3olSVU014NlZ7gPDXEFsshUi6fwHtHKvMiM=", "x86_64-darwin": "sha256-xPj370tbrauEJW8Q2ySPTNK+sTtHU23C8Uvd1O/VxVE=" From 0f79a472bc33f5482cf89701e9d32c7eaa2ce5b2 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Mon, 10 Nov 2025 22:46:08 +1100 Subject: [PATCH 39/72] nix flake: add more explicit sorting --- nix/node-modules.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 388948f58dd..5d627210ad3 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -65,7 +65,7 @@ stdenvNoCC.mkDerivation { echo "Installed optional package $name -> $dest" - for ws in packages/*; do + for ws in $(find packages -maxdepth 1 -mindepth 1 -type d | sort); do [ -d "$ws/node_modules" ] || continue ws_dest="$ws/node_modules/$remainder" if [ -n "$scope" ]; then @@ -93,7 +93,7 @@ stdenvNoCC.mkDerivation { dest="$out/$rel" mkdir -p "$(dirname "$dest")" cp -R "$dir" "$dest" - done < <(find . -type d -name node_modules -prune) + done < <(find . -type d -name node_modules -prune | sort) runHook postInstall ''; From 0c4ef6c446cccf9557b12d14001f8b6ac405d95b Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 10 Nov 2025 11:57:30 +0000 Subject: [PATCH 40/72] nix: update hashes (branch:nix-support-dev) --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 442ef4d65e1..dde3e447c2b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-4ZNhLYNMoGu/3Fe60ctzjP3LpkRiO1tVhSbZTlzE5bk=", + "x86_64-linux": "sha256-mLjOOpjHChJwB0dlroub6eWGu4dnEstJ4LuLbIxUqsA=", "aarch64-linux": "sha256-DRQMrcOjyEJ/keccyixZT4pZ9nSzAYUPb7J1zHIzusk=", "aarch64-darwin": "sha256-3JU0mt8L3olSVU014NlZ7gPDXEFsshUi6fwHtHKvMiM=", "x86_64-darwin": "sha256-xPj370tbrauEJW8Q2ySPTNK+sTtHU23C8Uvd1O/VxVE=" From 45972b5d8468e2a20a8b5ed20bb3c75a68425dd5 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Tue, 11 Nov 2025 22:35:44 +1100 Subject: [PATCH 41/72] nix flake: remove a redundant builds from workflow --- .github/workflows/update-nix-hashes.yml | 90 +++++++++++++------------ 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 37a5f0eead7..75b8889d53c 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -63,55 +63,61 @@ jobs: git config --global user.email "opencode@sst.dev" git config --global user.name "opencode" - - name: Preflight build - id: preflight + - name: Build and extract hashes + id: build_extract continue-on-error: true - run: | - echo "Verifying existing hashes for ${{ matrix.system }}..." - nix build .#packages.${{ matrix.system }}.default --system ${{ matrix.system }} -L - echo "${{ matrix.system }} preflight build successful." - - - name: Summarize preflight status - id: preflight_status - run: | - set -e - if [ "${PREFLIGHT_OUTCOME}" = "failure" ]; then - echo "Preflight detected stale hashes." - echo "failed=true" >> "$GITHUB_OUTPUT" - else - echo "Preflight build passed." - echo "failed=false" >> "$GITHUB_OUTPUT" - fi - env: - PREFLIGHT_OUTCOME: ${{ steps.preflight.outcome }} - - - name: Update build output hashes - env: - SYSTEMS: ${{ matrix.system }} run: | set -euo pipefail - nix/scripts/update-hashes.sh + echo "Building for ${{ matrix.system }} to verify/extract hashes..." + + BUILD_LOG=$(mktemp) + nix build .#packages.${{ matrix.system }}.default --system ${{ matrix.system }} -L 2>&1 | tee "$BUILD_LOG" || BUILD_FAILED=true + + if [ -z "${BUILD_FAILED:-}" ]; then + echo "Build successful with current hashes." + echo "needs_update=false" >> "$GITHUB_OUTPUT" + rm -f "$BUILD_LOG" + exit 0 + fi + + echo "Build failed, extracting correct hash..." + CORRECT_HASH=$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true) + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH=$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true) + fi + + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to extract hash from build output" + tail -100 "$BUILD_LOG" + rm -f "$BUILD_LOG" + exit 1 + fi + + echo "Extracted hash: $CORRECT_HASH" + echo "needs_update=true" >> "$GITHUB_OUTPUT" + echo "hash=$CORRECT_HASH" >> "$GITHUB_OUTPUT" + rm -f "$BUILD_LOG" - - name: Detect hash changes - id: hash_change + - name: Update hash file + if: steps.build_extract.outputs.needs_update == 'true' run: | set -euo pipefail - STATUS="$(git status --short -- nix/hashes.json || true)" - if [ -z "$STATUS" ]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No hash changes detected." - else - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "Hashes updated; running post-update verification." + HASH_FILE="nix/hashes.json" + SYSTEM="${{ matrix.system }}" + CORRECT_HASH="${{ steps.build_extract.outputs.hash }}" + + if [ ! -f "$HASH_FILE" ]; then + mkdir -p "$(dirname "$HASH_FILE")" + jq -n '{nodeModules: {}, optional: {}, metadata: {}}' > "$HASH_FILE" fi - - - name: Verification build - if: steps.hash_change.outputs.changed == 'true' - || steps.preflight_status.outputs.failed == 'true' - run: | - echo "Running post-update verification build for ${{ matrix.system }}..." - nix build .#packages.${{ matrix.system }}.default --system ${{ matrix.system }} -L - echo "${{ matrix.system }} build successful." + + echo "Updating hash for ${SYSTEM} to ${CORRECT_HASH}" + tmp=$(mktemp) + jq --arg system "$SYSTEM" --arg value "$CORRECT_HASH" '.nodeModules[$system] = $value' "$HASH_FILE" >"$tmp" + mv "$tmp" "$HASH_FILE" + + echo "Hash file updated." - name: Upload hash changes uses: actions/upload-artifact@v4 From 3c160aa206b87fb36b7a1853b2321f109b1494df Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 09:45:54 +1100 Subject: [PATCH 42/72] nix flake: remove commit metadata info --- .github/workflows/update-nix-hashes.yml | 64 ++----------------------- nix/scripts/update-hashes.sh | 62 +----------------------- 2 files changed, 5 insertions(+), 121 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 75b8889d53c..bc50bb792fd 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -109,7 +109,7 @@ jobs: if [ ! -f "$HASH_FILE" ]; then mkdir -p "$(dirname "$HASH_FILE")" - jq -n '{nodeModules: {}, optional: {}, metadata: {}}' > "$HASH_FILE" + jq -n '{nodeModules: {}, optional: {}}' > "$HASH_FILE" fi echo "Updating hash for ${SYSTEM} to ${CORRECT_HASH}" @@ -169,7 +169,7 @@ jobs: BASE_HASH="nix/hashes.json" if [ ! -f "$BASE_HASH" ]; then mkdir -p "$(dirname "$BASE_HASH")" - jq -n '{nodeModules: {}, optional: {}, metadata: {}}' > "$BASE_HASH" + jq -n '{nodeModules: {}, optional: {}}' > "$BASE_HASH" fi for system in x86_64-linux aarch64-linux aarch64-darwin x86_64-darwin; do if [ -f "hash-artifacts/hashes-${system}/hashes.json" ]; then @@ -198,62 +198,8 @@ jobs: echo "has_data=false" >> "$GITHUB_OUTPUT" fi - - name: Derive commit metadata - id: commit_meta - run: | - set -euo pipefail - EVENT="$GITHUB_EVENT_NAME" - LABEL="$GITHUB_REF_NAME" - SUMMARY="Hashes refreshed for $LABEL" - TAG_NAME="" - PR_NUMBER="" - if [ -f "$GITHUB_EVENT_PATH" ]; then - TAG_NAME="$(jq -r '.release.tag_name // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true)" - PR_NUMBER="$(jq -r '.number // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true)" - fi - case "$EVENT" in - release) - LABEL="release:${GITHUB_REF_NAME}" - if [ -n "$TAG_NAME" ]; then - LABEL="release:${TAG_NAME}" - fi - SUMMARY="Hashes refreshed for ${LABEL}" - ;; - pull_request) - LABEL="pr:${GITHUB_REF_NAME}" - if [ -n "$PR_NUMBER" ]; then - LABEL="pr:${PR_NUMBER}" - fi - SUMMARY="Hashes refreshed for ${LABEL}" - ;; - workflow_dispatch) - LABEL="manual:${GITHUB_REF_NAME}" - SUMMARY="Manual hash refresh for ${LABEL}" - ;; - *) - LABEL="branch:${GITHUB_REF_NAME}" - SUMMARY="Hashes refreshed for ${LABEL}" - ;; - esac - MESSAGE="nix: update hashes (${LABEL})" - if [ "${HAS_HASH_DATA}" != "true" ]; then - MESSAGE="nix: bootstrap hashes (${LABEL})" - fi - BODY="[skip ci]" - { - echo "message=$MESSAGE" - echo "body=$BODY" - echo "label=$LABEL" - echo "summary=$SUMMARY" - } >> "$GITHUB_OUTPUT" - env: - HAS_HASH_DATA: ${{ steps.merge_hashes.outputs.has_data }} - - name: Commit Nix hash changes env: - COMMIT_MESSAGE: ${{ steps.commit_meta.outputs.message }} - UPDATE_LABEL: ${{ steps.commit_meta.outputs.label }} - UPDATE_SUMMARY: ${{ steps.commit_meta.outputs.summary }} TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} run: | set -euo pipefail @@ -263,13 +209,9 @@ jobs: { echo "### Nix Hash Update" echo "" - echo "- context: ${UPDATE_LABEL}" echo "- ref: ${GITHUB_REF_NAME}" echo "- status: ${status}" } >> "$GITHUB_STEP_SUMMARY" - if [ -n "${UPDATE_SUMMARY:-}" ]; then - echo "- note: ${UPDATE_SUMMARY}" >> "$GITHUB_STEP_SUMMARY" - fi 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 @@ -285,7 +227,7 @@ jobs: fi git add flake.nix nix/node-modules.nix nix/hashes.json - git commit -m "$COMMIT_MESSAGE" + git commit -m "Update Nix hashes" BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" git push origin HEAD:"$BRANCH" diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 31555be9891..0ab60b64015 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -27,8 +27,7 @@ if [ ! -f "$HASH_FILE" ]; then cat <<'EOF' >"$HASH_FILE" { "nodeModules": {}, - "optional": {}, - "metadata": {} + "optional": {} } EOF fi @@ -191,61 +190,4 @@ for SYSTEM in $SYSTEMS; do rm -f "$BUILD_LOG" unset BUILD_LOG -done - -write_metadata() { - local tmp stamp ref trigger commit run_url - stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - ref="${HASH_SOURCE_REF:-${GITHUB_REF_NAME:-}}" - if [ -z "$ref" ] && command -v git >/dev/null 2>&1; then - ref="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" - if [ "$ref" = "HEAD" ]; then - ref="$(git rev-parse --short HEAD 2>/dev/null || true)" - fi - fi - if [ -z "$ref" ]; then - ref="unknown" - fi - trigger="${HASH_TRIGGER_COMMIT:-${GITHUB_SHA:-}}" - if [ -z "$trigger" ] && command -v git >/dev/null 2>&1; then - trigger="$(git rev-parse HEAD 2>/dev/null || true)" - fi - if [ -z "$trigger" ]; then - trigger="unknown" - fi - commit="${HASH_UPDATE_COMMIT:-}" - if [ -z "$commit" ] && command -v git >/dev/null 2>&1; then - commit="$(git rev-parse HEAD 2>/dev/null || true)" - fi - if [ -z "$commit" ]; then - commit="$trigger" - fi - run_url="${HASH_SOURCE_RUN:-}" - if [ -z "$run_url" ] && [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then - run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - fi - tmp=$(mktemp) - jq \ - --arg stamp "$stamp" \ - --arg ref "$ref" \ - --arg trigger "$trigger" \ - --arg commit "$commit" \ - ' - .metadata = { - stamp: $stamp, - ref: $ref, - trigger: $trigger, - commit: $commit - } - ' \ - "$HASH_FILE" >"$tmp" - mv "$tmp" "$HASH_FILE" - if [ -n "$run_url" ]; then - tmp=$(mktemp) - jq --arg run "$run_url" '.metadata.workflowRun = $run' "$HASH_FILE" >"$tmp" - mv "$tmp" "$HASH_FILE" - fi - echo "metadata updated: ref=${ref} commit=${commit}" -} - -write_metadata +done \ No newline at end of file From 22b928fb2085c64efc0f9cc443ade812d9f2ca67 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 11 Nov 2025 22:57:23 +0000 Subject: [PATCH 43/72] Update Nix hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index dde3e447c2b..c98af56806f 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,9 +1,9 @@ { "nodeModules": { - "x86_64-linux": "sha256-mLjOOpjHChJwB0dlroub6eWGu4dnEstJ4LuLbIxUqsA=", - "aarch64-linux": "sha256-DRQMrcOjyEJ/keccyixZT4pZ9nSzAYUPb7J1zHIzusk=", - "aarch64-darwin": "sha256-3JU0mt8L3olSVU014NlZ7gPDXEFsshUi6fwHtHKvMiM=", - "x86_64-darwin": "sha256-xPj370tbrauEJW8Q2ySPTNK+sTtHU23C8Uvd1O/VxVE=" + "x86_64-linux": "sha256-U2bUPxnKrUN6pRIDBIiyrg+CqghaoneSqY4BI5zXs5w=", + "aarch64-linux": "sha256-bd7FPZzhfhK4zUNcP+1hyXLYLyfetWKwBgGandk7x3o=", + "aarch64-darwin": "sha256-qUHSVtDkUcq7bUO3QbPUScqCcwpon2FqebRFA/NcXzA=", + "x86_64-darwin": "sha256-1yhZhwXvO/AlUhdN03eUqb7Jkyk2cgr8YwZd8RW3BWw=" }, "optional": { "@opentui/core-darwin-arm64": { From bbfa63da7b44ed5bc8f6d51d44139b7e13a7451c Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 11 Nov 2025 23:13:12 +0000 Subject: [PATCH 44/72] Update Nix hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index dde3e447c2b..132d9eb4f7a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,9 +1,9 @@ { "nodeModules": { - "x86_64-linux": "sha256-mLjOOpjHChJwB0dlroub6eWGu4dnEstJ4LuLbIxUqsA=", - "aarch64-linux": "sha256-DRQMrcOjyEJ/keccyixZT4pZ9nSzAYUPb7J1zHIzusk=", - "aarch64-darwin": "sha256-3JU0mt8L3olSVU014NlZ7gPDXEFsshUi6fwHtHKvMiM=", - "x86_64-darwin": "sha256-xPj370tbrauEJW8Q2ySPTNK+sTtHU23C8Uvd1O/VxVE=" + "x86_64-linux": "sha256-+dsSECijB/O6dA87Rdm8mcSrPn7FoMaNM4pzgouyFn0=", + "aarch64-linux": "sha256-CoF/CFOeCK1biwA8lPEQelEMhB0w2vD8dj8GbTCu7D8=", + "aarch64-darwin": "sha256-qUHSVtDkUcq7bUO3QbPUScqCcwpon2FqebRFA/NcXzA=", + "x86_64-darwin": "sha256-1yhZhwXvO/AlUhdN03eUqb7Jkyk2cgr8YwZd8RW3BWw=" }, "optional": { "@opentui/core-darwin-arm64": { From 3851f01d11cb5112f8626f6794b4746c31c40f7d Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 17:46:44 +1100 Subject: [PATCH 45/72] nix flake: use '--linker=isolated' in bun install --- nix/node-modules.nix | 3 ++- packages/opencode/script/build.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 5d627210ad3..a03c004de09 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -28,7 +28,8 @@ stdenvNoCC.mkDerivation { bun install \ --frozen-lockfile \ --ignore-scripts \ - --no-progress + --no-progress \ + --linker=isolated cp ${optionalPackagesFile} optional-packages.txt diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index f5c04d9ed96..10ece513438 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -88,8 +88,8 @@ const targets = singleFlag await $`rm -rf dist` const binaries: Record = {} -await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` -await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` +await $`bun install --frozen-lockfile --linker=isolated --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` +await $`bun install --frozen-lockfile --linker=isolated --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` for (const item of targets) { const name = [ pkg.name, From 1e13436e1b057565219a9d233bcef90a3e5c3a61 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 19:01:40 +1100 Subject: [PATCH 46/72] nix flake: revert bundle-utils --- packages/opencode/script/build.ts | 20 ------ packages/opencode/script/bundle-utils.ts | 86 ------------------------ 2 files changed, 106 deletions(-) delete mode 100644 packages/opencode/script/bundle-utils.ts diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 10ece513438..b196ae07bc9 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -5,15 +5,10 @@ import path from "path" import fs from "fs" import { $ } from "bun" import { fileURLToPath } from "url" -import { loadBundleMeta, ensureBundle } from "./bundle-utils" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") -const modulesDir = path.join(dir, "../../node_modules") -const repoRoot = path.join(dir, "..", "..") -const hashesPath = path.join(repoRoot, "nix/hashes.json") -const bundleMeta = await loadBundleMeta(hashesPath) process.chdir(dir) @@ -103,21 +98,6 @@ for (const item of targets) { console.log(`building ${name}`) await $`mkdir -p dist/${name}/bin` - const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}` - await ensureBundle({ - name: opentui, - modulesDir, - metadata: bundleMeta, - spec: `${opentui}@${pkg.dependencies["@opentui/core"]}`, - }) - - const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}` - await ensureBundle({ - name: watcher, - modulesDir, - metadata: bundleMeta, - }) - const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js")) const workerPath = "./src/cli/cmd/tui/worker.ts" diff --git a/packages/opencode/script/bundle-utils.ts b/packages/opencode/script/bundle-utils.ts deleted file mode 100644 index b45d0aca896..00000000000 --- a/packages/opencode/script/bundle-utils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import path from "path" -import { $ } from "bun" -import { promises as fsp } from "fs" - -const SRI_PREFIX = "sha512-" - -type MetaEntry = { - version?: string - sha512?: string - sha?: string -} - -export type BundleMeta = Record - -export async function loadBundleMeta(hashesPath: string) { - const input = await Bun.file(hashesPath).text() - const parsed = JSON.parse(input ?? "{}") - const optional = parsed.optional ?? {} - return optional as BundleMeta -} - -export async function bundleReady(modulesDir: string, name: string) { - const marker = Bun.file(path.join(modulesDir, name, "package.json")) - return marker.exists() -} - -const parseVersion = (spec?: string) => { - if (!spec) return - const idx = spec.lastIndexOf("@") - if (idx === -1) return - return spec.slice(idx + 1) -} - -type EnsureArgs = { - name: string - modulesDir: string - metadata: BundleMeta - spec?: string -} - -export async function ensureBundle({ name, modulesDir, metadata, spec }: EnsureArgs) { - if (await bundleReady(modulesDir, name)) { - return - } - const info = metadata[name] ?? {} - const expectedVersion = info.version - const expectedSha = info.sha512 ?? info.sha - let resolved = spec - if (!resolved && expectedVersion) { - resolved = `${name}@${expectedVersion}` - } - if (!resolved) { - throw new Error(`Missing spec for ${name}; update nix/hashes.json`) - } - const specVersion = parseVersion(resolved) - if (expectedVersion && specVersion && specVersion !== expectedVersion) { - throw new Error( - `Version mismatch for ${name}: resolved ${specVersion}, expected ${expectedVersion}`, - ) - } - if (!expectedSha) { - throw new Error(`Missing hash for ${name}; run nix/scripts/update-hashes.sh`) - } - const stdout = await $`npm pack ${resolved}`.cwd(modulesDir).text() - const tarball = stdout.trim().split("\n").pop()?.trim() - if (!tarball) { - throw new Error(`npm pack produced no output for ${resolved}`) - } - const tarPath = path.join(modulesDir, tarball) - const file = Bun.file(tarPath) - if (!(await file.exists())) { - throw new Error(`Tarball ${tarPath} missing after npm pack`) - } - const hasher = new Bun.CryptoHasher("sha512") - hasher.update(new Uint8Array(await file.arrayBuffer())) - const digest = SRI_PREFIX + hasher.digest("base64") - if (digest !== expectedSha) { - throw new Error( - `Hash mismatch for ${resolved}: expected ${expectedSha}, got ${digest}. Re-run hash refresh if intentional.`, - ) - } - const target = path.join(modulesDir, name) - await $`mkdir -p ${target}` - await $`tar -xf ${tarball} -C ${target} --strip-components=1`.cwd(modulesDir) - await fsp.rm(tarPath, { force: true }) -} From ffff63fd1cf374ae308ae753795734b4b6392559 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 19:44:10 +1100 Subject: [PATCH 47/72] nix flake: revert verify sha script --- nix/node-modules.nix | 1 - nix/opencode.nix | 1 - nix/scripts/verify-sha.ts | 19 ------------------- 3 files changed, 21 deletions(-) delete mode 100644 nix/scripts/verify-sha.ts diff --git a/nix/node-modules.nix b/nix/node-modules.nix index a03c004de09..6129230aa40 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -58,7 +58,6 @@ stdenvNoCC.mkDerivation { tmp=$(mktemp) curl --fail --location --silent --show-error --tlsv1.2 "$url" -o "$tmp" - bun --bun ${args.verifyShaScript} "$tmp" "$sha" mkdir -p "$dest" tar -xzf "$tmp" -C "$dest" --strip-components=1 package diff --git a/nix/opencode.nix b/nix/opencode.nix index ec65d955bfc..ecf65e19203 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -9,7 +9,6 @@ let // { canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; optionalMetadataScript = scripts + "/optional-metadata.ts"; - verifyShaScript = scripts + "/verify-sha.ts"; } ); in diff --git a/nix/scripts/verify-sha.ts b/nix/scripts/verify-sha.ts deleted file mode 100644 index a8d46dfae3d..00000000000 --- a/nix/scripts/verify-sha.ts +++ /dev/null @@ -1,19 +0,0 @@ -const argv = Bun.argv.slice(2) -if (argv.length < 2) { - console.error("usage: verify-sha ") - process.exit(1) -} - -const file = argv[0] -const raw = argv[1] -const want = raw.startsWith("sha512-") ? raw.slice(7) : raw - -const data = await Bun.file(file).arrayBuffer() -const sha = new Bun.SHA512() -sha.update(data) -const digest = sha.digest("base64") - -if (digest !== want) { - console.error(`hash mismatch: expected ${want}, got ${digest}`) - process.exit(1) -} From 9d142e29ad3d2fc13a8efb0a3301b500d40224d9 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 20:41:26 +1100 Subject: [PATCH 48/72] nix flake: revert excessive optional metadata verification --- flake.nix | 14 +------ nix/node-modules.nix | 8 +--- nix/opencode.nix | 1 - nix/scripts/bun-build.ts | 45 +++++++++++----------- nix/scripts/optional-metadata.ts | 65 -------------------------------- 5 files changed, 24 insertions(+), 109 deletions(-) delete mode 100644 nix/scripts/optional-metadata.ts diff --git a/flake.nix b/flake.nix index 862dd542849..95c935c067c 100644 --- a/flake.nix +++ b/flake.nix @@ -27,12 +27,10 @@ "aarch64-darwin" = "bun-darwin-arm64"; "x86_64-darwin" = "bun-darwin-x64"; }; - scripts = ./nix/scripts; - dummyHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; defaultNodeModules = builtins.listToAttrs ( map (system: { name = system; - value = dummyHash; + value = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }) systems ); hashesFile = "${./nix}/hashes.json"; @@ -41,13 +39,6 @@ hashes = { nodeModules = defaultNodeModules // (hashesData.nodeModules or { }); optional = hashesData.optional or { }; - metadata = hashesData.metadata or { }; - }; - optionalPackagesFiles = { - "aarch64-linux" = ./nix/optional-packages/aarch64-linux.txt; - "x86_64-linux" = ./nix/optional-packages/x86_64-linux.txt; - "aarch64-darwin" = ./nix/optional-packages/aarch64-darwin.txt; - "x86_64-darwin" = ./nix/optional-packages/x86_64-darwin.txt; }; modelsDev = forEachSystem ( system: @@ -82,7 +73,6 @@ pkgs = pkgsFor system; mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { hash = hashes.nodeModules.${system}; - optionalPackagesFile = optionalPackagesFiles.${system}; }; mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in @@ -90,7 +80,7 @@ default = mkPackage { version = packageJson.version; src = ./.; - scripts = scripts; + scripts = ./nix/scripts; target = bunTarget.${system}; modelsDev = "${modelsDev.${system}}/dist/_api.json"; mkNodeModules = mkNodeModules; diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 6129230aa40..32463832dca 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,4 +1,4 @@ -{ optionalPackagesFile, hash, lib, stdenvNoCC, bun, cacert, curl }: +{ hash, lib, stdenvNoCC, bun, cacert, curl }: args: let sourceDateEpoch = @@ -31,12 +31,6 @@ stdenvNoCC.mkDerivation { --no-progress \ --linker=isolated - cp ${optionalPackagesFile} optional-packages.txt - - bun --bun ${args.optionalMetadataScript} optional-packages.txt > optional-metadata.txt - - echo "Optional package metadata:" - cat optional-metadata.txt while IFS=$'\t' read -r name version sha; do [ -z "$name" ] && continue scope="''${name%%/*}" diff --git a/nix/opencode.nix b/nix/opencode.nix index ecf65e19203..93a4336173e 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -8,7 +8,6 @@ let attrs // { canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; - optionalMetadataScript = scripts + "/optional-metadata.ts"; } ); in diff --git a/nix/scripts/bun-build.ts b/nix/scripts/bun-build.ts index 3ecbf42a993..1c61a07d785 100644 --- a/nix/scripts/bun-build.ts +++ b/nix/scripts/bun-build.ts @@ -3,12 +3,10 @@ import path from "path" import fs from "fs" const version = "@VERSION@" -const repo = process.cwd() -const pkg = path.join(repo, "packages/opencode") +const pkg = path.join(process.cwd(), "packages/opencode") const parser = fs.realpathSync( path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"), ) -const dir = pkg const worker = "./src/cli/cmd/tui/worker.ts" const target = process.env["BUN_COMPILE_TARGET"] @@ -32,20 +30,20 @@ const readTrackedAssets = () => { const removeTrackedAssets = () => { for (const file of readTrackedAssets()) { - const targetPath = path.join(pkg, file) - if (fs.existsSync(targetPath)) { - fs.rmSync(targetPath, { force: true }) + const filePath = path.join(pkg, file) + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { force: true }) } } } -const trackedAssets = new Set() +const assets = new Set() -const addAsset = async (assetPath: string) => { - const file = path.basename(assetPath) +const addAsset = async (p: string) => { + const file = path.basename(p) const dest = path.join(pkg, file) - await Bun.write(dest, Bun.file(assetPath)) - trackedAssets.add(file) + await Bun.write(dest, Bun.file(p)) + assets.add(file) } removeTrackedAssets() @@ -58,7 +56,7 @@ const result = await Bun.build({ entrypoints: ["./src/index.ts", parser, worker], define: { OPENCODE_VERSION: `'@VERSION@'`, - OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parser).replace(/\\/g, "/"), + OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"), OPENCODE_CHANNEL: "'latest'", }, compile: { @@ -77,9 +75,9 @@ if (!result.success) { throw new Error("Compilation failed") } -const assetOutputs = result.outputs?.filter((item) => item.kind === "asset") ?? [] -for (const asset of assetOutputs) { - await addAsset(asset.path) +const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? [] +for (const x of assetOutputs) { + await addAsset(x.path) } const bundle = await Bun.build({ @@ -99,22 +97,21 @@ if (!bundle.success) { throw new Error("Worker compilation failed") } -const workerAssetOutputs = bundle.outputs?.filter((item) => item.kind === "asset") ?? [] -for (const asset of workerAssetOutputs) { - await addAsset(asset.path) +const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? [] +for (const x of workerAssets) { + await addAsset(x.path) } -const output = bundle.outputs.find((item) => item.kind === "entry-point") +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") -const src = output.path -await Bun.write(dest, Bun.file(src)) -fs.rmSync(path.dirname(src), { recursive: true, force: true }) +await Bun.write(dest, Bun.file(output.path)) +fs.rmSync(path.dirname(output.path), { recursive: true, force: true }) -const assetList = Array.from(trackedAssets) -await Bun.write(manifestPath, assetList.length > 0 ? assetList.join("\n") + "\n" : "") +const list = Array.from(assets) +await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "") console.log("Build successful!") diff --git a/nix/scripts/optional-metadata.ts b/nix/scripts/optional-metadata.ts deleted file mode 100644 index f7ca12359c5..00000000000 --- a/nix/scripts/optional-metadata.ts +++ /dev/null @@ -1,65 +0,0 @@ -import path from "path" - -const argv = Bun.argv.slice(2) -const arg = argv[0] ?? "optional-packages.txt" -const root = process.cwd() -const lock = path.join(root, "bun.lock") -const file = path.isAbsolute(arg) ? arg : path.join(root, arg) - -const mask = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - -const doc = await Bun.file(lock).text() -const text = await Bun.file(file).text() -const hashesPath = path.join(root, "nix/hashes.json") -let optional: Record = {} - -const hashesData = await Bun.file(hashesPath) - .text() - .catch((error) => { - console.warn(`missing-hashes\t${hashesPath}\t${(error as Error).message}`) - return "" - }) - -if (hashesData?.trim()) { - const parsed = JSON.parse(hashesData) - if (parsed && typeof parsed.optional === "object" && parsed.optional !== null) { - optional = parsed.optional as typeof optional - } -} - -const names = text - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - -if (names.length === 0) { - process.exit(0) -} - -const lines: string[] = [] - -for (const name of names) { - const safe = mask(name) - const verMatch = doc.match(new RegExp(`"${safe}": "([^"]+)"`)) - const store = optional[name] ?? {} - const ver = verMatch?.[1] ?? store.version - if (!ver) { - console.error(`missing-version\t${name}`) - process.exit(1) - } - if (store.version && verMatch && store.version !== verMatch[1]) { - console.warn(`version-mismatch\t${name}\t${store.version}\t${verMatch[1]}`) - } - const verSafe = mask(ver) - const shaHit = doc.match(new RegExp(`"${safe}@${verSafe}"[^\\n]*"(sha512-[^"]+)"`)) - const sha = shaHit?.[1] ?? store.sha512 ?? store.sha - if (!sha) { - console.error(`missing-sha\t${name}\t${ver}`) - process.exit(1) - } - lines.push(`${name}\t${ver}\t${sha}`) -} - -if (lines.length > 0) { - await Bun.write(Bun.stdout, lines.join("\n") + "\n") -} From bef9709a32632d4779135229569eb41db11bf986 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 20:55:05 +1100 Subject: [PATCH 49/72] nix flake: attempt full regen of hash file --- flake.nix | 20 +++++++++-------- nix/hashes.json | 51 -------------------------------------------- nix/node-modules.nix | 46 --------------------------------------- 3 files changed, 11 insertions(+), 106 deletions(-) delete mode 100644 nix/hashes.json diff --git a/flake.nix b/flake.nix index 95c935c067c..9e5a9fcefd3 100644 --- a/flake.nix +++ b/flake.nix @@ -97,15 +97,17 @@ opencode-dev = { type = "app"; meta = { - description = "Nix devshell shell for OpenCode"; - runtimeInputs = [ pkgs.bun ]; - }; - program = "${pkgs.writeShellApplication { - name = "opencode-dev"; - text = '' - exec bun run dev "$@" - ''; - }}/bin/opencode-dev"; + description = "Nix devshell shell for OpenCode"; + runtimeInputs = [ pkgs.bun ]; + }; + program = "${ + pkgs.writeShellApplication { + name = "opencode-dev"; + text = '' + exec bun run dev "$@" + ''; + } + }/bin/opencode-dev"; }; } ); diff --git a/nix/hashes.json b/nix/hashes.json deleted file mode 100644 index c98af56806f..00000000000 --- a/nix/hashes.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "nodeModules": { - "x86_64-linux": "sha256-U2bUPxnKrUN6pRIDBIiyrg+CqghaoneSqY4BI5zXs5w=", - "aarch64-linux": "sha256-bd7FPZzhfhK4zUNcP+1hyXLYLyfetWKwBgGandk7x3o=", - "aarch64-darwin": "sha256-qUHSVtDkUcq7bUO3QbPUScqCcwpon2FqebRFA/NcXzA=", - "x86_64-darwin": "sha256-1yhZhwXvO/AlUhdN03eUqb7Jkyk2cgr8YwZd8RW3BWw=" - }, - "optional": { - "@opentui/core-darwin-arm64": { - "version": "0.0.0-20251108-0c7899b1", - "sha512": "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g==" - }, - "@opentui/core-darwin-x64": { - "version": "0.0.0-20251108-0c7899b1", - "sha512": "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg==" - }, - "@opentui/core-linux-arm64": { - "version": "0.0.0-20251108-0c7899b1", - "sha512": "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA==" - }, - "@opentui/core-linux-x64": { - "version": "0.0.0-20251108-0c7899b1", - "sha512": "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q==" - }, - "@opentui/core-win32-x64": { - "version": "0.0.0-20251108-0c7899b1", - "sha512": "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg==" - }, - "@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "sha512": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==" - }, - "@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "sha512": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==" - }, - "@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "sha512": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" - }, - "@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "sha512": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==" - }, - "@parcel/watcher-win32-x64": { - "version": "2.5.1", - "sha512": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==" - } - }, - "metadata": {} -} diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 32463832dca..d97db47afa1 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -24,58 +24,12 @@ stdenvNoCC.mkDerivation { runHook preBuild export HOME=$(mktemp -d) export BUN_INSTALL_CACHE_DIR=$(mktemp -d) - export SOURCE_DATE_EPOCH=${toString sourceDateEpoch} bun install \ --frozen-lockfile \ --ignore-scripts \ --no-progress \ --linker=isolated - - while IFS=$'\t' read -r name version sha; do - [ -z "$name" ] && continue - scope="''${name%%/*}" - remainder="''${name#*/}" - if [ "$scope" = "$name" ]; then - scope="" - remainder="$name" - fi - - base="''${remainder##*/}" - encoded_scope="''${scope//@/%40}" - - url="https://registry.npmjs.org/''${remainder}/-/''${base}-''${version}.tgz" - dest="node_modules/''${remainder}" - if [ -n "$scope" ]; then - url="https://registry.npmjs.org/''${encoded_scope}/''${remainder}/-/''${base}-''${version}.tgz" - dest="node_modules/''${scope}/''${remainder}" - fi - - tmp=$(mktemp) - curl --fail --location --silent --show-error --tlsv1.2 "$url" -o "$tmp" - - mkdir -p "$dest" - tar -xzf "$tmp" -C "$dest" --strip-components=1 package - rm -f "$tmp" - - echo "Installed optional package $name -> $dest" - - for ws in $(find packages -maxdepth 1 -mindepth 1 -type d | sort); do - [ -d "$ws/node_modules" ] || continue - ws_dest="$ws/node_modules/$remainder" - if [ -n "$scope" ]; then - ws_dest="$ws/node_modules/$scope/$remainder" - fi - mkdir -p "$(dirname "$ws_dest")" - rm -rf "$ws_dest" - target="$(realpath --relative-to="$(dirname "$ws_dest")" "$dest")" - ln -s "$target" "$ws_dest" - done - done < optional-metadata.txt - - rm -f optional-packages.txt optional-metadata.txt - bun --bun ${args.canonicalizeScript} - runHook postBuild ''; From 170d1f79f7c33d6e93dfe76ef48e6902d9a19d58 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 21:32:39 +1100 Subject: [PATCH 50/72] nix flake: remove all optional metadata/packages handling --- .github/workflows/update-nix-hashes.yml | 14 ++-- nix/optional-packages/aarch64-darwin.txt | 2 - nix/optional-packages/aarch64-linux.txt | 2 - nix/optional-packages/win32-x64.txt | 2 - nix/optional-packages/x86_64-darwin.txt | 2 - nix/optional-packages/x86_64-linux.txt | 2 - nix/scripts/update-hashes.sh | 83 ++---------------------- 7 files changed, 10 insertions(+), 97 deletions(-) delete mode 100644 nix/optional-packages/aarch64-darwin.txt delete mode 100644 nix/optional-packages/aarch64-linux.txt delete mode 100644 nix/optional-packages/win32-x64.txt delete mode 100644 nix/optional-packages/x86_64-darwin.txt delete mode 100644 nix/optional-packages/x86_64-linux.txt diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index bc50bb792fd..2034f2fbd86 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -10,15 +10,11 @@ on: - 'bun.lock' - 'package.json' - 'packages/*/package.json' - - 'nix/optional-packages/**' - - 'nix/scripts/optional-metadata.ts' pull_request: paths: - 'bun.lock' - 'package.json' - 'packages/*/package.json' - - 'nix/optional-packages/**' - - 'nix/scripts/optional-metadata.ts' jobs: cancel-previous: @@ -109,7 +105,7 @@ jobs: if [ ! -f "$HASH_FILE" ]; then mkdir -p "$(dirname "$HASH_FILE")" - jq -n '{nodeModules: {}, optional: {}}' > "$HASH_FILE" + jq -n '{nodeModules: {}}' > "$HASH_FILE" fi echo "Updating hash for ${SYSTEM} to ${CORRECT_HASH}" @@ -169,7 +165,7 @@ jobs: BASE_HASH="nix/hashes.json" if [ ! -f "$BASE_HASH" ]; then mkdir -p "$(dirname "$BASE_HASH")" - jq -n '{nodeModules: {}, optional: {}}' > "$BASE_HASH" + jq -n '{nodeModules: {}}' > "$BASE_HASH" fi for system in x86_64-linux aarch64-linux aarch64-darwin x86_64-darwin; do if [ -f "hash-artifacts/hashes-${system}/hashes.json" ]; then @@ -177,8 +173,7 @@ jobs: # Deep merge: preserve all keys in nested objects jq --arg sys "$system" \ --slurpfile new "hash-artifacts/hashes-${system}/hashes.json" \ - '.nodeModules[$sys] = $new[0].nodeModules[$sys] | - .optional = (.optional + $new[0].optional)' \ + '.nodeModules[$sys] = $new[0].nodeModules[$sys]' \ "$BASE_HASH" > /tmp/merged.json mv /tmp/merged.json "$BASE_HASH" else @@ -191,8 +186,7 @@ jobs: DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" HAS_NODE=$(jq --arg dummy "$DUMMY" '[ (.nodeModules // {})[] | select(. != $dummy) ] | length' "$BASE_HASH") - HAS_OPTIONAL=$(jq '(.optional // {}) | length' "$BASE_HASH") - if [ "$HAS_NODE" -gt 0 ] || [ "$HAS_OPTIONAL" -gt 0 ]; then + if [ "$HAS_NODE" -gt 0 ]; then echo "has_data=true" >> "$GITHUB_OUTPUT" else echo "has_data=false" >> "$GITHUB_OUTPUT" diff --git a/nix/optional-packages/aarch64-darwin.txt b/nix/optional-packages/aarch64-darwin.txt deleted file mode 100644 index 34a4ec964e9..00000000000 --- a/nix/optional-packages/aarch64-darwin.txt +++ /dev/null @@ -1,2 +0,0 @@ -@parcel/watcher-darwin-arm64 -@opentui/core-darwin-arm64 diff --git a/nix/optional-packages/aarch64-linux.txt b/nix/optional-packages/aarch64-linux.txt deleted file mode 100644 index f1940174875..00000000000 --- a/nix/optional-packages/aarch64-linux.txt +++ /dev/null @@ -1,2 +0,0 @@ -@parcel/watcher-linux-arm64-glibc -@opentui/core-linux-arm64 diff --git a/nix/optional-packages/win32-x64.txt b/nix/optional-packages/win32-x64.txt deleted file mode 100644 index 2f304b369b1..00000000000 --- a/nix/optional-packages/win32-x64.txt +++ /dev/null @@ -1,2 +0,0 @@ -@parcel/watcher-win32-x64 -@opentui/core-win32-x64 diff --git a/nix/optional-packages/x86_64-darwin.txt b/nix/optional-packages/x86_64-darwin.txt deleted file mode 100644 index cf3df4cd3f4..00000000000 --- a/nix/optional-packages/x86_64-darwin.txt +++ /dev/null @@ -1,2 +0,0 @@ -@parcel/watcher-darwin-x64 -@opentui/core-darwin-x64 diff --git a/nix/optional-packages/x86_64-linux.txt b/nix/optional-packages/x86_64-linux.txt deleted file mode 100644 index 5c147c8a65c..00000000000 --- a/nix/optional-packages/x86_64-linux.txt +++ /dev/null @@ -1,2 +0,0 @@ -@parcel/watcher-linux-x64-glibc -@opentui/core-linux-x64 diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index 0ab60b64015..c3e7bfaadf6 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -5,29 +5,11 @@ set -euo pipefail DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} -OPTIONAL_PACKAGES_DIR=${OPTIONAL_PACKAGES_DIR:-nix/optional-packages} -OPTIONAL_FILE=${OPTIONAL_PACKAGES_FILE:-} -OPTIONAL_AGGREGATE="" - -if [ -z "$OPTIONAL_FILE" ] && [ -d "$OPTIONAL_PACKAGES_DIR" ]; then - OPTIONAL_AGGREGATE=$(mktemp) - find "$OPTIONAL_PACKAGES_DIR" -maxdepth 1 -type f -name '*.txt' | sort | while read -r file; do - [ -n "$file" ] || continue - cat "$file" - echo - done | sed 's/#.*$//' | sed '/^[[:space:]]*$/d' | sort -u >"$OPTIONAL_AGGREGATE" - OPTIONAL_FILE="$OPTIONAL_AGGREGATE" -fi - -if [ -z "$OPTIONAL_FILE" ]; then - OPTIONAL_FILE="nix/optional-packages.txt" -fi if [ ! -f "$HASH_FILE" ]; then cat <<'EOF' >"$HASH_FILE" { - "nodeModules": {}, - "optional": {} + "nodeModules": {} } EOF fi @@ -52,55 +34,11 @@ if [ -z "$SYSTEMS" ]; then fi cleanup() { - if [ -n "${JSON_OUTPUT:-}" ] && [ -f "$JSON_OUTPUT" ]; then - rm -f "$JSON_OUTPUT" - fi - if [ -n "${BUILD_LOG:-}" ] && [ -f "$BUILD_LOG" ]; then - rm -f "$BUILD_LOG" - fi - if [ -n "${TMP_EXPR:-}" ] && [ -f "$TMP_EXPR" ]; then - rm -f "$TMP_EXPR" - fi - if [ -n "$OPTIONAL_AGGREGATE" ] && [ -f "$OPTIONAL_AGGREGATE" ]; then - rm -f "$OPTIONAL_AGGREGATE" - fi + rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}" } trap cleanup EXIT -update_optional() { - if [ ! -f "$OPTIONAL_FILE" ]; then - echo "optional package list missing at $OPTIONAL_FILE" >&2 - return 1 - fi - local meta tmp name ver sha - if command -v bun >/dev/null 2>&1; then - meta="$(bun --bun nix/scripts/optional-metadata.ts "$OPTIONAL_FILE")" || return 1 - fi - if [ -z "${meta:-}" ] && command -v nix >/dev/null 2>&1; then - meta="$(nix shell --quiet nixpkgs#bun -c bun --bun nix/scripts/optional-metadata.ts "$OPTIONAL_FILE")" || return 1 - fi - if [ -z "${meta:-}" ]; then - echo "bun unavailable; skipping optional metadata refresh" >&2 - return 0 - fi - while IFS=$'\t' read -r name ver sha; do - [ -n "$name" ] || continue - tmp=$(mktemp) - jq \ - --arg name "$name" \ - --arg ver "$ver" \ - --arg sha "$sha" \ - '.optional[$name] = { version: $ver, sha512: $sha }' \ - "$HASH_FILE" >"$tmp" - mv "$tmp" "$HASH_FILE" - done </dev/null || true)" - if [ -n "$PREV_PATH" ]; then - nix store delete --ignore-liveness "$PREV_PATH" >/dev/null 2>&1 || true - fi - DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" echo "Setting dummy node_modules outputHash for ${SYSTEM}..." @@ -173,18 +105,15 @@ for SYSTEM in $SYSTEMS; do fi if [ -z "$CORRECT_HASH" ]; then - echo "Failed to extract node_modules hash for ${SYSTEM}" - echo "Build log (last 100 lines):" - tail -100 "$BUILD_LOG" || true + echo "Failed to determine correct node_modules hash for ${SYSTEM}." + echo "Build log:" + cat "$BUILD_LOG" exit 1 fi write_node_modules_hash "$CORRECT_HASH" - if ! jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null; then - echo "Failed to persist node_modules hash for ${SYSTEM}" - exit 1 - fi + jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" From 9224f351fd79ceeb74eea413fcd0d06fbdc1f893 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 12 Nov 2025 10:43:13 +0000 Subject: [PATCH 51/72] Update Nix hashes --- nix/hashes.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 nix/hashes.json diff --git a/nix/hashes.json b/nix/hashes.json new file mode 100644 index 00000000000..c062f9b67c8 --- /dev/null +++ b/nix/hashes.json @@ -0,0 +1,8 @@ +{ + "nodeModules": { + "x86_64-linux": "sha256-Io3kr7b3DIRSXVm+83sxFABKpSpe5PgHOFuMbC6DpD4=", + "aarch64-linux": "sha256-ilWivX9/YkQjhnB8Eb/chLU3JBzbhFDHOdM1Io8Kwl4=", + "aarch64-darwin": "sha256-Mt06nci5aMsb9kkrS8tDH1n6+Fvr9RRCJgD64A90rko=", + "x86_64-darwin": "sha256-0K1XNrxo3P8krPA+dJ2Yx/Cc9f7IQBQSyt9kXK2Ea3Y=" + } +} From 4d613a35a525fbe66187878aa8e108cd79ac2be5 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 12 Nov 2025 10:54:32 +0000 Subject: [PATCH 52/72] Update Nix hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index c062f9b67c8..c1b70a4c994 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -2,7 +2,7 @@ "nodeModules": { "x86_64-linux": "sha256-Io3kr7b3DIRSXVm+83sxFABKpSpe5PgHOFuMbC6DpD4=", "aarch64-linux": "sha256-ilWivX9/YkQjhnB8Eb/chLU3JBzbhFDHOdM1Io8Kwl4=", - "aarch64-darwin": "sha256-Mt06nci5aMsb9kkrS8tDH1n6+Fvr9RRCJgD64A90rko=", + "aarch64-darwin": "sha256-SG0pMdmthYIqBzQYG0QoqpgAQzcyWbWljMHWgE9o7FU=", "x86_64-darwin": "sha256-0K1XNrxo3P8krPA+dJ2Yx/Cc9f7IQBQSyt9kXK2Ea3Y=" } } From 80685fbac23ba98e4f5f4abc0d388d48ca5e84e2 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Wed, 12 Nov 2025 22:09:55 +1100 Subject: [PATCH 53/72] nix flake: test without isolation flags on opentui/watcher --- packages/opencode/script/build.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index b196ae07bc9..97631d90776 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -83,8 +83,8 @@ const targets = singleFlag await $`rm -rf dist` const binaries: Record = {} -await $`bun install --frozen-lockfile --linker=isolated --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` -await $`bun install --frozen-lockfile --linker=isolated --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` +await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` +await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` for (const item of targets) { const name = [ pkg.name, From 9465c275137c34d830737a3e69fe39ef59fc6bb1 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 12 Nov 2025 11:20:44 +0000 Subject: [PATCH 54/72] Update Nix hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index c1b70a4c994..c062f9b67c8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -2,7 +2,7 @@ "nodeModules": { "x86_64-linux": "sha256-Io3kr7b3DIRSXVm+83sxFABKpSpe5PgHOFuMbC6DpD4=", "aarch64-linux": "sha256-ilWivX9/YkQjhnB8Eb/chLU3JBzbhFDHOdM1Io8Kwl4=", - "aarch64-darwin": "sha256-SG0pMdmthYIqBzQYG0QoqpgAQzcyWbWljMHWgE9o7FU=", + "aarch64-darwin": "sha256-Mt06nci5aMsb9kkrS8tDH1n6+Fvr9RRCJgD64A90rko=", "x86_64-darwin": "sha256-0K1XNrxo3P8krPA+dJ2Yx/Cc9f7IQBQSyt9kXK2Ea3Y=" } } From 668ded55c5c27f2b66a383c2b23b423892f72a61 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 12 Nov 2025 11:31:14 +0000 Subject: [PATCH 55/72] Update Nix hashes --- nix/hashes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index c062f9b67c8..ce43c08ccf8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Io3kr7b3DIRSXVm+83sxFABKpSpe5PgHOFuMbC6DpD4=", + "x86_64-linux": "sha256-qv5q0L3fxLE+Hl/UIe/4yhg5XAVFKYFZfwObH+B9hiM=", "aarch64-linux": "sha256-ilWivX9/YkQjhnB8Eb/chLU3JBzbhFDHOdM1Io8Kwl4=", "aarch64-darwin": "sha256-Mt06nci5aMsb9kkrS8tDH1n6+Fvr9RRCJgD64A90rko=", - "x86_64-darwin": "sha256-0K1XNrxo3P8krPA+dJ2Yx/Cc9f7IQBQSyt9kXK2Ea3Y=" + "x86_64-darwin": "sha256-XUn38SvQ1bU/DiSMpNmDMKgUMemsLfm31HaWaoqFPDU=" } } From 83c4e0edb73553a2adf22fd5dabe786f41724c82 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 13 Nov 2025 23:01:40 +1100 Subject: [PATCH 56/72] Add workflow to snapshot node modules --- .github/workflows/node-modules-snapshot.yml | 60 +++++++++++++++++ script/node-modules-snapshot.sh | 72 +++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 .github/workflows/node-modules-snapshot.yml create mode 100755 script/node-modules-snapshot.sh diff --git a/.github/workflows/node-modules-snapshot.yml b/.github/workflows/node-modules-snapshot.yml new file mode 100644 index 00000000000..dafe816d477 --- /dev/null +++ b/.github/workflows/node-modules-snapshot.yml @@ -0,0 +1,60 @@ +name: Snapshot Node Modules + +on: + workflow_dispatch: + inputs: + suffix: + description: Optional label appended to artifact names + required: false + +jobs: + snapshot: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + system: x86_64-linux + - runner: ubuntu-24.04-arm + system: aarch64-linux + - runner: macos-latest + system: aarch64-darwin + - runner: macos-15-intel + system: x86_64-darwin + runs-on: ${{ matrix.runner }} + permissions: + contents: read + env: + SNAPSHOT_SUFFIX: ${{ inputs.suffix || github.run_attempt }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Nix + uses: DeterminateSystems/nix-installer-action@v20 + + - name: Capture node_modules snapshot + run: | + set -euo pipefail + nix develop .#devShells.${{ matrix.system }}.default -c \ + bash script/node-modules-snapshot.sh \ + "${{ matrix.system }}" \ + "${SNAPSHOT_SUFFIX}" + + - name: Upload snapshot artifacts + uses: actions/upload-artifact@v4 + with: + name: node-modules-${{ matrix.system }} + retention-days: 3 + path: | + node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.tar.gz + node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.sha256 + node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.list + node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.txt + + - name: Clean working tree + run: | + set -euo pipefail + find . -type d -name node_modules -prune -exec rm -rf {} + diff --git a/script/node-modules-snapshot.sh b/script/node-modules-snapshot.sh new file mode 100755 index 00000000000..0d92b2814f1 --- /dev/null +++ b/script/node-modules-snapshot.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +system="${1:-unknown}" +suffix="${2:-}" +label="$system" +if [ -n "$suffix" ]; then + label="$system-$suffix" +fi + +root="$(pwd)" +work="$root/.tmp-node-modules" + +cleanup() { + rm -rf "$work" + if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then + rm -rf "$HOME" + fi + if [ -n "${BUN_INSTALL_CACHE_DIR:-}" ] && [ -d "$BUN_INSTALL_CACHE_DIR" ]; then + rm -rf "$BUN_INSTALL_CACHE_DIR" + fi +} + +trap cleanup EXIT + +rm -rf "$work" +mkdir -p "$work" + +export HOME +export BUN_INSTALL_CACHE_DIR +HOME="$(mktemp -d)" +BUN_INSTALL_CACHE_DIR="$(mktemp -d)" + +while IFS= read -r existing; do + rm -rf "$existing" +done < <(find . -type d -name node_modules -prune | sort -r) + +bun install \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress \ + --linker=isolated + +bun --bun nix/scripts/canonicalize-node-modules.ts + +i=0 +while IFS= read -r dir; do + rel="${dir#./}" + dest="$work/$rel" + mkdir -p "$(dirname "$dest")" + cp -R "$dir" "$dest" + i=$((i + 1)) +done < <(find . -type d -name node_modules -prune | sort) + +hash_path="node-modules-${label}.sha256" +nix hash path --sri "$work" | tee "$hash_path" + +tar_name="node-modules-${label}.tar.gz" +tar -czf "$tar_name" -C "$work" . + +manifest="node-modules-${label}.list" +find "$work" -print | sed "s|$work/||" | sort > "$manifest" + +summary="node-modules-${label}.txt" +{ + printf "system=%s\n" "$system" + printf "suffix=%s\n" "$suffix" + printf "hash=%s\n" "$(cat "$hash_path")" + printf "directories=%s\n" "$i" + printf "tar=%s\n" "$tar_name" + printf "manifest=%s\n" "$manifest" +} > "$summary" From 459135e319bc46cd7af93ac17e0acb389e3889bc Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 13 Nov 2025 23:03:19 +1100 Subject: [PATCH 57/72] Trigger snapshot workflow on debug branches --- .github/workflows/node-modules-snapshot.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/node-modules-snapshot.yml b/.github/workflows/node-modules-snapshot.yml index dafe816d477..347b3a0bdd5 100644 --- a/.github/workflows/node-modules-snapshot.yml +++ b/.github/workflows/node-modules-snapshot.yml @@ -6,6 +6,9 @@ on: suffix: description: Optional label appended to artifact names required: false + push: + branches: + - "debug/**" jobs: snapshot: From cd7f3fa45f171f176ffb9d924d41ceaf747330a7 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 13 Nov 2025 23:06:20 +1100 Subject: [PATCH 58/72] Fix artifact glob for snapshot workflow --- .github/workflows/node-modules-snapshot.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/node-modules-snapshot.yml b/.github/workflows/node-modules-snapshot.yml index 347b3a0bdd5..70301a25169 100644 --- a/.github/workflows/node-modules-snapshot.yml +++ b/.github/workflows/node-modules-snapshot.yml @@ -52,10 +52,10 @@ jobs: name: node-modules-${{ matrix.system }} retention-days: 3 path: | - node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.tar.gz - node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.sha256 - node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.list - node-modules-${{ matrix.system }}-${SNAPSHOT_SUFFIX}.txt + node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.tar.gz + node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.sha256 + node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.list + node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.txt - name: Clean working tree run: | From 500f49c8b05f1f9e114693f313bded6602c0ff1e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Thu, 13 Nov 2025 23:39:37 +1100 Subject: [PATCH 59/72] Trigger snapshot run #2 From b65ee858bf5f5a9ea8572cdcea543fe3f8ac38fd Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 14 Nov 2025 00:16:19 +1100 Subject: [PATCH 60/72] Normalize bun .bin directories for determinism --- nix/node-modules.nix | 1 + nix/opencode.nix | 1 + nix/scripts/normalize-bun-binaries.ts | 138 ++++++++++++++++++++++++++ script/node-modules-snapshot.sh | 1 + 4 files changed, 141 insertions(+) create mode 100644 nix/scripts/normalize-bun-binaries.ts diff --git a/nix/node-modules.nix b/nix/node-modules.nix index d97db47afa1..0e0ab089a5f 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -30,6 +30,7 @@ stdenvNoCC.mkDerivation { --no-progress \ --linker=isolated bun --bun ${args.canonicalizeScript} + bun --bun ${args.normalizeBinsScript} runHook postBuild ''; diff --git a/nix/opencode.nix b/nix/opencode.nix index 93a4336173e..2148265870d 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -8,6 +8,7 @@ let attrs // { canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; + normalizeBinsScript = scripts + "/normalize-bun-binaries.ts"; } ); in diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts new file mode 100644 index 00000000000..531d8fd0567 --- /dev/null +++ b/nix/scripts/normalize-bun-binaries.ts @@ -0,0 +1,138 @@ +import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" +import { join, relative } from "path" + +type PackageManifest = { + name?: string + bin?: string | Record +} + +const root = process.cwd() +const bunRoot = join(root, "node_modules/.bun") +const bunEntries = (await safeReadDir(bunRoot)).sort() +let rewritten = 0 + +for (const entry of bunEntries) { + const modulesRoot = join(bunRoot, entry, "node_modules") + if (!(await exists(modulesRoot))) { + continue + } + const binRoot = join(modulesRoot, ".bin") + await rm(binRoot, { recursive: true, force: true }) + await mkdir(binRoot, { recursive: true }) + + const packageDirs = await collectPackages(modulesRoot) + for (const packageDir of packageDirs) { + const manifest = await readManifest(packageDir) + if (!manifest) { + continue + } + const binField = manifest.bin + if (!binField) { + continue + } + const seen = new Set() + if (typeof binField === "string") { + const fallback = manifest.name ?? packageDir.split("/").pop() + if (fallback) { + await linkBinary(binRoot, fallback, packageDir, binField, seen) + } + } else { + const entries = Object.entries(binField).sort((a, b) => a[0].localeCompare(b[0])) + for (const [name, target] of entries) { + await linkBinary(binRoot, name, packageDir, target, seen) + } + } + } +} + +console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`) + +async function collectPackages(modulesRoot: string) { + const found: string[] = [] + const topLevel = (await safeReadDir(modulesRoot)).sort() + for (const name of topLevel) { + if (name === ".bin" || name === ".bun") { + continue + } + const full = join(modulesRoot, name) + if (!(await isDirectory(full))) { + continue + } + if (name.startsWith("@")) { + const scoped = (await safeReadDir(full)).sort() + for (const child of scoped) { + const scopedDir = join(full, child) + if (await isDirectory(scopedDir)) { + found.push(scopedDir) + } + } + continue + } + found.push(full) + } + return found.sort() +} + +async function readManifest(dir: string) { + const file = Bun.file(join(dir, "package.json")) + if (!(await file.exists())) { + return null + } + const data = (await file.json()) as PackageManifest + return data +} + +async function linkBinary(binRoot: string, name: string, packageDir: string, target: string, seen: Set) { + if (!name || !target) { + return + } + const normalizedName = normalizeBinName(name) + if (seen.has(normalizedName)) { + return + } + const resolved = join(packageDir, target) + const script = Bun.file(resolved) + if (!(await script.exists())) { + return + } + seen.add(normalizedName) + const destination = join(binRoot, normalizedName) + const relativeTarget = relative(binRoot, resolved) || "." + await rm(destination, { force: true }) + await symlink(relativeTarget, destination) + rewritten++ +} + +async function exists(path: string) { + try { + await lstat(path) + return true + } catch { + return false + } +} + +async function isDirectory(path: string) { + try { + const info = await lstat(path) + return info.isDirectory() + } catch { + return false + } +} + +async function safeReadDir(path: string) { + try { + return await readdir(path) + } catch { + return [] + } +} + +function normalizeBinName(name: string) { + const slash = name.lastIndexOf("/") + if (slash >= 0) { + return name.slice(slash + 1) + } + return name +} diff --git a/script/node-modules-snapshot.sh b/script/node-modules-snapshot.sh index 0d92b2814f1..57f7e17378a 100755 --- a/script/node-modules-snapshot.sh +++ b/script/node-modules-snapshot.sh @@ -42,6 +42,7 @@ bun install \ --linker=isolated bun --bun nix/scripts/canonicalize-node-modules.ts +bun --bun nix/scripts/normalize-bun-binaries.ts i=0 while IFS= read -r dir; do From e876e6fddb0fcc87bd7b46c486a8edb35563f798 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 14 Nov 2025 00:43:12 +1100 Subject: [PATCH 61/72] Trigger snapshot run #3 From be0b3008f187ef894d8f2e1126c006a1b6d4bac4 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 14 Nov 2025 01:34:10 +1100 Subject: [PATCH 62/72] Publish hash metadata separately --- .github/workflows/node-modules-snapshot.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/node-modules-snapshot.yml b/.github/workflows/node-modules-snapshot.yml index 70301a25169..b95d7ede891 100644 --- a/.github/workflows/node-modules-snapshot.yml +++ b/.github/workflows/node-modules-snapshot.yml @@ -53,6 +53,13 @@ jobs: retention-days: 3 path: | node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.tar.gz + + - name: Upload hash metadata + uses: actions/upload-artifact@v4 + with: + name: node-modules-${{ matrix.system }}-meta + retention-days: 30 + path: | node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.sha256 node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.list node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.txt From 3aec0ab6988d6de54bab7520364fdc51d702ea0e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 14 Nov 2025 01:48:22 +1100 Subject: [PATCH 63/72] Update Nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index ce43c08ccf8..eff5064974f 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-qv5q0L3fxLE+Hl/UIe/4yhg5XAVFKYFZfwObH+B9hiM=", - "aarch64-linux": "sha256-ilWivX9/YkQjhnB8Eb/chLU3JBzbhFDHOdM1Io8Kwl4=", - "aarch64-darwin": "sha256-Mt06nci5aMsb9kkrS8tDH1n6+Fvr9RRCJgD64A90rko=", - "x86_64-darwin": "sha256-XUn38SvQ1bU/DiSMpNmDMKgUMemsLfm31HaWaoqFPDU=" + "x86_64-linux": "sha256-C6vL6o4tdEBPeuWzDJgJVV817uQjy9ezF8OApULLyKc=", + "aarch64-linux": "sha256-wB/wpcDb4hwnXHQKRxwQwTFPHRrIvIPu79dHBgIW5aI=", + "aarch64-darwin": "sha256-bBXTToF+8hVTLzF/Ea+bpy/CzQyOYeCqBB96neM50JU=", + "x86_64-darwin": "sha256-77SMwEdFMjKAjJOcv53KhfVGI2nV2NWq4/HBCTr2Shc=" } } From 1222dd56ee2bf52b5240b8f6743a0506b01dbf7d Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 13 Nov 2025 21:18:52 +0000 Subject: [PATCH 64/72] Update Nix hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index eff5064974f..0a214f77ef8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-C6vL6o4tdEBPeuWzDJgJVV817uQjy9ezF8OApULLyKc=", - "aarch64-linux": "sha256-wB/wpcDb4hwnXHQKRxwQwTFPHRrIvIPu79dHBgIW5aI=", - "aarch64-darwin": "sha256-bBXTToF+8hVTLzF/Ea+bpy/CzQyOYeCqBB96neM50JU=", - "x86_64-darwin": "sha256-77SMwEdFMjKAjJOcv53KhfVGI2nV2NWq4/HBCTr2Shc=" + "x86_64-linux": "sha256-2DZvke70drgT9yuXTcWyqlLnlIoBFWGg96F9nL2Qjrk=", + "aarch64-linux": "sha256-Rt60mm+gVPr9GFKqPz0oxOuPMQpBtmm1+EMhblfe95k=", + "aarch64-darwin": "sha256-oves/qyrOFabfqPCwlDceJzuVch2FYpNTSJTGN+pcLg=", + "x86_64-darwin": "sha256-/Ky7JYedxGQJL0a92m/od4QQa2YYJTH4Jw0OvCfP6Ok=" } } From f08e58e8bad1a495616dce74715dfd68c39ed5bb Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 14 Nov 2025 09:04:14 +1100 Subject: [PATCH 65/72] nix flake: remove debug and cleanup --- .github/workflows/guard-release-hashes.yml | 154 -------------------- .github/workflows/node-modules-snapshot.yml | 70 --------- nix/node-modules.nix | 4 - script/node-modules-snapshot.sh | 73 ---------- 4 files changed, 301 deletions(-) delete mode 100644 .github/workflows/guard-release-hashes.yml delete mode 100644 .github/workflows/node-modules-snapshot.yml delete mode 100755 script/node-modules-snapshot.sh diff --git a/.github/workflows/guard-release-hashes.yml b/.github/workflows/guard-release-hashes.yml deleted file mode 100644 index deadecc5d90..00000000000 --- a/.github/workflows/guard-release-hashes.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: Guard Release Hashes - -on: - release: - types: - - published - - prereleased - -permissions: - contents: read - actions: read - -jobs: - ensure-ready: - runs-on: ubuntu-latest - steps: - - name: Check pending hash workflow - id: pending - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TARGET_BRANCH: ${{ github.event.release.target_commitish }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - set -euo pipefail - branch="${TARGET_BRANCH:-}" - if [ -z "$branch" ]; then - branch="$DEFAULT_BRANCH" - fi - if printf '%s' "$branch" | grep -Eq '^[0-9a-f]{40}$'; then - branch="$DEFAULT_BRANCH" - fi - api="${{ github.api_url }}/repos/${{ github.repository }}/actions/workflows/update-nix-hashes.yml/runs?per_page=20&branch=${branch}" - data=$(curl -fsSL -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" "$api") - if [ -z "$data" ]; then - echo "Failed to read workflow runs" - exit 1 - fi - pending=$(printf '%s' "$data" | jq '[.workflow_runs[] | select(.status != "completed")]') - count=$(printf '%s' "$pending" | jq 'length') - if [ "$count" -gt 0 ]; then - urls=$(printf '%s' "$pending" | jq -r '.[].html_url') - { - echo "pending=true" - echo "branch=$branch" - echo "urls<> "$GITHUB_OUTPUT" - echo "Pending hash workflow detected on ${branch}" - exit 1 - fi - { - echo "pending=false" - echo "branch=$branch" - } >> "$GITHUB_OUTPUT" - - - name: Checkout release commit - uses: actions/checkout@v4 - with: - ref: ${{ github.event.release.tag_name }} - fetch-depth: 0 - - - name: Validate dependency coverage - id: coverage - run: | - set -euo pipefail - if [ ! -f nix/hashes.json ]; then - echo "nix/hashes.json missing in release commit" - exit 1 - fi - trigger_commit=$(jq -r '.metadata.trigger // empty' nix/hashes.json) - if [ -z "$trigger_commit" ]; then - echo "metadata.trigger missing; rerun nix workflow" - exit 1 - fi - if ! git cat-file -e "${trigger_commit}^{commit}" >/dev/null 2>&1; then - echo "metadata trigger ${trigger_commit} not found in repo history" - exit 1 - fi - hash_commit=$(jq -r '.metadata.commit // empty' nix/hashes.json) - if [ -z "$hash_commit" ]; then - hash_commit="$trigger_commit" - fi - if ! git cat-file -e "${hash_commit}^{commit}" >/dev/null 2>&1; then - echo "metadata commit ${hash_commit} not found in repo history" - exit 1 - fi - mapfile -t package_files < <(git ls-files 'packages/*/package.json') - paths=() - if git ls-files --error-unmatch bun.lock >/dev/null 2>&1; then - paths+=("bun.lock") - fi - if git ls-files --error-unmatch package.json >/dev/null 2>&1; then - paths+=("package.json") - fi - if [ "${#package_files[@]}" -gt 0 ]; then - paths+=("${package_files[@]}") - fi - release_commit=$(git rev-parse HEAD) - diff_out="" - if [ "${#paths[@]}" -gt 0 ]; then - diff_out=$(git diff --name-only "${trigger_commit}..${release_commit}" -- "${paths[@]}" || true) - fi - if [ -n "${diff_out:-}" ]; then - echo "Dependency files changed since last hash update:" - printf '%s\n' "$diff_out" - { - echo "outdated=true" - echo "diff<> "$GITHUB_OUTPUT" - exit 1 - fi - { - echo "outdated=false" - echo "trigger_commit=${trigger_commit}" - echo "hash_commit=${hash_commit}" - echo "release_commit=${release_commit}" - } >> "$GITHUB_OUTPUT" - - - name: Summarize guard status - if: always() - run: | - { - echo "### Release Hash Guard" - echo "" - echo "- tag: ${{ github.event.release.tag_name }}" - echo "- branch: ${{ steps.pending.outputs.branch }}" - echo "- pending-hash-run: ${{ steps.pending.outputs.pending }}" - echo "- dependency-changes: ${{ steps.coverage.outputs.outdated }}" - echo "- hash-trigger: ${{ steps.coverage.outputs.trigger_commit }}" - echo "- hash-commit: ${{ steps.coverage.outputs.hash_commit }}" - } >> "$GITHUB_STEP_SUMMARY" - if [ "${{ steps.coverage.outputs.outdated }}" = "true" ]; then - { - echo "- release-commit: ${{ steps.coverage.outputs.release_commit }}" - echo "" - echo "#### Files requiring hash refresh" - echo "" - printf '%s\n' "${{ steps.coverage.outputs.diff }}" - } >> "$GITHUB_STEP_SUMMARY" - fi - if [ "${{ steps.pending.outputs.pending }}" = "true" ]; then - { - echo "" - echo "#### Pending Runs" - echo "" - printf '%s\n' "${{ steps.pending.outputs.urls }}" - } >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/node-modules-snapshot.yml b/.github/workflows/node-modules-snapshot.yml deleted file mode 100644 index b95d7ede891..00000000000 --- a/.github/workflows/node-modules-snapshot.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Snapshot Node Modules - -on: - workflow_dispatch: - inputs: - suffix: - description: Optional label appended to artifact names - required: false - push: - branches: - - "debug/**" - -jobs: - snapshot: - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - system: x86_64-linux - - runner: ubuntu-24.04-arm - system: aarch64-linux - - runner: macos-latest - system: aarch64-darwin - - runner: macos-15-intel - system: x86_64-darwin - runs-on: ${{ matrix.runner }} - permissions: - contents: read - env: - SNAPSHOT_SUFFIX: ${{ inputs.suffix || github.run_attempt }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Nix - uses: DeterminateSystems/nix-installer-action@v20 - - - name: Capture node_modules snapshot - run: | - set -euo pipefail - nix develop .#devShells.${{ matrix.system }}.default -c \ - bash script/node-modules-snapshot.sh \ - "${{ matrix.system }}" \ - "${SNAPSHOT_SUFFIX}" - - - name: Upload snapshot artifacts - uses: actions/upload-artifact@v4 - with: - name: node-modules-${{ matrix.system }} - retention-days: 3 - path: | - node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.tar.gz - - - name: Upload hash metadata - uses: actions/upload-artifact@v4 - with: - name: node-modules-${{ matrix.system }}-meta - retention-days: 30 - path: | - node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.sha256 - node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.list - node-modules-${{ matrix.system }}-${{ env.SNAPSHOT_SUFFIX }}.txt - - - name: Clean working tree - run: | - set -euo pipefail - find . -type d -name node_modules -prune -exec rm -rf {} + diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 0e0ab089a5f..2253d0f0513 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -1,9 +1,5 @@ { hash, lib, stdenvNoCC, bun, cacert, curl }: args: -let - sourceDateEpoch = - if args ? sourceDateEpoch then args.sourceDateEpoch else 1; -in stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; version = args.version; diff --git a/script/node-modules-snapshot.sh b/script/node-modules-snapshot.sh deleted file mode 100755 index 57f7e17378a..00000000000 --- a/script/node-modules-snapshot.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -system="${1:-unknown}" -suffix="${2:-}" -label="$system" -if [ -n "$suffix" ]; then - label="$system-$suffix" -fi - -root="$(pwd)" -work="$root/.tmp-node-modules" - -cleanup() { - rm -rf "$work" - if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then - rm -rf "$HOME" - fi - if [ -n "${BUN_INSTALL_CACHE_DIR:-}" ] && [ -d "$BUN_INSTALL_CACHE_DIR" ]; then - rm -rf "$BUN_INSTALL_CACHE_DIR" - fi -} - -trap cleanup EXIT - -rm -rf "$work" -mkdir -p "$work" - -export HOME -export BUN_INSTALL_CACHE_DIR -HOME="$(mktemp -d)" -BUN_INSTALL_CACHE_DIR="$(mktemp -d)" - -while IFS= read -r existing; do - rm -rf "$existing" -done < <(find . -type d -name node_modules -prune | sort -r) - -bun install \ - --frozen-lockfile \ - --ignore-scripts \ - --no-progress \ - --linker=isolated - -bun --bun nix/scripts/canonicalize-node-modules.ts -bun --bun nix/scripts/normalize-bun-binaries.ts - -i=0 -while IFS= read -r dir; do - rel="${dir#./}" - dest="$work/$rel" - mkdir -p "$(dirname "$dest")" - cp -R "$dir" "$dest" - i=$((i + 1)) -done < <(find . -type d -name node_modules -prune | sort) - -hash_path="node-modules-${label}.sha256" -nix hash path --sri "$work" | tee "$hash_path" - -tar_name="node-modules-${label}.tar.gz" -tar -czf "$tar_name" -C "$work" . - -manifest="node-modules-${label}.list" -find "$work" -print | sed "s|$work/||" | sort > "$manifest" - -summary="node-modules-${label}.txt" -{ - printf "system=%s\n" "$system" - printf "suffix=%s\n" "$suffix" - printf "hash=%s\n" "$(cat "$hash_path")" - printf "directories=%s\n" "$i" - printf "tar=%s\n" "$tar_name" - printf "manifest=%s\n" "$manifest" -} > "$summary" From 6c9ec8fd78cc1d8277e0ce8d590d3b415a2063fb Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 16 Nov 2025 20:51:03 +1100 Subject: [PATCH 66/72] nix flake: add fzf/ripgrep runtime deps to the wrapper --- nix/opencode.nix | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nix/opencode.nix b/nix/opencode.nix index 2148265870d..220dbc1b27c 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -1,4 +1,4 @@ -{ lib, stdenv, stdenvNoCC, bun, makeBinaryWrapper }: +{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }: args: let scripts = args.scripts; @@ -84,9 +84,8 @@ stdenvNoCC.mkDerivation (finalAttrs: { runHook postInstall ''; - postFixup = lib.optionalString stdenvNoCC.hostPlatform.isLinux '' - wrapProgram $out/bin/opencode \ - --set LD_LIBRARY_PATH "${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}" + postFixup = '' + wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}${lib.optionalString stdenvNoCC.hostPlatform.isLinux " --set LD_LIBRARY_PATH \"${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}\""} ''; meta = { From 3604e9adae491c50903b1ada3adaaa7bb88c8e17 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 16 Nov 2025 20:55:59 +1100 Subject: [PATCH 67/72] nix: fetch bun deps for all platforms --- .github/workflows/update-nix-hashes.yml | 164 +----------------------- flake.nix | 14 +- nix/hashes.json | 7 +- nix/node-modules.nix | 2 + nix/scripts/update-hashes.sh | 118 ++++++++--------- 5 files changed, 67 insertions(+), 238 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 2034f2fbd86..fe388697e8d 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -17,32 +17,10 @@ on: - 'packages/*/package.json' jobs: - cancel-previous: + update: runs-on: ubuntu-latest - steps: - - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ github.token }} - - verify: - needs: cancel-previous - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - system: x86_64-linux - platform: linux - - runner: ubuntu-24.04-arm - system: aarch64-linux - platform: linux - - runner: macos-latest - system: aarch64-darwin - platform: darwin - - runner: macos-15-intel - system: x86_64-darwin - platform: darwin - runs-on: ${{ matrix.runner }} + env: + SYSTEM: x86_64-linux steps: - name: Checkout repository @@ -59,140 +37,12 @@ jobs: git config --global user.email "opencode@sst.dev" git config --global user.name "opencode" - - name: Build and extract hashes - id: build_extract - continue-on-error: true - run: | - set -euo pipefail - echo "Building for ${{ matrix.system }} to verify/extract hashes..." - - BUILD_LOG=$(mktemp) - nix build .#packages.${{ matrix.system }}.default --system ${{ matrix.system }} -L 2>&1 | tee "$BUILD_LOG" || BUILD_FAILED=true - - if [ -z "${BUILD_FAILED:-}" ]; then - echo "Build successful with current hashes." - echo "needs_update=false" >> "$GITHUB_OUTPUT" - rm -f "$BUILD_LOG" - exit 0 - fi - - echo "Build failed, extracting correct hash..." - CORRECT_HASH=$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true) - - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH=$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true) - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to extract hash from build output" - tail -100 "$BUILD_LOG" - rm -f "$BUILD_LOG" - exit 1 - fi - - echo "Extracted hash: $CORRECT_HASH" - echo "needs_update=true" >> "$GITHUB_OUTPUT" - echo "hash=$CORRECT_HASH" >> "$GITHUB_OUTPUT" - rm -f "$BUILD_LOG" - - - name: Update hash file - if: steps.build_extract.outputs.needs_update == 'true' - run: | - set -euo pipefail - HASH_FILE="nix/hashes.json" - SYSTEM="${{ matrix.system }}" - CORRECT_HASH="${{ steps.build_extract.outputs.hash }}" - - if [ ! -f "$HASH_FILE" ]; then - mkdir -p "$(dirname "$HASH_FILE")" - jq -n '{nodeModules: {}}' > "$HASH_FILE" - fi - - echo "Updating hash for ${SYSTEM} to ${CORRECT_HASH}" - tmp=$(mktemp) - jq --arg system "$SYSTEM" --arg value "$CORRECT_HASH" '.nodeModules[$system] = $value' "$HASH_FILE" >"$tmp" - mv "$tmp" "$HASH_FILE" - - echo "Hash file updated." - - - name: Upload hash changes - uses: actions/upload-artifact@v4 - with: - name: hashes-${{ matrix.system }} - path: nix/hashes.json - retention-days: 1 - - commit-hashes: - needs: verify - if: always() - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Configure git - run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - - - name: Download all hash files - uses: actions/download-artifact@v4 - with: - path: hash-artifacts - - - name: Sync branch state - env: - TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} - run: | - set -euo pipefail - BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - git fetch origin "$BRANCH" - if git show-ref --verify --quiet "refs/heads/$BRANCH"; then - git checkout "$BRANCH" - else - git checkout -b "$BRANCH" - fi - git reset --hard "origin/$BRANCH" - - - name: Merge hash files - id: merge_hashes + - name: Update node_modules hash run: | set -euo pipefail + nix/scripts/update-hashes.sh - BASE_HASH="nix/hashes.json" - if [ ! -f "$BASE_HASH" ]; then - mkdir -p "$(dirname "$BASE_HASH")" - jq -n '{nodeModules: {}}' > "$BASE_HASH" - fi - for system in x86_64-linux aarch64-linux aarch64-darwin x86_64-darwin; do - if [ -f "hash-artifacts/hashes-${system}/hashes.json" ]; then - echo "Merging ${system} hashes..." - # Deep merge: preserve all keys in nested objects - jq --arg sys "$system" \ - --slurpfile new "hash-artifacts/hashes-${system}/hashes.json" \ - '.nodeModules[$sys] = $new[0].nodeModules[$sys]' \ - "$BASE_HASH" > /tmp/merged.json - mv /tmp/merged.json "$BASE_HASH" - else - echo "Warning: No hash artifact found for ${system}" - fi - done - - echo "Final merged hashes.json:" - cat "$BASE_HASH" - - DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - HAS_NODE=$(jq --arg dummy "$DUMMY" '[ (.nodeModules // {})[] | select(. != $dummy) ] | length' "$BASE_HASH") - if [ "$HAS_NODE" -gt 0 ]; then - echo "has_data=true" >> "$GITHUB_OUTPUT" - else - echo "has_data=false" >> "$GITHUB_OUTPUT" - fi - - - name: Commit Nix hash changes + - name: Commit hash changes env: TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} run: | @@ -220,7 +70,7 @@ jobs: exit 0 fi - git add flake.nix nix/node-modules.nix nix/hashes.json + git add "${FILES[@]}" git commit -m "Update Nix hashes" BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" diff --git a/flake.nix b/flake.nix index 9e5a9fcefd3..a6614a5dc9c 100644 --- a/flake.nix +++ b/flake.nix @@ -27,19 +27,11 @@ "aarch64-darwin" = "bun-darwin-arm64"; "x86_64-darwin" = "bun-darwin-x64"; }; - defaultNodeModules = builtins.listToAttrs ( - map (system: { - name = system; - value = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - }) systems - ); + defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; hashesFile = "${./nix}/hashes.json"; hashesData = if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { }; - hashes = { - nodeModules = defaultNodeModules // (hashesData.nodeModules or { }); - optional = hashesData.optional or { }; - }; + nodeModulesHash = hashesData.nodeModules or defaultNodeModules; modelsDev = forEachSystem ( system: let @@ -72,7 +64,7 @@ let pkgs = pkgsFor system; mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { - hash = hashes.nodeModules.${system}; + hash = nodeModulesHash; }; mkPackage = pkgs.callPackage ./nix/opencode.nix { }; in diff --git a/nix/hashes.json b/nix/hashes.json index 0a214f77ef8..9a9935b505a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,3 @@ { - "nodeModules": { - "x86_64-linux": "sha256-2DZvke70drgT9yuXTcWyqlLnlIoBFWGg96F9nL2Qjrk=", - "aarch64-linux": "sha256-Rt60mm+gVPr9GFKqPz0oxOuPMQpBtmm1+EMhblfe95k=", - "aarch64-darwin": "sha256-oves/qyrOFabfqPCwlDceJzuVch2FYpNTSJTGN+pcLg=", - "x86_64-darwin": "sha256-/Ky7JYedxGQJL0a92m/od4QQa2YYJTH4Jw0OvCfP6Ok=" - } + "nodeModules": "sha256-cW4qtVdlRrbxkI0VHOQEwW7Ze1zdh9mXdxYU+OYir/E=" } diff --git a/nix/node-modules.nix b/nix/node-modules.nix index 2253d0f0513..7b22ef8e7da 100644 --- a/nix/node-modules.nix +++ b/nix/node-modules.nix @@ -21,6 +21,8 @@ stdenvNoCC.mkDerivation { export HOME=$(mktemp -d) export BUN_INSTALL_CACHE_DIR=$(mktemp -d) bun install \ + --cpu="*" \ + --os="*" \ --frozen-lockfile \ --ignore-scripts \ --no-progress \ diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh index c3e7bfaadf6..7bf183c5b32 100755 --- a/nix/scripts/update-hashes.sh +++ b/nix/scripts/update-hashes.sh @@ -3,13 +3,14 @@ set -euo pipefail DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +SYSTEM=${SYSTEM:-x86_64-linux} DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} if [ ! -f "$HASH_FILE" ]; then - cat <<'EOF' >"$HASH_FILE" + cat >"$HASH_FILE" <"$temp" + jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp" mv "$temp" "$HASH_FILE" } -for SYSTEM in $SYSTEMS; do - TARGET="packages.${SYSTEM}.default" - MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" - CORRECT_HASH="" +TARGET="packages.${SYSTEM}.default" +MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" +CORRECT_HASH="" + +DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" - DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" +echo "Setting dummy node_modules outputHash for ${SYSTEM}..." +write_node_modules_hash "$DUMMY" - echo "Setting dummy node_modules outputHash for ${SYSTEM}..." - write_node_modules_hash "$DUMMY" +BUILD_LOG=$(mktemp) +JSON_OUTPUT=$(mktemp) - BUILD_LOG=$(mktemp) - JSON_OUTPUT=$(mktemp) +echo "Building node_modules for ${SYSTEM} to discover correct outputHash..." +echo "Attempting to realize derivation: ${DRV_PATH}" +REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) + +BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) +if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then + echo "Realized node_modules output: $BUILD_PATH" + CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) +fi - echo "Building node_modules for ${SYSTEM} to discover correct outputHash..." - echo "Attempting to realize derivation: ${DRV_PATH}" - REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) +if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" - BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) - if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then - echo "Realized node_modules output: $BUILD_PATH" - CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" fi if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + echo "Searching for kept failed build directory..." + KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" + if [ -z "$KEPT_DIR" ]; then + KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1) fi - if [ -z "$CORRECT_HASH" ]; then - echo "Searching for kept failed build directory..." - KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) - - if [ -z "$KEPT_DIR" ]; then - KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1) + if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then + echo "Found kept build directory: $KEPT_DIR" + if [ -d "$KEPT_DIR/build" ]; then + HASH_PATH="$KEPT_DIR/build" + else + HASH_PATH="$KEPT_DIR" fi - if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then - echo "Found kept build directory: $KEPT_DIR" - if [ -d "$KEPT_DIR/build" ]; then - HASH_PATH="$KEPT_DIR/build" - else - HASH_PATH="$KEPT_DIR" - fi - - echo "Attempting to hash: $HASH_PATH" - ls -la "$HASH_PATH" || true - - if [ -d "$HASH_PATH/node_modules" ]; then - CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) - echo "Computed hash from kept build: $CORRECT_HASH" - fi + echo "Attempting to hash: $HASH_PATH" + ls -la "$HASH_PATH" || true + + if [ -d "$HASH_PATH/node_modules" ]; then + CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) + echo "Computed hash from kept build: $CORRECT_HASH" fi fi fi +fi - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to determine correct node_modules hash for ${SYSTEM}." - echo "Build log:" - cat "$BUILD_LOG" - exit 1 - fi +if [ -z "$CORRECT_HASH" ]; then + echo "Failed to determine correct node_modules hash for ${SYSTEM}." + echo "Build log:" + cat "$BUILD_LOG" + exit 1 +fi - write_node_modules_hash "$CORRECT_HASH" +write_node_modules_hash "$CORRECT_HASH" - jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null +jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null - echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" +echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" - rm -f "$BUILD_LOG" - unset BUILD_LOG -done \ No newline at end of file +rm -f "$BUILD_LOG" +unset BUILD_LOG From c88230f3c06d9f94da37ab51696498c5b9316843 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 16 Nov 2025 21:46:00 +1100 Subject: [PATCH 68/72] nix flake: drop LD_LIBRARY_PATH --- nix/opencode.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/opencode.nix b/nix/opencode.nix index 220dbc1b27c..bec2997608e 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -85,7 +85,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { ''; postFixup = '' - wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}${lib.optionalString stdenvNoCC.hostPlatform.isLinux " --set LD_LIBRARY_PATH \"${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}\""} + wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} ''; meta = { From 8826b06228c41647c4d1677eef89bdafd819f8a2 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 17 Nov 2025 22:36:28 +0000 Subject: [PATCH 69/72] Update Nix hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 9a9935b505a..16b1f9e5a87 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-cW4qtVdlRrbxkI0VHOQEwW7Ze1zdh9mXdxYU+OYir/E=" + "nodeModules": "sha256-kz3Iwdui16bniVzH+L+tvwyNC3u/fGmpuYCmqcRALOA=" } From dd21c9e1d151ee0dcb10b4c4de00ae3afc5b0b75 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 17 Nov 2025 23:50:43 +0000 Subject: [PATCH 70/72] Update Nix hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 16b1f9e5a87..b66407b4f1c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-kz3Iwdui16bniVzH+L+tvwyNC3u/fGmpuYCmqcRALOA=" + "nodeModules": "sha256-srbGIRjvpqUF+jWq4GAx7sGAasq02dRySnxTjijJJT8=" } From 09295c4f48fbd84236417db7a183c6062f804973 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Tue, 18 Nov 2025 12:51:57 +1100 Subject: [PATCH 71/72] nix flake: update readme to include nix method --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f136ee05e73..4173fb8115b 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,10 @@ curl -fsSL https://opencode.ai/install | bash npm i -g opencode-ai@latest # or bun/pnpm/yarn scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows -brew install opencode # macOS and Linux +brew install opencode # macOS and Linux paru -S opencode-bin # Arch Linux mise use --pin -g ubi:sst/opencode # Any OS +nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch ``` > [!TIP] From 07c652d9a99eeeca137fd11185fa169c9b946185 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 18 Nov 2025 06:15:40 +0000 Subject: [PATCH 72/72] build: update SDK client utils after build --- packages/sdk/js/src/gen/client/utils.gen.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/sdk/js/src/gen/client/utils.gen.ts b/packages/sdk/js/src/gen/client/utils.gen.ts index c159584cce4..209bfbe8e62 100644 --- a/packages/sdk/js/src/gen/client/utils.gen.ts +++ b/packages/sdk/js/src/gen/client/utils.gen.ts @@ -155,11 +155,8 @@ export const buildUrl: Client["buildUrl"] = (options) => export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b } - if (config.baseUrl) { - const base = - typeof config.baseUrl === "string" ? config.baseUrl : String(config.baseUrl) - const trimmed = base.endsWith("/") ? base.substring(0, base.length - 1) : base - config.baseUrl = trimmed + if (config.baseUrl?.endsWith("/")) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) } config.headers = mergeHeaders(a.headers, b.headers) return config