From 2a131d201ecf891f3074cd997c2b9c558acc0803 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 1 Feb 2026 17:16:53 +0100 Subject: [PATCH] feat(natives): fixed binary installs with embedded native addon extraction - Added automatic extraction of embedded native addon payload to ~/.omp/natives/ on first run of compiled binaries. - Added embed:native build script for generating platform-specific native addon payloads embedded in compiled binaries. - Enhanced native addon loading to prioritize extracted embedded addon over downloaded versions. - Improved error messages with platform-specific guidance for compiled vs npm-installed binaries. - Refactored CI workflow to use reusable build_binary() function for cross-platform binary compilation. --- .github/workflows/ci.yml | 19 ++++-- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/package.json | 2 +- packages/natives/CHANGELOG.md | 6 +- packages/natives/package.json | 1 + packages/natives/scripts/embed-native.ts | 24 +++++++ packages/natives/src/embedded-addon.ts | 7 ++ packages/natives/src/native.ts | 86 +++++++++++++++++++++--- 8 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 packages/natives/scripts/embed-native.ts create mode 100644 packages/natives/src/embedded-addon.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7ce1c1ac..119f7fe4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,11 +167,20 @@ jobs: mkdir -p packages/coding-agent/binaries ENTRYPOINT="./packages/coding-agent/src/cli.ts" DEFINES="--define OMP_COMPILED=true --root ." - bun build --compile $DEFINES --target=bun-darwin-arm64 $ENTRYPOINT --outfile packages/coding-agent/binaries/omp-darwin-arm64 - bun build --compile $DEFINES --target=bun-darwin-x64 $ENTRYPOINT --outfile packages/coding-agent/binaries/omp-darwin-x64 - bun build --compile $DEFINES --target=bun-linux-x64 $ENTRYPOINT --outfile packages/coding-agent/binaries/omp-linux-x64 - bun build --compile $DEFINES --target=bun-linux-arm64 $ENTRYPOINT --outfile packages/coding-agent/binaries/omp-linux-arm64 - bun build --compile $DEFINES --target=bun-windows-x64 $ENTRYPOINT --outfile packages/coding-agent/binaries/omp-windows-x64.exe + build_binary() { + PLATFORM="$1" + ARCH="$2" + TARGET="$3" + OUTFILE="$4" + TARGET_PLATFORM="$PLATFORM" TARGET_ARCH="$ARCH" bun --cwd=packages/natives run embed:native + bun build --compile $DEFINES --target="$TARGET" "$ENTRYPOINT" --outfile "$OUTFILE" + } + build_binary darwin arm64 bun-darwin-arm64 packages/coding-agent/binaries/omp-darwin-arm64 + build_binary darwin x64 bun-darwin-x64 packages/coding-agent/binaries/omp-darwin-x64 + build_binary linux x64 bun-linux-x64 packages/coding-agent/binaries/omp-linux-x64 + build_binary linux arm64 bun-linux-arm64 packages/coding-agent/binaries/omp-linux-arm64 + build_binary win32 x64 bun-windows-x64 packages/coding-agent/binaries/omp-windows-x64.exe + bun --cwd=packages/natives run embed:native --reset - name: Stage native addons for release run: cp packages/natives/native/*.node packages/coding-agent/binaries/ - name: Create GitHub Release diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 12250c34c..5ed11c19d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,6 +21,7 @@ - Migrated bash command execution from ptree-based persistent sessions to native shell bindings with streaming support - Simplified bash executor to use brush-core native API instead of managing long-lived shell processes - Routed clipboard copy and image paste through native arboard bindings instead of shell commands +- Embedded native addon payload for compiled binaries and extract to `~/.omp/natives/` on first run ### Removed diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 55eeda8d8..483415a82 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -74,7 +74,7 @@ "scripts": { "check": "tsgo -p tsconfig.json", "format-prompts": "bun scripts/format-prompts.ts", - "build:binary": "cd ../.. && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp", + "build:binary": "cd ../.. && bun --cwd=packages/natives run embed:native && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset", "generate-template": "bun scripts/generate-template.ts", "test": "bun test" }, diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 5b21a0108..7c15fadcc 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Breaking Changes - Removed `resize()` function; use `PhotonImage.resize()` method instead @@ -14,6 +13,8 @@ ### Added +- Added automatic extraction of embedded native addon to `~/.omp/natives/` on first run for compiled binaries +- Added `embed:native` build script to embed platform-specific native addon payloads into compiled binaries - Exported `Shell` class for creating persistent shell sessions with `run()` method and session options - Exported `ShellOptions`, `ShellRunOptions`, and `ShellRunResult` types for shell session management - Exported `find()` function for file discovery with glob patterns and .gitignore support @@ -30,12 +31,15 @@ ### Changed +- Enhanced native addon loading to prioritize extracted embedded addon for compiled binaries before falling back to system paths +- Improved error messages to provide platform-specific guidance for addon loading failures, including manual download instructions for compiled binaries - Reorganized native bindings into modular type files with declaration merging via `NativeBindings` interface - Moved type definitions from implementation files to dedicated `types.ts` modules for better separation of concerns - Enhanced `SystemInfo` type with additional fields: `os`, `arch`, `hostname`, `shell`, `terminal`, `de`, `wm`, and `gpu` - Refactored module exports to use direct destructuring from native bindings instead of wrapper functions - Changed `PhotonImage` API to use instance methods (`resize()`, `encode()`) instead of standalone functions - Changed `PhotonImage` to use property accessors for `width` and `height` instead of getter methods +- Embedded native addon payload for compiled binaries and extract to `~/.omp/natives/` on first run ## [9.7.0] - 2026-02-01 diff --git a/packages/natives/package.json b/packages/natives/package.json index 298927eb1..ebf6bb1ce 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -18,6 +18,7 @@ "scripts": { "build:native": "bun scripts/build-native.ts", "dev:native": "bun scripts/build-native.ts --dev", + "embed:native": "bun scripts/embed-native.ts", "check": "biome check . && tsgo -p tsconfig.json", "fix": "biome check --write --unsafe .", "test": "bun run build:native && bun test", diff --git a/packages/natives/scripts/embed-native.ts b/packages/natives/scripts/embed-native.ts new file mode 100644 index 000000000..2e4b69775 --- /dev/null +++ b/packages/natives/scripts/embed-native.ts @@ -0,0 +1,24 @@ +import * as path from "node:path"; + +const reset = process.argv.includes("--reset"); + +const outputPath = path.join(import.meta.dir, "../src/embedded-addon.ts"); +const packageJsonPath = path.join(import.meta.dir, "../package.json"); + +const stubContent = "export interface EmbeddedAddon {\n\tplatform: string;\n\tversion: string;\n\tfilePath: string;\n}\n\nexport const embeddedAddon: EmbeddedAddon | null = null;\n"; + +if (reset) { + await Bun.write(outputPath, stubContent); + process.exit(0); +} + +const targetPlatform = process.env.TARGET_PLATFORM || process.platform; +const targetArch = process.env.TARGET_ARCH || process.arch; +const platformTag = `${targetPlatform}-${targetArch}`; +const addonFilename = `pi_natives.${platformTag}.node`; + +const packageJson = (await Bun.file(packageJsonPath).json()) as { version: string }; + +const content = `import addonPath from ${JSON.stringify("../native/" + addonFilename)} with { type: \"file\" };\n\nexport interface EmbeddedAddon {\n\tplatform: string;\n\tversion: string;\n\tfilePath: string;\n}\n\nexport const embeddedAddon: EmbeddedAddon | null = {\n\tplatform: ${JSON.stringify(platformTag)},\n\tversion: ${JSON.stringify(packageJson.version)},\n\tfilePath: addonPath,\n};\n`; + +await Bun.write(outputPath, content); diff --git a/packages/natives/src/embedded-addon.ts b/packages/natives/src/embedded-addon.ts new file mode 100644 index 000000000..7235b2b95 --- /dev/null +++ b/packages/natives/src/embedded-addon.ts @@ -0,0 +1,7 @@ +export interface EmbeddedAddon { + platform: string; + version: string; + filePath: string; +} + +export const embeddedAddon: EmbeddedAddon | null = null; diff --git a/packages/natives/src/native.ts b/packages/natives/src/native.ts index 75277c4b1..e22bf8a87 100644 --- a/packages/natives/src/native.ts +++ b/packages/natives/src/native.ts @@ -4,9 +4,13 @@ * Each module extends NativeBindings via declaration merging in its types.ts. */ +import * as fs from "node:fs"; import { createRequire } from "node:module"; +import * as os from "node:os"; import * as path from "node:path"; +import packageJson from "../package.json" with { type: "json" }; import type { NativeBindings } from "./bindings"; +import { embeddedAddon } from "./embedded-addon"; // Import types to trigger declaration merging import "./clipboard/types"; @@ -25,35 +29,87 @@ export type { NativeBindings, TsFunc } from "./bindings"; const require = createRequire(import.meta.url); const platformTag = `${process.platform}-${process.arch}`; +const addonFilename = `pi_natives.${platformTag}.node`; +const packageVersion = (packageJson as { version: string }).version; const nativeDir = path.join(import.meta.dir, "..", "native"); -const repoRoot = path.join(import.meta.dir, "..", "..", ".."); const execDir = path.dirname(process.execPath); +const versionedDir = path.join(os.homedir(), ".omp", "natives", packageVersion); +const versionedAddonPath = path.join(versionedDir, addonFilename); +const legacyUserDataDir = + process.platform === "win32" + ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "omp") + : path.join(os.homedir(), ".local", "bin"); +const downloadUrl = `https://github.com/can1357/oh-my-pi/releases/latest/download/${addonFilename}`; +const isCompiledBinary = + import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN"); const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"]; const debugCandidates = [path.join(nativeDir, "pi_natives.dev.node"), path.join(execDir, "pi_natives.dev.node")]; -const releaseCandidates = [ +const baseReleaseCandidates = [ // Platform-tagged builds (preferred - always correct platform) - path.join(nativeDir, `pi_natives.${platformTag}.node`), - path.join(execDir, `pi_natives.${platformTag}.node`), + path.join(nativeDir, addonFilename), + path.join(execDir, addonFilename), // Fallback untagged (only created for native builds, not cross-compilation) path.join(nativeDir, "pi_natives.node"), path.join(execDir, "pi_natives.node"), ]; +const compiledCandidates = [ + versionedAddonPath, + path.join(legacyUserDataDir, addonFilename), + path.join(legacyUserDataDir, "pi_natives.node"), +]; + +const releaseCandidates = isCompiledBinary ? [...compiledCandidates, ...baseReleaseCandidates] : baseReleaseCandidates; const candidates = process.env.OMP_DEV ? [...debugCandidates, ...releaseCandidates] : releaseCandidates; +function maybeExtractEmbeddedAddon(errors: string[]): string | null { + if (!isCompiledBinary || !embeddedAddon) return null; + if (embeddedAddon.platform !== platformTag || embeddedAddon.version !== packageVersion) return null; + + try { + fs.mkdirSync(versionedDir, { recursive: true }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + errors.push(`embedded addon dir: ${message}`); + return null; + } + + try { + fs.statSync(versionedAddonPath); + return versionedAddonPath; + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + const message = err instanceof Error ? err.message : String(err); + errors.push(`embedded addon stat: ${message}`); + return null; + } + } + + try { + const buffer = fs.readFileSync(embeddedAddon.filePath); + fs.writeFileSync(versionedAddonPath, buffer); + return versionedAddonPath; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + errors.push(`embedded addon write: ${message}`); + return null; + } +} + function loadNative(): NativeBindings { const errors: string[] = []; + const embeddedCandidate = maybeExtractEmbeddedAddon(errors); + const runtimeCandidates = embeddedCandidate ? [embeddedCandidate, ...candidates] : candidates; - for (const candidate of candidates) { + for (const candidate of runtimeCandidates) { try { const bindings = require(candidate) as NativeBindings; validateNative(bindings, candidate); if (process.env.OMP_DEV) { console.log(`Loaded native addon from ${candidate}`); - console.log(` - Root: ${repoRoot}`); } return bindings; } catch (err) { @@ -75,11 +131,21 @@ function loadNative(): NativeBindings { } const details = errors.map(error => `- ${error}`).join("\n"); - throw new Error( - `Failed to load pi_natives native addon for ${platformTag}.\n\n` + - `Tried:\n${details}\n\n` + + let helpMessage: string; + if (isCompiledBinary) { + helpMessage = + `The compiled binary should extract the native addon to:\n` + + ` ${versionedAddonPath}\n\n` + + `If it is missing, delete ${versionedDir} and re-run, or download manually:\n` + + ` curl -fsSL "${downloadUrl}" -o "${versionedAddonPath}"`; + } else { + helpMessage = "If installed via npm/bun, try reinstalling: bun install @oh-my-pi/pi-natives\n" + - "If developing locally, build with: bun --cwd=packages/natives run build:native", + "If developing locally, build with: bun --cwd=packages/natives run build:native"; + } + + throw new Error( + `Failed to load pi_natives native addon for ${platformTag}.\n\n` + `Tried:\n${details}\n\n${helpMessage}`, ); }