Skip to content

Commit 3266fe5

Browse files
committed
nix flake: bundle TUI worker for nix builds
1 parent c07919f commit 3266fe5

File tree

4 files changed

+295
-2
lines changed

4 files changed

+295
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ dist
1313
.turbo
1414
**/.serena
1515
.serena/
16+
/result

flake.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,264 @@
3636
}
3737
);
3838

39+
packages = forEachSystem (
40+
system:
41+
let
42+
pkgs = import nixpkgs { inherit system; };
43+
bun-target = {
44+
"aarch64-linux" = "bun-linux-arm64";
45+
"x86_64-linux" = "bun-linux-x64";
46+
};
47+
48+
models-dev = pkgs.stdenvNoCC.mkDerivation {
49+
pname = "models-dev";
50+
version = "unstable";
51+
52+
src = pkgs.fetchurl {
53+
url = "https://models.dev/api.json";
54+
hash = "sha256-xQ1FjLTz8g4YbgZZ97j8FrYeuZd9aDUtLB67I23RQDQ=";
55+
};
56+
57+
dontUnpack = true;
58+
dontBuild = true;
59+
60+
installPhase = ''
61+
mkdir -p $out/dist
62+
cp $src $out/dist/_api.json
63+
'';
64+
};
65+
in
66+
{
67+
default = pkgs.callPackage (
68+
{
69+
lib,
70+
stdenv,
71+
stdenvNoCC,
72+
bun,
73+
makeBinaryWrapper,
74+
}:
75+
stdenvNoCC.mkDerivation (finalAttrs: {
76+
pname = "opencode";
77+
version = "1.0.23";
78+
79+
src = ./.;
80+
81+
node_modules = stdenvNoCC.mkDerivation {
82+
pname = "opencode-node_modules";
83+
inherit (finalAttrs) version src;
84+
85+
impureEnvVars =
86+
lib.fetchers.proxyImpureEnvVars
87+
++ [
88+
"GIT_PROXY_COMMAND"
89+
"SOCKS_SERVER"
90+
];
91+
92+
nativeBuildInputs = [ bun ];
93+
94+
dontConfigure = true;
95+
96+
buildPhase = ''
97+
runHook preBuild
98+
export HOME=$(mktemp -d)
99+
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
100+
bun install \
101+
--frozen-lockfile \
102+
--ignore-scripts \
103+
--no-progress
104+
runHook postBuild
105+
'';
106+
107+
installPhase = ''
108+
runHook preInstall
109+
mkdir -p $out
110+
while IFS= read -r dir; do
111+
rel="''${dir#./}"
112+
dest="$out/$rel"
113+
mkdir -p "$(dirname "$dest")"
114+
cp -R "$dir" "$dest"
115+
done < <(find . -type d -name node_modules -prune)
116+
runHook postInstall
117+
'';
118+
119+
dontFixup = true;
120+
121+
outputHashAlgo = "sha256";
122+
outputHashMode = "recursive";
123+
outputHash = "sha256-S77NbdzNuHALDapU3Qr/lGPwvHCvyGxr+nyVEO9zeBg=";
124+
};
125+
126+
nativeBuildInputs = [
127+
bun
128+
makeBinaryWrapper
129+
];
130+
131+
configurePhase = ''
132+
runHook preConfigure
133+
cp -R ${finalAttrs.node_modules}/. .
134+
runHook postConfigure
135+
'';
136+
137+
env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
138+
139+
buildPhase = ''
140+
runHook preBuild
141+
142+
cat > tsconfig.build.json <<'EOF'
143+
{
144+
"compilerOptions": {
145+
"jsx": "preserve",
146+
"jsxImportSource": "@opentui/solid",
147+
"allowImportingTsExtensions": true,
148+
"baseUrl": ".",
149+
"paths": {
150+
"@/*": ["./packages/opencode/src/*"],
151+
"@tui/*": ["./packages/opencode/src/cli/cmd/tui/*"]
152+
}
153+
}
154+
}
155+
EOF
156+
157+
cat > bun-build.ts <<'EOF'
158+
import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
159+
import path from "path"
160+
import fs from "fs"
161+
162+
const version = "@VERSION@"
163+
const channel = "@CHANNEL@"
164+
const repoRoot = process.cwd()
165+
const packageDir = path.join(repoRoot, "packages/opencode")
166+
167+
const parserWorker = fs.realpathSync(
168+
path.join(packageDir, "./node_modules/@opentui/core/parser.worker.js"),
169+
)
170+
const dir = packageDir
171+
const workerPath = "./src/cli/cmd/tui/worker.ts"
172+
const target = process.env["BUN_COMPILE_TARGET"]
173+
174+
if (!target) {
175+
throw new Error("BUN_COMPILE_TARGET not set")
176+
}
177+
178+
// Change to package directory like the original build script does
179+
process.chdir(packageDir)
180+
181+
const result = await Bun.build({
182+
conditions: ["browser"],
183+
tsconfig: "./tsconfig.json",
184+
plugins: [solidPlugin],
185+
sourcemap: "external",
186+
entrypoints: ["./src/index.ts", parserWorker, workerPath],
187+
define: {
188+
OPENCODE_VERSION: `'@VERSION@'`,
189+
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replace(/\\/g, "/"),
190+
OPENCODE_CHANNEL: `'@CHANNEL@'`,
191+
},
192+
compile: {
193+
target,
194+
outfile: "opencode",
195+
execArgv: ["--user-agent=opencode/" + version, "--env-file=\"\"", "--"],
196+
windows: {},
197+
},
198+
})
199+
200+
if (!result.success) {
201+
console.error("Build failed!")
202+
for (const log of result.logs) {
203+
console.error(log)
204+
}
205+
throw new Error("Compilation failed")
206+
}
207+
208+
// Nix packaging needs a real file for Worker() lookups, so emit a JS bundle alongside the binary.
209+
const workerBundle = await Bun.build({
210+
entrypoints: [workerPath],
211+
tsconfig: "./tsconfig.json",
212+
plugins: [solidPlugin],
213+
target: "bun",
214+
outdir: "./.opencode-worker",
215+
sourcemap: "none",
216+
})
217+
218+
if (!workerBundle.success) {
219+
console.error("Worker build failed!")
220+
for (const log of workerBundle.logs) {
221+
console.error(log)
222+
}
223+
throw new Error("Worker compilation failed")
224+
}
225+
226+
const workerOutput = workerBundle.outputs.find((output) => output.kind === "entry-point")
227+
if (!workerOutput) {
228+
throw new Error("Worker build produced no entry-point output")
229+
}
230+
231+
const workerTarget = path.join(packageDir, "opencode-worker.js")
232+
const workerSource = workerOutput.path
233+
await Bun.write(workerTarget, Bun.file(workerSource))
234+
fs.rmSync(path.dirname(workerSource), { recursive: true, force: true })
235+
236+
console.log("Build successful!")
237+
EOF
238+
239+
substituteInPlace bun-build.ts \
240+
--replace '@VERSION@' "${finalAttrs.version}" \
241+
--replace '@CHANNEL@' "latest"
242+
243+
export BUN_COMPILE_TARGET=${bun-target.${stdenvNoCC.hostPlatform.system}}
244+
bun --bun bun-build.ts
245+
246+
runHook postBuild
247+
'';
248+
249+
dontStrip = true;
250+
251+
installPhase = ''
252+
runHook preInstall
253+
254+
# The binary is created in the package directory after chdir
255+
cd packages/opencode
256+
if [ ! -f opencode ]; then
257+
echo "ERROR: opencode binary not found in $(pwd)"
258+
ls -la
259+
exit 1
260+
fi
261+
if [ ! -f opencode-worker.js ]; then
262+
echo "ERROR: opencode worker bundle not found in $(pwd)"
263+
ls -la
264+
exit 1
265+
fi
266+
267+
install -Dm755 opencode $out/bin/opencode
268+
install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
269+
runHook postInstall
270+
'';
271+
272+
postFixup = lib.optionalString stdenvNoCC.hostPlatform.isLinux ''
273+
wrapProgram $out/bin/opencode \
274+
--set LD_LIBRARY_PATH "${lib.makeLibraryPath [ stdenv.cc.cc.lib ]}"
275+
'';
276+
277+
meta = {
278+
description = "AI coding agent built for the terminal";
279+
longDescription = ''
280+
OpenCode is a terminal-based agent that can build anything.
281+
It combines a TypeScript/JavaScript core with a Go-based TUI
282+
to provide an interactive AI coding experience.
283+
'';
284+
homepage = "https://github.com/sst/opencode";
285+
license = lib.licenses.mit;
286+
platforms = [
287+
"aarch64-linux"
288+
"x86_64-linux"
289+
];
290+
mainProgram = "opencode";
291+
};
292+
})
293+
) { };
294+
}
295+
);
296+
39297
apps = forEachSystem (
40298
system:
41299
let

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,15 @@ export const TuiThreadCommand = cmd({
6868
const baseCwd = process.env.PWD ?? process.cwd()
6969
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
7070
const defaultWorker = new URL("./worker.ts", import.meta.url)
71-
const workerPath =
72-
typeof OPENCODE_WORKER_PATH !== "undefined" ? OPENCODE_WORKER_PATH : defaultWorker
71+
// Nix build creates a bundled worker next to the binary; prefer it when present.
72+
const execDir = path.dirname(process.execPath)
73+
const bundledWorker = path.join(execDir, "opencode-worker.js")
74+
const hasBundledWorker = await Bun.file(bundledWorker).exists()
75+
const workerPath = (() => {
76+
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
77+
if (hasBundledWorker) return bundledWorker
78+
return defaultWorker
79+
})()
7380
try {
7481
process.chdir(cwd)
7582
} catch (e) {

0 commit comments

Comments
 (0)