222 lines
8.6 KiB
Nix
222 lines
8.6 KiB
Nix
{ bun2nix, fetchFromGitHub, fetchurl, fetchzip, runCommand, python3, pkgs, stdenv, ... }:
|
|
|
|
# NOTE: This derivation works around two open bun2nix bugs. Remove the
|
|
# workarounds and simplify once they are fixed upstream.
|
|
#
|
|
# Bug 1 — missing .npm manifest cache files
|
|
# https://github.com/nix-community/bun2nix/issues/77
|
|
#
|
|
# bun's install cache requires two kinds of entries per package:
|
|
# - the extracted package directory (e.g. handlebars@4.7.9@@@1/)
|
|
# - a hashed .npm manifest file (e.g. 02dd05ab1686ff3a.npm)
|
|
# bun2nix only provides the former. bun therefore fetches the manifest from
|
|
# the registry during the "Resolving" phase, which fails in the Nix sandbox.
|
|
#
|
|
# Workaround: pass --offline to bun install. This tells bun to skip manifest
|
|
# fetches and trust the lockfile for resolution instead.
|
|
#
|
|
# Bug 2 — catalog: specifiers still trigger network resolution with --offline
|
|
# https://github.com/nix-community/bun2nix/issues/77 (same thread)
|
|
#
|
|
# bun2nix's bunResolveCatalogRefs rewrites "catalog:" specifiers in
|
|
# package.json to the version *range* from the catalog table (e.g. "^1.3.14")
|
|
# rather than the exact pinned version from bun.lock's packages section.
|
|
# Even with --offline, bun reads the catalog table from bun.lock and tries to
|
|
# resolve those ranges, hitting the network.
|
|
#
|
|
# Workaround: the Python script below pre-processes the source before the
|
|
# build. It pins every dep in every workspace package.json to the exact
|
|
# version from bun.lock's packages section (so no ranges remain for bun to
|
|
# resolve), and strips the catalog/catalogs keys from bun.lock entirely.
|
|
#
|
|
# bun.lock is JSONC (trailing-comma JSON) so we parse it with Python's stdlib
|
|
# json after stripping trailing commas with a regex.
|
|
#
|
|
# Additionally, oh-my-pi's bun.lock was generated with bun >=1.3.14 which uses
|
|
# a different Wyhash seed for cache keys than nixpkgs's bun 1.3.13. Bumping bun
|
|
# globally breaks other packages (e.g. opencode), so instead we patch the
|
|
# generated wrapper script in postInstall to reference bun 1.3.14 directly.
|
|
# Hashes from https://github.com/NixOS/nixpkgs/pull/519796
|
|
|
|
let
|
|
version = "15.2.1";
|
|
|
|
# bun 1.3.14 — needed for correct cache key hashes; scoped to this package.
|
|
bun_1_3_14 = pkgs.bun.overrideAttrs (_: {
|
|
version = "1.3.14";
|
|
src =
|
|
let
|
|
sources = {
|
|
"aarch64-darwin" = fetchurl {
|
|
url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-darwin-aarch64.zip";
|
|
hash = "sha256-2LliIYKK1vl6x6wKt+lYcjQa92MAHogD6CZ2UsJlJiA=";
|
|
};
|
|
"aarch64-linux" = fetchurl {
|
|
url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-aarch64.zip";
|
|
hash = "sha256-on/7Y6gxA3WDbg1vZorhf6jY0YuIw3yCHGUzGXOhmjs=";
|
|
};
|
|
"x86_64-darwin" = fetchurl {
|
|
url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-darwin-x64-baseline.zip";
|
|
hash = "sha256-PjWtb1OXGpg0v55nhuKt9ytfGSHMmpxf3gc9KXKUQHY=";
|
|
};
|
|
"x86_64-linux" = fetchurl {
|
|
url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip";
|
|
hash = "sha256-lR7iruhV8IWVruxiJSJqKY0/6oOj3NZGXAnLzN9+hI8=";
|
|
};
|
|
};
|
|
in
|
|
sources.${stdenv.hostPlatform.system} or (throw "bun 1.3.14 not available for ${stdenv.hostPlatform.system}");
|
|
});
|
|
|
|
src = fetchFromGitHub {
|
|
owner = "can1357";
|
|
repo = "oh-my-pi";
|
|
rev = "v${version}";
|
|
hash = "sha256-fztQJrhDG5ZbTlgqoHA96eCgwYm5WIna3mAPlCDWYLM=";
|
|
};
|
|
|
|
# The workspace source for @oh-my-pi/pi-natives has no pre-built .node
|
|
# binaries — those only exist in the npm tarball. Fetch it so we can copy
|
|
# the platform binaries into packages/natives/native/ before the build.
|
|
piNativesTarball = fetchzip {
|
|
url = "https://registry.npmjs.org/@oh-my-pi/pi-natives/-/pi-natives-${version}.tgz";
|
|
hash = "sha256-mEEnvTNxWFVSs1An61K83sSjUJ5bz4yrluwZvz1+6fg=";
|
|
stripRoot = false;
|
|
};
|
|
|
|
srcWithBunNix = runCommand "oh-my-pi-src" {
|
|
nativeBuildInputs = [ bun2nix bun_1_3_14 python3 ];
|
|
} ''
|
|
cp -r ${src} $out
|
|
chmod -R u+w $out
|
|
|
|
# Copy pre-built .node binaries from the npm tarball into the workspace
|
|
# source so the runtime can load the native addon without building Rust.
|
|
cp ${piNativesTarball}/package/native/*.node $out/packages/natives/native/
|
|
|
|
bun2nix --lock-file $out/bun.lock --output-file $out/bun.nix
|
|
|
|
python3 - "$out" << 'EOF'
|
|
import sys, re, json, os
|
|
|
|
root = sys.argv[1]
|
|
lock_path = os.path.join(root, "bun.lock")
|
|
|
|
raw = open(lock_path).read()
|
|
lock = json.loads(re.sub(r',(\s*[}\]])', r'\1', raw))
|
|
|
|
packages = lock.get("packages", {})
|
|
catalog = lock.get("catalog", {})
|
|
catalogs = lock.get("catalogs", {})
|
|
|
|
# Build name -> exact resolved version from the packages section.
|
|
resolved = {}
|
|
for name, entry in packages.items():
|
|
if isinstance(entry, list) and entry and isinstance(entry[0], str):
|
|
spec = entry[0]
|
|
if spec.startswith(name + "@"):
|
|
resolved[name] = spec[len(name) + 1:]
|
|
|
|
def pin(name, spec):
|
|
"""Pin a dep specifier to its exact resolved version from bun.lock."""
|
|
if not isinstance(spec, str):
|
|
return spec
|
|
# catalog: specifiers — resolve via catalog table then pinned version.
|
|
if spec.startswith("catalog:"):
|
|
cname = spec[len("catalog:"):]
|
|
table = catalog if cname == "" else catalogs.get(cname, {})
|
|
rv = resolved.get(name)
|
|
cv = table.get(name)
|
|
if isinstance(rv, str) and rv.startswith("workspace:"):
|
|
return "workspace:*"
|
|
if isinstance(rv, str):
|
|
return rv
|
|
if isinstance(cv, str):
|
|
return cv
|
|
return spec
|
|
# Any npm version range — pin to exact resolved version.
|
|
if not spec.startswith(("workspace:", "file:", "link:", "git", "http", "/")):
|
|
rv = resolved.get(name)
|
|
if isinstance(rv, str) and rv.startswith("workspace:"):
|
|
return "workspace:*"
|
|
if isinstance(rv, str):
|
|
return rv
|
|
return spec
|
|
|
|
sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]
|
|
|
|
def rewrite(holder):
|
|
for sec in sections:
|
|
deps = holder.get(sec)
|
|
if isinstance(deps, dict):
|
|
for name in list(deps):
|
|
deps[name] = pin(name, deps[name])
|
|
|
|
# Rewrite bun.lock workspaces and drop the catalog tables.
|
|
for ws in lock.get("workspaces", {}).values():
|
|
rewrite(ws)
|
|
lock.pop("catalog", None)
|
|
lock.pop("catalogs", None)
|
|
open(lock_path, "w").write(json.dumps(lock, indent=2) + "\n")
|
|
|
|
# Rewrite each workspace package.json (root "" included).
|
|
for ws_dir in lock.get("workspaces", {}):
|
|
pkg_path = os.path.join(root, ws_dir, "package.json")
|
|
if not os.path.exists(pkg_path):
|
|
continue
|
|
pkg = json.loads(open(pkg_path).read())
|
|
rewrite(pkg)
|
|
open(pkg_path, "w").write(json.dumps(pkg, indent=2) + "\n")
|
|
EOF
|
|
'';
|
|
in
|
|
|
|
(bun2nix.writeBunApplication {
|
|
pname = "omp";
|
|
inherit version;
|
|
|
|
src = srcWithBunNix;
|
|
|
|
# oh-my-pi requires bun >=1.3.14 at runtime. writeBunApplication prepends
|
|
# pkgs.bun (1.3.13) to PATH in the startup script, so we use an absolute
|
|
# path to bun 1.3.14 instead of relying on PATH resolution.
|
|
#
|
|
# writeBunApplication's installPhase does `cd $out/share/$pname` before
|
|
# exec, so $PWD is always the store path. OLDPWD is set by bash's cd to the
|
|
# user's original directory. We cd back so omp's process.cwd() is correct,
|
|
# and use an absolute path to the entry point so bun resolves modules from
|
|
# the store regardless of cwd.
|
|
# At this point the wrapper has already done `cd $out/share/omp`, so $PWD
|
|
# is the store package dir and OLDPWD is the user's original directory.
|
|
# Capture the store dir, cd back to the user's dir so omp's process.cwd()
|
|
# is correct, then exec bun with an absolute path so module resolution
|
|
# still works from the store.
|
|
startScript = ''
|
|
_omp_pkg="$PWD"
|
|
cd "''${OLDPWD:-$PWD}"
|
|
exec ${bun_1_3_14}/bin/bun run "$_omp_pkg/packages/coding-agent/src/cli.ts" "$@"
|
|
'';
|
|
|
|
dontUseBunBuild = true;
|
|
dontUseBunCheck = true;
|
|
|
|
# --offline: workaround for Bug 1 above (missing .npm manifest cache files).
|
|
bunInstallFlags = [ "--offline" "--linker=isolated" "--ignore-scripts" ];
|
|
|
|
# Generate the docs index embedded into the binary at build time.
|
|
# The prepack script reads docs/**/*.md and emits docs-index.generated.ts.
|
|
postBunNodeModulesInstallPhase = ''
|
|
${bun_1_3_14}/bin/bun run packages/coding-agent/scripts/generate-docs-index.ts
|
|
'';
|
|
|
|
bunDeps = bun2nix.fetchBunDeps {
|
|
bunNix = "${srcWithBunNix}/bun.nix";
|
|
};
|
|
|
|
meta = {
|
|
description = "AI coding agent for the terminal — batteries included";
|
|
homepage = "https://omp.sh";
|
|
mainProgram = "omp";
|
|
};
|
|
})
|