mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
feat(nix): overhaul nix flake and packages (#9032)
This commit is contained in:
77
.github/workflows/update-nix-hashes.yml
vendored
77
.github/workflows/update-nix-hashes.yml
vendored
@@ -19,84 +19,7 @@ on:
|
||||
- ".github/workflows/update-nix-hashes.yml"
|
||||
|
||||
jobs:
|
||||
update-flake:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
TITLE: flake.lock
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Update ${{ env.TITLE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Updating $TITLE..."
|
||||
nix flake update
|
||||
echo "$TITLE updated successfully"
|
||||
|
||||
- name: Commit ${{ env.TITLE }} changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Checking for changes in tracked files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix $TITLE"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
|
||||
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
FILES=(flake.lock flake.nix)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "No changes detected."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "Committing changes..."
|
||||
git commit -m "Update $TITLE"
|
||||
echo "Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase --autostash origin "$BRANCH"
|
||||
echo "Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
compute-node-modules-hash:
|
||||
needs: update-flake
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768456270,
|
||||
"narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=",
|
||||
"lastModified": 1768302833,
|
||||
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f4606b01b39e09065df37905a2133905246db9ed",
|
||||
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
110
flake.nix
110
flake.nix
@@ -6,11 +6,7 @@
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
{ self, nixpkgs, ... }:
|
||||
let
|
||||
systems = [
|
||||
"aarch64-linux"
|
||||
@@ -18,99 +14,35 @@
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
inherit (nixpkgs) lib;
|
||||
forEachSystem = lib.genAttrs systems;
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||
bunTarget = {
|
||||
"aarch64-linux" = "bun-linux-arm64";
|
||||
"x86_64-linux" = "bun-linux-x64";
|
||||
"aarch64-darwin" = "bun-darwin-arm64";
|
||||
"x86_64-darwin" = "bun-darwin-x64";
|
||||
};
|
||||
|
||||
# Parse "bun-{os}-{cpu}" to {os, cpu}
|
||||
parseBunTarget =
|
||||
target:
|
||||
let
|
||||
parts = lib.splitString "-" target;
|
||||
in
|
||||
{
|
||||
os = builtins.elemAt parts 1;
|
||||
cpu = builtins.elemAt parts 2;
|
||||
};
|
||||
|
||||
hashesFile = "${./nix}/hashes.json";
|
||||
hashesData =
|
||||
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
|
||||
# Lookup hash: supports per-system ({system: hash}) or legacy single hash
|
||||
nodeModulesHashFor =
|
||||
system:
|
||||
if builtins.isAttrs hashesData.nodeModules then
|
||||
hashesData.nodeModules.${system}
|
||||
else
|
||||
hashesData.nodeModules;
|
||||
modelsDev = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
pkgs."models-dev"
|
||||
);
|
||||
forEachSystem = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||
rev = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
devShells = forEachSystem (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
packages = forEachSystem (
|
||||
system:
|
||||
pkgs:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
bunPlatform = parseBunTarget bunTarget.${system};
|
||||
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||
hash = nodeModulesHashFor system;
|
||||
bunCpu = bunPlatform.cpu;
|
||||
bunOs = bunPlatform.os;
|
||||
opencode = pkgs.callPackage ./nix/opencode.nix {
|
||||
inherit rev;
|
||||
};
|
||||
mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
|
||||
mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
|
||||
|
||||
opencodePkg = mkOpencode {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
inherit mkNodeModules;
|
||||
};
|
||||
|
||||
desktopPkg = mkDesktop {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
mkNodeModules = mkNodeModules;
|
||||
opencode = opencodePkg;
|
||||
desktop = pkgs.callPackage ./nix/desktop.nix {
|
||||
inherit opencode;
|
||||
};
|
||||
in
|
||||
{
|
||||
default = self.packages.${system}.opencode;
|
||||
opencode = opencodePkg;
|
||||
desktop = desktopPkg;
|
||||
default = opencode;
|
||||
inherit opencode desktop;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const dir = process.cwd()
|
||||
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
|
||||
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||
const version = process.env.OPENCODE_VERSION ?? "local"
|
||||
const channel = process.env.OPENCODE_CHANNEL ?? "local"
|
||||
|
||||
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts", worker, parser],
|
||||
outdir: "./dist",
|
||||
target: "bun",
|
||||
sourcemap: "none",
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
external: ["@opentui/core"],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${version}'`,
|
||||
OPENCODE_CHANNEL: `'${channel}'`,
|
||||
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
|
||||
OPENCODE_WORKER_PATH: "undefined",
|
||||
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error("bundle failed")
|
||||
for (const log of result.logs) console.error(log)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
|
||||
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
|
||||
await Bun.write(parserOut, Bun.file(parser))
|
||||
191
nix/desktop.nix
191
nix/desktop.nix
@@ -2,166 +2,99 @@
|
||||
lib,
|
||||
stdenv,
|
||||
rustPlatform,
|
||||
bun,
|
||||
pkg-config,
|
||||
dbus ? null,
|
||||
openssl,
|
||||
glib ? null,
|
||||
gtk3 ? null,
|
||||
libsoup_3 ? null,
|
||||
webkitgtk_4_1 ? null,
|
||||
librsvg ? null,
|
||||
libappindicator-gtk3 ? null,
|
||||
cargo-tauri,
|
||||
bun,
|
||||
nodejs,
|
||||
cargo,
|
||||
rustc,
|
||||
makeBinaryWrapper,
|
||||
copyDesktopItems,
|
||||
makeDesktopItem,
|
||||
nodejs,
|
||||
jq,
|
||||
wrapGAppsHook4,
|
||||
makeWrapper,
|
||||
dbus,
|
||||
glib,
|
||||
gtk4,
|
||||
libsoup_3,
|
||||
librsvg,
|
||||
libappindicator,
|
||||
glib-networking,
|
||||
openssl,
|
||||
webkitgtk_4_1,
|
||||
gst_all_1,
|
||||
opencode,
|
||||
}:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
in
|
||||
rustPlatform.buildRustPackage rec {
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "opencode-desktop";
|
||||
version = args.version;
|
||||
inherit (opencode)
|
||||
version
|
||||
src
|
||||
node_modules
|
||||
patches
|
||||
;
|
||||
|
||||
src = args.src;
|
||||
|
||||
# We need to set the root for cargo, but we also need access to the whole repo.
|
||||
postUnpack = ''
|
||||
# Update sourceRoot to point to the tauri app
|
||||
sourceRoot+=/packages/desktop/src-tauri
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ../packages/desktop/src-tauri/Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
node_modules = mkModules {
|
||||
version = version;
|
||||
src = src;
|
||||
};
|
||||
cargoRoot = "packages/desktop/src-tauri";
|
||||
cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock;
|
||||
buildAndTestSubdir = finalAttrs.cargoRoot;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
cargo-tauri.hook
|
||||
bun
|
||||
makeBinaryWrapper
|
||||
copyDesktopItems
|
||||
nodejs # for patchShebangs node_modules
|
||||
cargo
|
||||
rustc
|
||||
nodejs
|
||||
jq
|
||||
];
|
||||
|
||||
# based on packages/desktop/src-tauri/release/appstream.metainfo.xml
|
||||
desktopItems = lib.optionals stdenv.isLinux [
|
||||
(makeDesktopItem {
|
||||
name = "ai.opencode.opencode";
|
||||
desktopName = "OpenCode";
|
||||
comment = "Open source AI coding agent";
|
||||
exec = "opencode-desktop";
|
||||
icon = "opencode";
|
||||
terminal = false;
|
||||
type = "Application";
|
||||
categories = [ "Development" "IDE" ];
|
||||
startupWMClass = "opencode";
|
||||
})
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
openssl
|
||||
makeWrapper
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
|
||||
|
||||
buildInputs = lib.optionals stdenv.isLinux [
|
||||
dbus
|
||||
glib
|
||||
gtk3
|
||||
gtk4
|
||||
libsoup_3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
libappindicator-gtk3
|
||||
libappindicator
|
||||
glib-networking
|
||||
openssl
|
||||
webkitgtk_4_1
|
||||
gst_all_1.gstreamer
|
||||
gst_all_1.gst-plugins-base
|
||||
gst_all_1.gst-plugins-good
|
||||
];
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
preBuild = ''
|
||||
# Restore node_modules
|
||||
pushd ../../..
|
||||
|
||||
# Copy node_modules from the fixed-output derivation
|
||||
# We use cp -r --no-preserve=mode to ensure we can write to them if needed,
|
||||
# though we usually just read.
|
||||
cp -r ${node_modules}/node_modules .
|
||||
cp -r ${node_modules}/packages .
|
||||
|
||||
# Ensure node_modules is writable so patchShebangs can update script headers
|
||||
chmod -R u+w node_modules
|
||||
# Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo)
|
||||
chmod -R u+w packages
|
||||
# Patch shebangs so scripts can run
|
||||
cp -a ${finalAttrs.node_modules}/{node_modules,packages} .
|
||||
chmod -R u+w node_modules packages
|
||||
patchShebangs node_modules
|
||||
patchShebangs packages/desktop/node_modules
|
||||
|
||||
# Copy sidecar
|
||||
mkdir -p packages/desktop/src-tauri/sidecars
|
||||
targetTriple=${stdenv.hostPlatform.rust.rustcTarget}
|
||||
cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple
|
||||
|
||||
# Merge prod config into tauri.conf.json
|
||||
if ! jq -s '.[0] * .[1]' \
|
||||
packages/desktop/src-tauri/tauri.conf.json \
|
||||
packages/desktop/src-tauri/tauri.prod.conf.json \
|
||||
> packages/desktop/src-tauri/tauri.conf.json.tmp; then
|
||||
echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json
|
||||
|
||||
# Build the frontend
|
||||
cd packages/desktop
|
||||
|
||||
# The 'build' script runs 'bun run typecheck && vite build'.
|
||||
bun run build
|
||||
|
||||
popd
|
||||
cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget}
|
||||
'';
|
||||
|
||||
# Tauri bundles the assets during the rust build phase (which happens after preBuild).
|
||||
# It looks for them in the location specified in tauri.conf.json.
|
||||
# see publish-tauri job in .github/workflows/publish.yml
|
||||
tauriBuildFlags = [
|
||||
"--config"
|
||||
"tauri.prod.conf.json"
|
||||
"--no-sign" # no code signing or auto updates
|
||||
];
|
||||
|
||||
postInstall = lib.optionalString stdenv.isLinux ''
|
||||
# Install icon
|
||||
mkdir -p $out/share/icons/hicolor/128x128/apps
|
||||
cp ../../../packages/desktop/src-tauri/icons/prod/128x128.png $out/share/icons/hicolor/128x128/apps/opencode.png
|
||||
|
||||
# Wrap the binary to ensure it finds the libraries
|
||||
wrapProgram $out/bin/opencode-desktop \
|
||||
--prefix LD_LIBRARY_PATH : ${
|
||||
lib.makeLibraryPath [
|
||||
gtk3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
glib
|
||||
libsoup_3
|
||||
]
|
||||
}
|
||||
# FIXME: workaround for concerns about case insensitive filesystems
|
||||
# should be removed once binary is renamed or decided otherwise
|
||||
# darwin output is a .app bundle so no conflict
|
||||
postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
|
||||
mv $out/bin/OpenCode $out/bin/opencode-desktop
|
||||
sed -i 's|^Exec=OpenCode$|Exec=opencode-desktop|' $out/share/applications/OpenCode.desktop
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
meta = {
|
||||
description = "OpenCode Desktop App";
|
||||
homepage = "https://opencode.ai";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "opencode-desktop";
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
inherit (opencode.meta) platforms;
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
hash,
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
cacert,
|
||||
curl,
|
||||
bunCpu,
|
||||
bunOs,
|
||||
}:
|
||||
args:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "opencode-node_modules";
|
||||
inherit (args) version src;
|
||||
|
||||
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
cacert
|
||||
curl
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
export HOME=$(mktemp -d)
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
bun install \
|
||||
--cpu="${bunCpu}" \
|
||||
--os="${bunOs}" \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress \
|
||||
--linker=isolated
|
||||
bun --bun ${args.canonicalizeScript}
|
||||
bun --bun ${args.normalizeBinsScript}
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
while IFS= read -r dir; do
|
||||
rel="''${dir#./}"
|
||||
dest="$out/$rel"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp -R "$dir" "$dest"
|
||||
done < <(find . -type d -name node_modules -prune | sort)
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash = hash;
|
||||
}
|
||||
211
nix/opencode.nix
211
nix/opencode.nix
@@ -2,60 +2,115 @@
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
ripgrep,
|
||||
sysctl,
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
rev ? "dirty",
|
||||
}:
|
||||
args:
|
||||
let
|
||||
inherit (args) scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
packageJson = lib.pipe ../packages/opencode/package.json [
|
||||
builtins.readFile
|
||||
builtins.fromJSON
|
||||
];
|
||||
in
|
||||
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
pname = "opencode";
|
||||
inherit (args) version src;
|
||||
version = "${packageJson.version}-${rev}";
|
||||
|
||||
node_modules = mkModules {
|
||||
src = lib.fileset.toSource {
|
||||
root = ../.;
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../packages
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
node_modules = stdenvNoCC.mkDerivation {
|
||||
pname = "${finalAttrs.pname}-node_modules";
|
||||
inherit (finalAttrs) version src;
|
||||
|
||||
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
export HOME=$(mktemp -d)
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
bun install \
|
||||
--cpu="${if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64"}" \
|
||||
--os="${if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin"}" \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress \
|
||||
--linker=isolated
|
||||
bun --bun ${./scripts/canonicalize-node-modules.ts}
|
||||
bun --bun ${./scripts/normalize-bun-binaries.ts}
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out
|
||||
find . -type d -name node_modules -exec cp -R --parents {} $out \;
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash =
|
||||
(lib.pipe ./hashes.json [
|
||||
builtins.readFile
|
||||
builtins.fromJSON
|
||||
]).nodeModules.${stdenvNoCC.hostPlatform.system};
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
installShellFiles
|
||||
makeBinaryWrapper
|
||||
models-dev
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
|
||||
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||
env.OPENCODE_VERSION = args.version;
|
||||
env.OPENCODE_CHANNEL = "stable";
|
||||
dontConfigure = true;
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
|
||||
cp -R ${finalAttrs.node_modules}/. .
|
||||
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
|
||||
env.OPENCODE_VERSION = finalAttrs.version;
|
||||
env.OPENCODE_CHANNEL = "local";
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||
cp -r ${finalAttrs.node_modules}/packages .
|
||||
|
||||
(
|
||||
cd packages/opencode
|
||||
|
||||
chmod -R u+w ./node_modules
|
||||
mkdir -p ./node_modules/@opencode-ai
|
||||
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
|
||||
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
|
||||
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
|
||||
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
|
||||
|
||||
cp ${./bundle.ts} ./bundle.ts
|
||||
chmod +x ./bundle.ts
|
||||
bun run ./bundle.ts
|
||||
)
|
||||
cd ./packages/opencode
|
||||
bun --bun ./script/build.ts --single --skip-install
|
||||
bun --bun ./script/schema.ts schema.json
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
@@ -63,76 +118,52 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
cd packages/opencode
|
||||
if [ ! -d dist ]; then
|
||||
echo "ERROR: dist directory missing after bundle step"
|
||||
exit 1
|
||||
fi
|
||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||
|
||||
mkdir -p $out/lib/opencode
|
||||
cp -r dist $out/lib/opencode/
|
||||
chmod -R u+w $out/lib/opencode/dist
|
||||
|
||||
# Select bundled worker assets deterministically (sorted find output)
|
||||
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||
if [ -z "$worker_file" ]; then
|
||||
echo "ERROR: bundled worker not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
|
||||
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
|
||||
for patch_file in "$worker_file" "$parser_worker_file"; do
|
||||
[ -z "$patch_file" ] && continue
|
||||
[ ! -f "$patch_file" ] && continue
|
||||
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
|
||||
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
|
||||
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p $out/lib/opencode/node_modules
|
||||
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
|
||||
mkdir -p $out/lib/opencode/node_modules/@opentui
|
||||
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||
--add-flags "run" \
|
||||
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||
--prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
|
||||
--argv0 opencode
|
||||
wrapProgram $out/bin/opencode \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
ripgrep
|
||||
]
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
|
||||
if [ -d "$pkg" ]; then
|
||||
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
|
||||
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
|
||||
$out/lib/opencode/node_modules/@opentui/$pkgName
|
||||
fi
|
||||
done
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# trick yargs into also generating zsh completions
|
||||
installShellCompletion --cmd opencode \
|
||||
--bash <($out/bin/opencode completion) \
|
||||
--zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
nativeInstallCheckInputs = [
|
||||
versionCheckHook
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
doInstallCheck = true;
|
||||
versionCheckKeepEnvironment = [ "HOME" ];
|
||||
versionCheckProgramArg = "--version";
|
||||
|
||||
passthru = {
|
||||
jsonschema = "${placeholder "out"}/share/opencode/schema.json";
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "AI coding agent built for the terminal";
|
||||
longDescription = ''
|
||||
OpenCode is a terminal-based agent that can build anything.
|
||||
It combines a TypeScript/JavaScript core with a Go-based TUI
|
||||
to provide an interactive AI coding experience.
|
||||
'';
|
||||
homepage = "https://github.com/anomalyco/opencode";
|
||||
description = "The open source coding agent";
|
||||
homepage = "https://opencode.ai/";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "opencode";
|
||||
platforms = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
mainProgram = "opencode";
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const version = "@VERSION@"
|
||||
const pkg = path.join(process.cwd(), "packages/opencode")
|
||||
const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"))
|
||||
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||
const target = process.env["BUN_COMPILE_TARGET"]
|
||||
|
||||
if (!target) {
|
||||
throw new Error("BUN_COMPILE_TARGET not set")
|
||||
}
|
||||
|
||||
process.chdir(pkg)
|
||||
|
||||
const manifestName = "opencode-assets.manifest"
|
||||
const manifestPath = path.join(pkg, manifestName)
|
||||
|
||||
const readTrackedAssets = () => {
|
||||
if (!fs.existsSync(manifestPath)) return []
|
||||
return fs
|
||||
.readFileSync(manifestPath, "utf8")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
const removeTrackedAssets = () => {
|
||||
for (const file of readTrackedAssets()) {
|
||||
const filePath = path.join(pkg, file)
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.rmSync(filePath, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assets = new Set<string>()
|
||||
|
||||
const addAsset = async (p: string) => {
|
||||
const file = path.basename(p)
|
||||
const dest = path.join(pkg, file)
|
||||
await Bun.write(dest, Bun.file(p))
|
||||
assets.add(file)
|
||||
}
|
||||
|
||||
removeTrackedAssets()
|
||||
|
||||
const result = await Bun.build({
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
sourcemap: "external",
|
||||
entrypoints: ["./src/index.ts", parser, worker],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'@VERSION@'`,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"),
|
||||
OPENCODE_CHANNEL: "'latest'",
|
||||
},
|
||||
compile: {
|
||||
target,
|
||||
outfile: "opencode",
|
||||
autoloadBunfig: false,
|
||||
autoloadDotenv: false,
|
||||
//@ts-ignore (bun types aren't up to date)
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
execArgv: ["--user-agent=opencode/" + version, "--use-system-ca", "--"],
|
||||
windows: {},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Build failed!")
|
||||
for (const log of result.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
throw new Error("Compilation failed")
|
||||
}
|
||||
|
||||
const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||
for (const x of assetOutputs) {
|
||||
await addAsset(x.path)
|
||||
}
|
||||
|
||||
const bundle = await Bun.build({
|
||||
entrypoints: [worker],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
target: "bun",
|
||||
outdir: "./.opencode-worker",
|
||||
sourcemap: "none",
|
||||
})
|
||||
|
||||
if (!bundle.success) {
|
||||
console.error("Worker build failed!")
|
||||
for (const log of bundle.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
throw new Error("Worker compilation failed")
|
||||
}
|
||||
|
||||
const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||
for (const x of workerAssets) {
|
||||
await addAsset(x.path)
|
||||
}
|
||||
|
||||
const output = bundle.outputs.find((x) => x.kind === "entry-point")
|
||||
if (!output) {
|
||||
throw new Error("Worker build produced no entry-point output")
|
||||
}
|
||||
|
||||
const dest = path.join(pkg, "opencode-worker.js")
|
||||
await Bun.write(dest, Bun.file(output.path))
|
||||
fs.rmSync(path.dirname(output.path), { recursive: true, force: true })
|
||||
|
||||
const list = Array.from(assets)
|
||||
await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "")
|
||||
|
||||
console.log("Build successful!")
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
/**
|
||||
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
|
||||
* argv: [node, script, file, mainWasm, ...wasmPaths]
|
||||
*/
|
||||
const [, , file, mainWasm, ...wasmPaths] = process.argv
|
||||
|
||||
if (!file || !mainWasm) {
|
||||
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(file, "utf8")
|
||||
const byName = new Map<string, string>()
|
||||
|
||||
for (const wasm of wasmPaths) {
|
||||
const name = path.basename(wasm)
|
||||
byName.set(name, wasm)
|
||||
}
|
||||
|
||||
let next = content
|
||||
|
||||
for (const [name, wasmPath] of byName) {
|
||||
next = next.replaceAll(name, wasmPath)
|
||||
}
|
||||
|
||||
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||
|
||||
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||
const nixStorePrefix = process.env.NIX_STORE || "/nix/store"
|
||||
next = next.replace(/(\.\/)+/g, "./")
|
||||
next = next.replace(
|
||||
new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
|
||||
"/$2",
|
||||
)
|
||||
next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
|
||||
if (next !== content) fs.writeFileSync(file, next)
|
||||
@@ -90,6 +90,11 @@ const targets = singleFlag
|
||||
return baselineFlag
|
||||
}
|
||||
|
||||
// also skip abi-specific builds for the same reason
|
||||
if (item.abi !== undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
: allTargets
|
||||
|
||||
Reference in New Issue
Block a user