{ lib, stdenv, fetchFromGitHub, bun, nodejs, node-gyp, electron, autoPatchelfHook, makeWrapper, copyDesktopItems, imagemagick, python3, cacert, }: let version = "2.1.4"; src = fetchFromGitHub { owner = "iOfficeAI"; repo = "AionUi"; rev = "v${version}"; hash = "sha256-A7dKGEuo5n+M7D9fJR3TN95q1P7En/ulsTO+ev6SAUY="; }; # Fixed-output derivation: fetch all pure-JS dependencies via bun. # We skip install scripts because they use #!/usr/bin/env shebangs # that don't exist in the Nix sandbox. Native addons are built # separately in the main derivation. bunDeps = stdenv.mkDerivation { name = "aionui-bun-deps-${version}"; inherit src; nativeBuildInputs = [ bun cacert ]; outputHashAlgo = "sha256"; outputHashMode = "recursive"; outputHash = "sha256-Fuj55vxDgpx5A4iwnU/5IZKPfaYMH3DeZ1yauuZHn8g="; dontPatchShebangs = true; dontFixup = true; SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt"; buildPhase = '' export HOME=$TMPDIR bun install --frozen-lockfile --ignore-scripts ''; installPhase = '' mkdir -p $out cp -r node_modules $out/ cp -r packages $out/ # Workspace packages reference root configs with relative paths cp tsconfig.json $out/ ''; }; in stdenv.mkDerivation rec { pname = "aionui"; inherit version src; nativeBuildInputs = [ bun nodejs node-gyp autoPatchelfHook makeWrapper copyDesktopItems imagemagick python3 ]; buildInputs = [ stdenv.cc.cc.lib ]; buildPhase = '' runHook preBuild export HOME=$TMPDIR # Use the pre-fetched node_modules from the FOD (symlink farm) ln -sf ${bunDeps}/node_modules . # ── Build better-sqlite3 native addon ────────────────────── # The FOD output is read-only; build in a temp dir. echo "Building better-sqlite3 from source..." BT3_SRC=$TMPDIR/bts3-src BT3_OUT=$TMPDIR/bts3-out mkdir -p "$BT3_SRC" "$BT3_OUT" cp -rL node_modules/better-sqlite3/* "$BT3_SRC/" chmod -R u+w "$BT3_SRC" ( cd "$BT3_SRC" export npm_config_nodedir=${nodejs} node-gyp rebuild mkdir -p "$BT3_OUT/build/Release" cp build/Release/better_sqlite3.node "$BT3_OUT/build/Release/" 2>/dev/null || true ) # ── Build sharp native addon ────────────────────────────── echo "Building sharp from source..." SHARP_SRC=$TMPDIR/sharp-src SHARP_OUT=$TMPDIR/sharp-out mkdir -p "$SHARP_SRC" "$SHARP_OUT" cp -rL node_modules/sharp/* "$SHARP_SRC/" 2>/dev/null || true if [ -f "$SHARP_SRC/package.json" ]; then chmod -R u+w "$SHARP_SRC" ( cd "$SHARP_SRC" export npm_config_nodedir=${nodejs} node-gyp rebuild 2>/dev/null || echo " sharp: build skipped (needs libvips)" mkdir -p "$SHARP_OUT/build/Release" cp build/Release/sharp.node "$SHARP_OUT/build/Release/" 2>/dev/null || true ) fi # ── electron-vite build ─────────────────────────────────── echo "Running electron-vite build..." ${nodejs}/bin/node node_modules/.bin/electron-vite build \ --config packages/desktop/electron.vite.config.ts # ── Bundle MCP servers ──────────────────────────────────── echo "Building MCP servers..." ${nodejs}/bin/node scripts/build-mcp-servers.js runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out/{bin,lib/aionui} # ── Built output ────────────────────────────────────────── cp -r out $out/lib/aionui/ cp -r public $out/lib/aionui/ cp package.json $out/lib/aionui/ # ── Runtime dependencies ────────────────────────────────── # Copy node_modules (from the symlink) – the compiled output # has externalized require() calls for every dependency. cp -rL node_modules $out/lib/aionui/ # Remove prebuilt musl (Alpine) binaries from sharp — they # are incompatible with glibc and we built from source anyway. find $out/lib/aionui/node_modules -path "*/sharp-linuxmusl*" -type d -exec rm -rf {} + 2>/dev/null || true find $out/lib/aionui/node_modules -path "*/sharp-libvips-linuxmusl*" -type d -exec rm -rf {} + 2>/dev/null || true # ── Inject compiled native addons ───────────────────────── # cp -rL from the FOD preserves read-only permissions. # Make native addon dirs writable before overlaying binaries. chmod -R u+w $out/lib/aionui/node_modules/better-sqlite3 2>/dev/null || true chmod -R u+w $out/lib/aionui/node_modules/sharp 2>/dev/null || true BT3_OUT=$TMPDIR/bts3-out if [ -f "$BT3_OUT/build/Release/better_sqlite3.node" ]; then mkdir -p $out/lib/aionui/node_modules/better-sqlite3/build/Release cp "$BT3_OUT/build/Release/better_sqlite3.node" \ $out/lib/aionui/node_modules/better-sqlite3/build/Release/ fi SHARP_OUT=$TMPDIR/sharp-out if [ -f "$SHARP_OUT/build/Release/sharp.node" ]; then mkdir -p $out/lib/aionui/node_modules/sharp/build/Release cp "$SHARP_OUT/build/Release/sharp.node" \ $out/lib/aionui/node_modules/sharp/build/Release/ fi # ── Desktop entry ───────────────────────────────────────── mkdir -p $out/share/applications cat > $out/share/applications/aionui.desktop << EOF [Desktop Entry] Name=AionUi Comment=Free, open-source Cowork app with AI Agents Exec=$out/bin/aionui Icon=aionui Terminal=false Type=Application Categories=Office;Utility; EOF mkdir -p $out/share/icons/hicolor/256x256/apps if [ -f resources/app.png ]; then convert resources/app.png -resize 256x256 \ $out/share/icons/hicolor/256x256/apps/aionui.png fi # ── Launcher ────────────────────────────────────────────── makeWrapper ${electron}/bin/electron $out/bin/aionui \ --add-flags "$out/lib/aionui" runHook postInstall ''; dontStrip = true; autoPatchelfIgnoreMissingDeps = true; desktopItems = [ "aionui.desktop" ]; doCheck = false; passthru = { category = "AI Coding Agents"; updateScript = [ "nix-update" "--flake" ".#aionui" ]; }; meta = with lib; { description = "Free, open-source, Cowork app with AI Agents"; homepage = "https://github.com/iOfficeAI/AionUi"; changelog = "https://github.com/iOfficeAI/AionUi/releases/tag/v${version}"; license = licenses.asl20; sourceProvenance = with sourceTypes; [ fromSource ]; mainProgram = "aionui"; platforms = platforms.linux; }; }