feat(natives): fixed binary installs with embedded native addon extraction

- Added automatic extraction of embedded native addon payload to ~/.omp/natives/<version> 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.
This commit is contained in:
can1357
2026-02-01 17:16:53 +01:00
parent feb3a51d1e
commit 2a131d201e
8 changed files with 129 additions and 17 deletions
+14 -5
View File
@@ -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
+1
View File
@@ -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/<version>` on first run
### Removed
+1 -1
View File
@@ -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"
},
+5 -1
View File
@@ -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/<version>` 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/<version>` on first run
## [9.7.0] - 2026-02-01
+1
View File
@@ -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",
+24
View File
@@ -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);
+7
View File
@@ -0,0 +1,7 @@
export interface EmbeddedAddon {
platform: string;
version: string;
filePath: string;
}
export const embeddedAddon: EmbeddedAddon | null = null;
+76 -10
View File
@@ -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}`,
);
}