Vendor opencode source for docker build

This commit is contained in:
southseact-3d
2026-02-07 20:54:46 +00:00
parent b30ff1cfa4
commit efda260214
3195 changed files with 387717 additions and 1 deletions

100
opencode/nix/desktop.nix Normal file
View File

@@ -0,0 +1,100 @@
{
lib,
stdenv,
rustPlatform,
pkg-config,
cargo-tauri,
bun,
nodejs,
cargo,
rustc,
jq,
wrapGAppsHook4,
makeWrapper,
dbus,
glib,
gtk4,
libsoup_3,
librsvg,
libappindicator,
glib-networking,
openssl,
webkitgtk_4_1,
gst_all_1,
opencode,
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "opencode-desktop";
inherit (opencode)
version
src
node_modules
patches
;
cargoRoot = "packages/desktop/src-tauri";
cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock;
buildAndTestSubdir = finalAttrs.cargoRoot;
nativeBuildInputs = [
pkg-config
cargo-tauri.hook
bun
nodejs # for patchShebangs node_modules
cargo
rustc
jq
makeWrapper
] ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
buildInputs = lib.optionals stdenv.isLinux [
dbus
glib
gtk4
libsoup_3
librsvg
libappindicator
glib-networking
openssl
webkitgtk_4_1
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
];
strictDeps = true;
preBuild = ''
cp -a ${finalAttrs.node_modules}/{node_modules,packages} .
chmod -R u+w node_modules packages
patchShebangs node_modules
patchShebangs packages/desktop/node_modules
mkdir -p packages/desktop/src-tauri/sidecars
cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget}
'';
# see publish-tauri job in .github/workflows/publish.yml
tauriBuildFlags = [
"--config"
"tauri.prod.conf.json"
"--no-sign" # no code signing or auto updates
];
# 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 = {
description = "OpenCode Desktop App";
homepage = "https://opencode.ai";
license = lib.licenses.mit;
mainProgram = "opencode-desktop";
inherit (opencode.meta) platforms;
};
})

8
opencode/nix/hashes.json Normal file
View File

@@ -0,0 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-UBz5qXhO+Xy6XptVdbo9V0wKsvZgItmHkWDm6I5VRCk=",
"aarch64-linux": "sha256-G2ezu/ThZR3kYfHnbD0EOcLoAa6hwtICpmo9r+bqibE=",
"aarch64-darwin": "sha256-PhSE23OzNlyfNFP5LffA3AtyN+hsyCeGInmDBBRjr0g=",
"x86_64-darwin": "sha256-vWusYJD+7ClDLUFy1wEqRLf9hY8V43iqdqnZ6YWkh1Q="
}
}

View File

@@ -0,0 +1,83 @@
{
lib,
stdenvNoCC,
bun,
rev ? "dirty",
hash ?
(lib.pipe ./hashes.json [
builtins.readFile
builtins.fromJSON
]).nodeModules.${stdenvNoCC.hostPlatform.system},
}:
let
packageJson = lib.pipe ../packages/opencode/package.json [
builtins.readFile
builtins.fromJSON
];
platform = stdenvNoCC.hostPlatform;
bunCpu = if platform.isAarch64 then "arm64" else "x64";
bunOs = if platform.isLinux then "linux" else "darwin";
in
stdenvNoCC.mkDerivation {
pname = "opencode-node_modules";
version = "${packageJson.version}-${rev}";
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
lib.fileset.unions [
../packages
../bun.lock
../package.json
../patches
../install
]
);
};
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
"GIT_PROXY_COMMAND"
"SOCKS_SERVER"
];
nativeBuildInputs = [ bun ];
dontConfigure = true;
buildPhase = ''
runHook preBuild
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
bun install \
--cpu="${bunCpu}" \
--os="${bunOs}" \
--filter '!./' \
--filter './packages/opencode' \
--filter './packages/desktop' \
--frozen-lockfile \
--ignore-scripts \
--no-progress
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 = hash;
meta.platforms = [
"aarch64-linux"
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
}

96
opencode/nix/opencode.nix Normal file
View File

@@ -0,0 +1,96 @@
{
lib,
stdenvNoCC,
callPackage,
bun,
sysctl,
makeBinaryWrapper,
models-dev,
ripgrep,
installShellFiles,
versionCheckHook,
writableTmpDirAsHomeHook,
node_modules ? callPackage ./node-modules.nix { },
}:
stdenvNoCC.mkDerivation (finalAttrs: {
pname = "opencode";
inherit (node_modules) version src;
inherit node_modules;
nativeBuildInputs = [
bun
installShellFiles
makeBinaryWrapper
models-dev
writableTmpDirAsHomeHook
];
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
cd ./packages/opencode
bun --bun ./script/build.ts --single --skip-install
bun --bun ./script/schema.ts schema.json
runHook postBuild
'';
installPhase = ''
runHook preInstall
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
install -Dm644 schema.json $out/share/opencode/schema.json
wrapProgram $out/bin/opencode \
--prefix PATH : ${
lib.makeBinPath (
[
ripgrep
]
# bun runs sysctl to detect if dunning on rosetta2
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
)
}
runHook postInstall
'';
postInstall = 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)
'';
nativeInstallCheckInputs = [
versionCheckHook
writableTmpDirAsHomeHook
];
doInstallCheck = true;
versionCheckKeepEnvironment = [ "HOME" ];
versionCheckProgramArg = "--version";
passthru = {
jsonschema = "${placeholder "out"}/share/opencode/schema.json";
};
meta = {
description = "The open source coding agent";
homepage = "https://opencode.ai/";
license = lib.licenses.mit;
mainProgram = "opencode";
inherit (node_modules.meta) platforms;
};
})

View File

@@ -0,0 +1,113 @@
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<string, Entry[]>()
for (const entry of directories) {
const full = join(bunRoot, entry)
const info = await lstat(full)
if (!info.isDirectory()) {
continue
}
const parsed = parseEntry(entry)
if (!parsed) {
continue
}
const list = versions.get(parsed.name) ?? []
list.push({ dir: full, version: parsed.version, label: entry })
versions.set(parsed.name, list)
}
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
| SemverLike
| {
default: SemverLike
}
const semver = "default" in semverModule ? semverModule.default : semverModule
const selections = new Map<string, Entry>()
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 exists = await lstat(desired)
.then((info) => info.isDirectory())
.catch(() => false)
if (!exists) {
continue
}
const relativeTarget = relative(parent, desired)
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
await rm(linkPath, { recursive: true, force: true })
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(" ...")
}
function parseEntry(label: string) {
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
if (marker <= 0) {
return null
}
const name = label.slice(0, marker).replace(/\+/g, "/")
const version = label.slice(marker + 1)
if (!name || !version) {
return null
}
return { name, version }
}

View File

@@ -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<string, string>
}
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<string>()
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<string>) {
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
}